From b370591e64dc04527a1dccb68be3fa8d7bc28340 Mon Sep 17 00:00:00 2001 From: Norman Feske Date: Wed, 21 Feb 2024 14:42:23 +0100 Subject: [PATCH] Mobile version of Sculpt OS This patch contains the mobile variant of Sculpt OS, which evolved at the genode-allwinner repository until now. In consists of the following parts: - gems/src/app/phone_manager plays the role of the sculpt manager - sculpt/phone-linux allows for test driving the mobile variant on base-linux - gems/src/app/dummy_modem mockup of a modem's behavior, used for GUI development and testing The parts targeting a specific device (PinePhone) remain local to the genode-allwinner repository. To give it a try: make run/sculpt_test KERNEL=linux BOARD=linux \ SCULPT=phone LOG=core DEPOT=tar Fixes #5125 --- repos/gems/sculpt/drivers/phone_linux | 63 + repos/gems/sculpt/event_filter/phone_linux | 9 + repos/gems/sculpt/fonts/phone | 21 + repos/gems/sculpt/launcher/direct_nano3d | 6 + .../gems/sculpt/launcher/direct_system_shell | 15 + repos/gems/sculpt/leitzentrale/phone | 169 ++ repos/gems/sculpt/nitpicker/phone | 28 + repos/gems/sculpt/phone-linux.sculpt | 12 + repos/gems/src/app/dummy_modem/main.cc | 256 ++ repos/gems/src/app/dummy_modem/target.mk | 3 + repos/gems/src/app/phone_manager/feature.h | 30 + repos/gems/src/app/phone_manager/main.cc | 2418 +++++++++++++++++ .../app/phone_manager/model/audio_volume.h | 21 + .../app/phone_manager/model/current_call.h | 215 ++ .../app/phone_manager/model/dialed_number.h | 84 + .../src/app/phone_manager/model/mic_state.h | 21 + .../src/app/phone_manager/model/modem_state.h | 195 ++ .../src/app/phone_manager/model/power_state.h | 102 + .../src/app/phone_manager/model/sim_pin.h | 123 + .../phone_manager/runtime/touch_keyboard.h | 89 + repos/gems/src/app/phone_manager/target.mk | 15 + .../phone_manager/view/component_add_widget.h | 250 ++ .../view/component_info_widget.h | 34 + .../view/conditional_float_widget.h | 83 + .../phone_manager/view/current_call_widget.h | 165 ++ .../view/device_controls_widget.h | 148 + .../phone_manager/view/device_power_widget.h | 210 ++ .../app/phone_manager/view/dialpad_widget.h | 86 + .../phone_manager/view/index_menu_widget.h | 178 ++ .../app/phone_manager/view/index_pkg_widget.h | 106 + .../phone_manager/view/modem_power_widget.h | 50 + .../app/phone_manager/view/outbound_widget.h | 46 + .../src/app/phone_manager/view/pin_widget.h | 88 + .../phone_manager/view/selectable_title_bar.h | 84 + .../phone_manager/view/software_add_widget.h | 271 ++ .../view/software_options_widget.h | 89 + .../phone_manager/view/software_tabs_widget.h | 89 + 37 files changed, 5872 insertions(+) create mode 100644 repos/gems/sculpt/drivers/phone_linux create mode 100644 repos/gems/sculpt/event_filter/phone_linux create mode 100644 repos/gems/sculpt/fonts/phone create mode 100644 repos/gems/sculpt/launcher/direct_nano3d create mode 100644 repos/gems/sculpt/launcher/direct_system_shell create mode 100644 repos/gems/sculpt/leitzentrale/phone create mode 100644 repos/gems/sculpt/nitpicker/phone create mode 100644 repos/gems/sculpt/phone-linux.sculpt create mode 100644 repos/gems/src/app/dummy_modem/main.cc create mode 100644 repos/gems/src/app/dummy_modem/target.mk create mode 100644 repos/gems/src/app/phone_manager/feature.h create mode 100644 repos/gems/src/app/phone_manager/main.cc create mode 100644 repos/gems/src/app/phone_manager/model/audio_volume.h create mode 100644 repos/gems/src/app/phone_manager/model/current_call.h create mode 100644 repos/gems/src/app/phone_manager/model/dialed_number.h create mode 100644 repos/gems/src/app/phone_manager/model/mic_state.h create mode 100644 repos/gems/src/app/phone_manager/model/modem_state.h create mode 100644 repos/gems/src/app/phone_manager/model/power_state.h create mode 100644 repos/gems/src/app/phone_manager/model/sim_pin.h create mode 100644 repos/gems/src/app/phone_manager/runtime/touch_keyboard.h create mode 100644 repos/gems/src/app/phone_manager/target.mk create mode 100644 repos/gems/src/app/phone_manager/view/component_add_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/component_info_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/conditional_float_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/current_call_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/device_controls_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/device_power_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/dialpad_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/index_menu_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/index_pkg_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/modem_power_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/outbound_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/pin_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/selectable_title_bar.h create mode 100644 repos/gems/src/app/phone_manager/view/software_add_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/software_options_widget.h create mode 100644 repos/gems/src/app/phone_manager/view/software_tabs_widget.h diff --git a/repos/gems/sculpt/drivers/phone_linux b/repos/gems/sculpt/drivers/phone_linux new file mode 100644 index 0000000000..bc37fb4d57 --- /dev/null +++ b/repos/gems/sculpt/drivers/phone_linux @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/repos/gems/sculpt/event_filter/phone_linux b/repos/gems/sculpt/event_filter/phone_linux new file mode 100644 index 0000000000..24c6d88506 --- /dev/null +++ b/repos/gems/sculpt/event_filter/phone_linux @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/repos/gems/sculpt/fonts/phone b/repos/gems/sculpt/fonts/phone new file mode 100644 index 0000000000..b2330062e3 --- /dev/null +++ b/repos/gems/sculpt/fonts/phone @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/repos/gems/sculpt/launcher/direct_nano3d b/repos/gems/sculpt/launcher/direct_nano3d new file mode 100644 index 0000000000..4de7c496ec --- /dev/null +++ b/repos/gems/sculpt/launcher/direct_nano3d @@ -0,0 +1,6 @@ + + + + + + diff --git a/repos/gems/sculpt/launcher/direct_system_shell b/repos/gems/sculpt/launcher/direct_system_shell new file mode 100644 index 0000000000..8356892e47 --- /dev/null +++ b/repos/gems/sculpt/launcher/direct_system_shell @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/repos/gems/sculpt/leitzentrale/phone b/repos/gems/sculpt/leitzentrale/phone new file mode 100644 index 0000000000..d45c2d4321 --- /dev/null +++ b/repos/gems/sculpt/leitzentrale/phone @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/repos/gems/sculpt/nitpicker/phone b/repos/gems/sculpt/nitpicker/phone new file mode 100644 index 0000000000..bcf4f0df01 --- /dev/null +++ b/repos/gems/sculpt/nitpicker/phone @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/repos/gems/sculpt/phone-linux.sculpt b/repos/gems/sculpt/phone-linux.sculpt new file mode 100644 index 0000000000..c6a54f002a --- /dev/null +++ b/repos/gems/sculpt/phone-linux.sculpt @@ -0,0 +1,12 @@ +drivers: phone_linux +leitzentrale: phone +nitpicker: phone +fonts: phone +event_filter: phone_linux +build: app/phone_manager server/nitpicker app/menu_view server/wm +build: server/event_filter +build: app/dummy_modem +import: pkg/drivers_interactive-linux pkg/touch_keyboard +deploy: example +ram_fs: depot +launcher: sticks_blue_backdrop direct_nano3d direct_system_shell diff --git a/repos/gems/src/app/dummy_modem/main.cc b/repos/gems/src/app/dummy_modem/main.cc new file mode 100644 index 0000000000..a5f6793ee9 --- /dev/null +++ b/repos/gems/src/app/dummy_modem/main.cc @@ -0,0 +1,256 @@ +/* + * \brief Simulation of a modem driver + * \author Norman Feske + * \date 2022-06-02 + */ + +/* Genode includes */ +#include +#include +#include +#include + +namespace Dummy_modem { + + using namespace Genode; + + struct Main; +} + + +struct Dummy_modem::Main +{ + Env &_env; + + Expanding_reporter _reporter { _env, "modem", "state" }; + + enum class Power_state { UNKNOWN, OFF, STARTING_UP, ON, SHUTTING_DOWN }; + + Power_state _power_state = Power_state::ON; + + unsigned _startup_seconds = 0; + unsigned _shutdown_seconds = 0; + + struct Pin + { + enum class State { REQUIRED, CHECKING, OK, PUK_NEEDED }; + State state = State::OK; + unsigned check_countdown_seconds = 0; + + enum { INITIAL_REMAINING_ATTEMPTS = 3 }; + unsigned remaining_attempts = INITIAL_REMAINING_ATTEMPTS; + + using Code = String<10>; + Code current_code { }; + Code failed_code { }; + + Pin() { } + }; + + Pin _pin { }; + + void _generate_report(Xml_generator &xml) const + { + auto power_value = [] (Power_state state) + { + switch (state) { + case Power_state::OFF: return "off"; + case Power_state::STARTING_UP: return "starting up"; + case Power_state::ON: return "on"; + case Power_state::SHUTTING_DOWN: return "shutting down"; + case Power_state::UNKNOWN: break; + } + return ""; + }; + + xml.attribute("power", power_value(_power_state)); + + if (_power_state == Power_state::STARTING_UP) + xml.attribute("startup_seconds", _startup_seconds); + + if (_power_state == Power_state::SHUTTING_DOWN) + xml.attribute("shutdown_seconds", _shutdown_seconds); + + auto pin_value = [] (Pin::State state) + { + switch (state) { + case Pin::State::REQUIRED: return "required"; + case Pin::State::CHECKING: return "checking"; + case Pin::State::OK: return "ok"; + case Pin::State::PUK_NEEDED: return "puk needed"; + } + return ""; + }; + + if (_power_state == Power_state::ON) { + xml.attribute("pin", pin_value(_pin.state)); + + if (_pin.remaining_attempts != Pin::INITIAL_REMAINING_ATTEMPTS) + xml.attribute("pin_remaining_attempts", _pin.remaining_attempts); + } + + if (_pin.state == Pin::State::OK) { + + enum Scenario { IDLE, INCOMING_CALL, INITIATED_CALL }; + + Scenario scenario = INITIATED_CALL; + + if (scenario == INCOMING_CALL) { + xml.node("call", [&] { + xml.attribute("number", "+49123456789"); + xml.attribute("state", "incoming"); + }); + } + + if (scenario == INITIATED_CALL) { + xml.node("call", [&] { + xml.attribute("number", "+4911223344"); + xml.attribute("state", "outbound"); + }); + } + } + } + + void _update_state_report() + { + _reporter.generate([&] (Xml_generator &xml) { + _generate_report(xml); }); + } + + Timer::Connection _timer { _env }; + + Signal_handler
_timer_handler { + _env.ep(), *this, &Main::_handle_timer }; + + Attached_rom_dataspace _config { _env, "config" }; + + Signal_handler
_config_handler { + _env.ep(), *this, &Main::_handle_config }; + + void _handle_timer() + { + /* apply time-driven rules */ + if (_power_state == Power_state::STARTING_UP) { + _startup_seconds++; + if (_startup_seconds > 4) { + _power_state = Power_state::ON; + _startup_seconds = 0; + } + } + + if (_power_state == Power_state::SHUTTING_DOWN) { + _shutdown_seconds++; + if (_shutdown_seconds > 5) { + _power_state = Power_state::OFF; + _shutdown_seconds = 0; + _pin = Pin { }; + } + } + + switch (_pin.state) { + + case Pin::State::REQUIRED: + case Pin::State::OK: + case Pin::State::PUK_NEEDED: + break; + + case Pin::State::CHECKING: + if (_pin.check_countdown_seconds > 0) + _pin.check_countdown_seconds--; + + if (_pin.check_countdown_seconds == 0) { + if (_pin.current_code == "1234") { + _pin.state = Pin::State::OK; + } else { + _pin.state = Pin::State::REQUIRED; + _pin.failed_code = _pin.current_code; + if (_pin.remaining_attempts == 0) + _pin.state = Pin::State::PUK_NEEDED; + else + _pin.remaining_attempts--; + } + } else { + _trigger_timer_in_one_second(); + } + break; + } + + /* re-apply rules dictated by the configuration */ + _handle_config(); + } + + void _trigger_timer_in_one_second() { _timer.trigger_once(1000*1000UL); } + + void _handle_config() + { + _config.update(); + + Xml_node const config = _config.xml(); + log("_handle_config: ", config); + + auto power_from_config = [&] + { + auto name = config.attribute_value("power", String<10>()); + + if (name == "on") return Power_state::ON; + if (name == "off") return Power_state::OFF; + + return Power_state::UNKNOWN; + }; + + Power_state const requested_power_state = power_from_config(); + + if (requested_power_state != _power_state) { + + if (requested_power_state == Power_state::ON) { + if (_power_state == Power_state::OFF) + _power_state = Power_state::STARTING_UP; + } + + if (requested_power_state == Power_state::OFF) { + if (_power_state == Power_state::ON) + _power_state = Power_state::SHUTTING_DOWN; + } + + if (requested_power_state != Power_state::UNKNOWN) + _trigger_timer_in_one_second(); + } + + switch (_pin.state) { + + case Pin::State::REQUIRED: + if (config.has_attribute("pin")) { + Pin::Code const code = config.attribute_value("pin", Pin::Code()); + /* don't re-submit failed pin */ + if (code != _pin.failed_code) { + _pin.current_code = code; + _pin.state = Pin::State::CHECKING; + _pin.check_countdown_seconds = 2; + _trigger_timer_in_one_second(); + } + } + break; + + case Pin::State::CHECKING: /* handled by _handle_timer */ + case Pin::State::OK: + case Pin::State::PUK_NEEDED: + break; + } + + _update_state_report(); + } + + Main(Env &env) : _env(env) + { + _timer.sigh(_timer_handler); + _config.sigh(_config_handler); + _handle_config(); + } +}; + + +void Component::construct(Genode::Env &env) +{ + static Dummy_modem::Main main(env); +} + diff --git a/repos/gems/src/app/dummy_modem/target.mk b/repos/gems/src/app/dummy_modem/target.mk new file mode 100644 index 0000000000..f4e633b791 --- /dev/null +++ b/repos/gems/src/app/dummy_modem/target.mk @@ -0,0 +1,3 @@ +TARGET := dummy_modem +SRC_CC := main.cc +LIBS := base diff --git a/repos/gems/src/app/phone_manager/feature.h b/repos/gems/src/app/phone_manager/feature.h new file mode 100644 index 0000000000..0df990f04c --- /dev/null +++ b/repos/gems/src/app/phone_manager/feature.h @@ -0,0 +1,30 @@ +/* + * \brief Compile-time feature selection + * \author Norman Feske + * \date 2022-11-10 + */ + +/* + * Copyright (C) 2022 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _FEATURE_H_ +#define _FEATURE_H_ + +#include + +namespace Sculpt { struct Feature; }; + + +struct Sculpt::Feature +{ + static constexpr bool PRESENT_PLUS_MENU = false; + static constexpr bool STORAGE_DIALOG_HOSTED_IN_GRAPH = false; + static constexpr bool INSPECT_VIEW = false; + static constexpr bool VISUAL_HOVER = false; +}; + +#endif /* _FEATURE_H_ */ diff --git a/repos/gems/src/app/phone_manager/main.cc b/repos/gems/src/app/phone_manager/main.cc new file mode 100644 index 0000000000..2d29e46bf9 --- /dev/null +++ b/repos/gems/src/app/phone_manager/main.cc @@ -0,0 +1,2418 @@ +/* + * \brief Sculpt system manager for a phone + * \author Norman Feske + * \date 2022-05-20 + * + * Based on repos/gems/src/app/sculpt_manager/main.cc + */ + +/* + * Copyright (C) 2022 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +/* Genode includes */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* local includes */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Sculpt { struct Main; } + + +struct Sculpt::Main : Input_event_handler, + Runtime_config_generator, + Deploy::Action, + Storage::Action, + Network::Action, + Network::Info, + Graph::Action, + Depot_query, + Component::Construction_info, + Device_controls_widget::Action, + Device_power_widget::Action, + Modem_power_widget::Action, + Pin_widget::Action, + Dialpad_widget::Action, + Current_call_widget::Action, + Software_presets_widget::Action, + Software_options_widget::Action, + Software_update_widget::Action, + Software_add_widget::Action, + Screensaver::Action +{ + Env &_env; + + Heap _heap { _env.ram(), _env.rm() }; + + Sculpt_version const _sculpt_version { _env }; + + Build_info const _build_info = + Build_info::from_xml(Attached_rom_dataspace(_env, "build_info").xml()); + + Registry _child_states { }; + + Input::Seq_number _global_input_seq_number { }; + + Gui::Connection _gui { _env, "input" }; + + bool _gui_mode_ready = false; /* becomes true once the graphics driver is up */ + + Gui::Root _gui_root { _env, _heap, *this, _global_input_seq_number }; + + Signal_handler
_input_handler { + _env.ep(), *this, &Main::_handle_input }; + + void _handle_input() + { + _gui.input()->for_each_event([&] (Input::Event const &ev) { + handle_input_event(ev); }); + } + + Managed_config
_system_config { + _env, "system", "system", *this, &Main::_handle_system_config }; + + struct System + { + bool usb; + bool storage; + + using State = String<32>; + + State state; + + using Power_profile = String<32>; + + Power_profile power_profile; + + unsigned brightness; + + bool display; + + static System from_xml(Xml_node const &node) + { + return System { + .usb = node.attribute_value("usb", false), + .storage = node.attribute_value("storage", false), + .state = node.attribute_value("state", State()), + .power_profile = node.attribute_value("power_profile", Power_profile()), + .brightness = node.attribute_value("brightness", 0u), + .display = node.attribute_value("display", true) + }; + } + + void generate(Xml_generator &xml) const + { + if (usb) xml.attribute("usb", "yes"); + if (storage) xml.attribute("storage", "yes"); + + if (state.length() > 1) + xml.attribute("state", state); + + if (power_profile.length() > 1) { + if (power_profile == "performance" && !display) + xml.attribute("power_profile", "economic"); + else + xml.attribute("power_profile", power_profile); + } + + xml.attribute("brightness", brightness); + xml.attribute("display", display ? "yes" : "no"); + } + + bool operator != (System const &other) const + { + return (other.usb != usb) + || (other.storage != storage) + || (other.state != state) + || (other.power_profile != power_profile) + || (other.brightness != brightness) + || (other.display != display); + } + + } _system { }; + + void _update_managed_system_config() + { + _system_config.generate([&] (Xml_generator &xml) { + _system.generate(xml); }); + } + + void _handle_system_config(Xml_node node) + { + _system = System::from_xml(node); + _update_managed_system_config(); + } + + void _enter_second_driver_stage() + { + /* + * At the first stage, we start only the drivers needed for the + * bare-bones GUI functionality needed to pick up a call. Once the GUI + * is up, we can kick off the start of the remaining drivers. + */ + + if (_system.usb && _system.storage) + return; + + System const orig_system = _system; + + _system.usb = true; + _system.storage = true; + + if (_system != orig_system) + _update_managed_system_config(); + } + + Signal_handler
_gui_mode_handler { + _env.ep(), *this, &Main::_handle_gui_mode }; + + void _handle_gui_mode(); + + bool _verbose_modem = false; + + Attached_rom_dataspace _config { _env, "config" }; + + Signal_handler
_config_handler { + _env.ep(), *this, &Main::_handle_config }; + + void _handle_config() + { + _config.update(); + + Xml_node const config = _config.xml(); + + _verbose_modem = config.attribute_value("verbose_modem", false); + } + + Screensaver _screensaver { _env, *this }; + + /** + * Screensaver::Action interface + */ + void screensaver_changed() override + { + _system.display = _screensaver.display_enabled(); + _update_managed_system_config(); + } + + Attached_rom_dataspace _leitzentrale_rom { _env, "leitzentrale" }; + + Signal_handler
_leitzentrale_handler { + _env.ep(), *this, &Main::_handle_leitzentrale }; + + void _handle_leitzentrale() + { + _leitzentrale_rom.update(); + + _leitzentrale_visible = _leitzentrale_rom.xml().attribute_value("enabled", false); + + /* disable automatic blanking while the application runtime is visible */ + _screensaver.blank_after_some_time(_leitzentrale_visible); + + _handle_window_layout(); + } + + + /*************************** + ** Configuration loading ** + ***************************/ + + Prepare_version _prepare_version { 0 }; + Prepare_version _prepare_completed { 0 }; + + bool _prepare_in_progress() const + { + return _prepare_version.value != _prepare_completed.value; + } + + Storage _storage { _env, _heap, _child_states, *this, *this }; + + /** + * Storage::Action interface + */ + void use_storage_target(Storage_target const &target) override + { + _storage._sculpt_partition = target; + + /* trigger loading of the configuration from the sculpt partition */ + _prepare_version.value++; + + _deploy.restart(); + + generate_runtime_config(); + } + + /** + * Storage::Action interface + */ + void refresh_storage_dialog() override { _generate_dialog(); } + + Pci_info _pci_info { .wifi_present = true, + .modem_present = true }; + + Network _network { _env, _heap, *this, *this, _child_states, *this, _runtime_state, _pci_info }; + + /** + * Network::Action interface + */ + void update_network_dialog() override + { + _generate_dialog(); + } + + /** + * Network::Info interface + */ + bool ap_list_hovered() const override + { + return _main_view.if_hovered([&] (Hovered_at const &at) { + return _network_widget.if_hovered(at, [&] (Hovered_at const &at) { + return _network_widget.hosted.if_hovered(at, [&] (Hovered_at const &at) { + return _network_widget.hosted.ap_list_hovered(at); }); }); }); + } + + + /************ + ** Update ** + ************/ + + Attached_rom_dataspace _update_state_rom { + _env, "report -> runtime/update/state" }; + + void _handle_update_state(); + + Signal_handler
_update_state_handler { + _env.ep(), *this, &Main::_handle_update_state }; + + /** + * Condition for spawning the update subsystem + */ + bool _update_running() const + { + return _storage._sculpt_partition.valid() + && !_prepare_in_progress() + && _network.ready() + && _deploy.update_needed(); + } + + Download_queue _download_queue { _heap }; + + File_operation_queue _file_operation_queue { _heap }; + + Fs_tool_version _fs_tool_version { 0 }; + + Index_update_queue _index_update_queue { + _heap, _file_operation_queue, _download_queue }; + + + /***************** + ** Depot query ** + *****************/ + + Depot_query::Version _query_version { 0 }; + + Depot::Archive::User _image_index_user = _build_info.depot_user; + + Depot::Archive::User _index_user = _build_info.depot_user; + + Expanding_reporter _depot_query_reporter { _env, "query", "depot_query"}; + + /** + * Depot_query interface + */ + Depot_query::Version depot_query_version() const override + { + return _query_version; + } + + Timer::Connection _timer { _env }; + + Timer::One_shot_timeout
_deferred_depot_query_handler { + _timer, *this, &Main::_handle_deferred_depot_query }; + + bool _software_tab_watches_depot() + { + if (!_software_title_bar.selected()) + return false; + + return _software_tabs_widget.hosted.add_selected() + || _software_tabs_widget.hosted.update_selected(); + } + + void _handle_deferred_depot_query(Duration) + { + if (_deploy._arch.valid()) { + _query_version.value++; + _depot_query_reporter.generate([&] (Xml_generator &xml) { + xml.attribute("arch", _deploy._arch); + xml.attribute("version", _query_version.value); + + if (_software_tab_watches_depot() || _scan_rom.xml().has_type("empty")) + xml.node("scan", [&] () { + xml.attribute("users", "yes"); }); + + if (_software_tab_watches_depot() || _image_index_rom.xml().has_type("empty")) + xml.node("index", [&] () { + xml.attribute("user", _index_user); + xml.attribute("version", _sculpt_version); + xml.attribute("content", "yes"); + }); + + if (_software_tab_watches_depot() || _image_index_rom.xml().has_type("empty")) + xml.node("image_index", [&] () { + xml.attribute("os", "sculpt"); + xml.attribute("board", _build_info.board); + xml.attribute("user", _image_index_user); + }); + + _runtime_state.with_construction([&] (Component const &component) { + xml.node("blueprint", [&] () { + xml.attribute("pkg", component.path); }); }); + + /* update query for blueprints of all unconfigured start nodes */ + _deploy.gen_depot_query(xml); + }); + } + } + + /** + * Depot_query interface + */ + void trigger_depot_query() override + { + /* + * Defer the submission of the query for a few milliseconds because + * 'trigger_depot_query' may be consecutively called several times + * while evaluating different conditions. Without deferring, the depot + * query component would produce intermediate results that take time + * but are ultimately discarded. + */ + _deferred_depot_query_handler.schedule(Microseconds{5000}); + } + + + /****************** + ** Browse index ** + ******************/ + + Attached_rom_dataspace _index_rom { _env, "report -> runtime/depot_query/index" }; + + Signal_handler
_index_handler { + _env.ep(), *this, &Main::_handle_index }; + + void _handle_index() + { + _index_rom.update(); + + bool const software_add_widget_shown = _software_title_bar.selected() + && _software_tabs_widget.hosted.add_selected(); + if (software_add_widget_shown) + _generate_dialog(); + } + + /** + * Software_add_widget::Action interface + */ + void query_index(Depot::Archive::User const &user) override + { + _index_user = user; + trigger_depot_query(); + } + + + /********************* + ** Blueprint query ** + *********************/ + + Attached_rom_dataspace _blueprint_rom { _env, "report -> runtime/depot_query/blueprint" }; + + Signal_handler
_blueprint_handler { + _env.ep(), *this, &Main::_handle_blueprint }; + + void _handle_blueprint() + { + _blueprint_rom.update(); + + Xml_node const blueprint = _blueprint_rom.xml(); + + /* + * Drop intermediate results that will be superseded by a newer query. + * This is important because an outdated blueprint would be disregarded + * by 'handle_deploy' anyway while at the same time a new query is + * issued. This can result a feedback loop where blueprints are + * requested but never applied. + */ + if (blueprint.attribute_value("version", 0U) != _query_version.value) + return; + + _runtime_state.apply_to_construction([&] (Component &component) { + component.try_apply_blueprint(blueprint); }); + + _deploy.handle_deploy(); + _generate_dialog(); + } + + + /************ + ** Deploy ** + ************/ + + Deploy::Prio_levels const _prio_levels { 4 }; + + Attached_rom_dataspace _scan_rom { _env, "report -> runtime/depot_query/scan" }; + + Signal_handler
_scan_handler { _env.ep(), *this, &Main::_handle_scan }; + + void _handle_scan() + { + _scan_rom.update(); + _generate_dialog(); + _software_update_widget.hosted.sanitize_user_selection(); + _software_add_widget.hosted.sanitize_user_selection(); + } + + Attached_rom_dataspace _image_index_rom { _env, "report -> runtime/depot_query/image_index" }; + + Signal_handler
_image_index_handler { _env.ep(), *this, &Main::_handle_image_index }; + + void _handle_image_index() + { + _image_index_rom.update(); + _generate_dialog(); + } + + Attached_rom_dataspace _launcher_listing_rom { + _env, "report -> /runtime/launcher_query/listing" }; + + Launchers _launchers { _heap }; + Presets _presets { _heap }; + + Signal_handler
_launcher_and_preset_listing_handler { + _env.ep(), *this, &Main::_handle_launcher_and_preset_listing }; + + void _handle_launcher_and_preset_listing() + { + _launcher_listing_rom.update(); + + Xml_node const listing = _launcher_listing_rom.xml(); + listing.for_each_sub_node("dir", [&] (Xml_node const &dir) { + + Path const dir_path = dir.attribute_value("path", Path()); + + if (dir_path == "/launcher") + _launchers.update_from_xml(dir); /* iterate over nodes */ + + if (dir_path == "/presets") + _presets.update_from_xml(dir); /* iterate over nodes */ + }); + + _generate_dialog(); + _deploy._handle_managed_deploy(); + } + + Deploy _deploy { _env, _heap, _child_states, _runtime_state, *this, *this, *this, + _launcher_listing_rom, _blueprint_rom, _download_queue }; + + /** + * Deploy::Action interface + */ + void refresh_deploy_dialog() override { _generate_dialog(); } + + Attached_rom_dataspace _manual_deploy_rom { _env, "config -> deploy" }; + + void _handle_manual_deploy() + { + _runtime_state.reset_abandoned_and_launched_children(); + _manual_deploy_rom.update(); + _deploy.use_as_deploy_template(_manual_deploy_rom.xml()); + _deploy.update_managed_deploy_config(); + } + + Signal_handler
_manual_deploy_handler { + _env.ep(), *this, &Main::_handle_manual_deploy }; + + + /************ + ** Global ** + ************/ + + Area _screen_size { }; + + bool _leitzentrale_visible = false; + + Color const _background_color { 62, 62, 67, 255 }; + + Affinity::Space _affinity_space { 1, 1 }; + + Sim_pin _sim_pin { }; + + Modem_state _modem_state { }; + + Current_call _current_call { }; + + Dialed_number _dialed_number { }; + + Power_state _power_state { }; + + enum class Section { NONE, DEVICE, PHONE, STORAGE, NETWORK, SOFTWARE }; + + Section _selected_section { Section::NONE }; + + using Title_bar = Selectable_title_bar
; + + struct Software_status_widget : Widget + { + void view(Scope &s, Main const &main) const + { + s.sub_scope([&] (Scope &s) { + + if (main._manually_managed_runtime) + return; + + if (main._diagnostics_available()) { + + Hosted diag { Id { "Diagnostics" } }; + s.widget(diag, Titled_frame::Attr { .min_ex = 40 }, [&] { + + if (main._network_missing()) + s.sub_scope("network needed for installation"); + + s.as_new_scope([&] (Scope<> &s) { main._deploy.view_diag(s); }); + }); + } + + Xml_node const state = main._update_state_rom.xml(); + + bool const download_in_progress = + main._update_running() && state.attribute_value("progress", false); + + if (download_in_progress || main._download_queue.any_failed_download()) { + Hosted download_status { Id { "Download" } }; + s.widget(download_status, state, main._download_queue); + } + }); + } + }; + + + /* + * Device section + */ + + Hosted _device_title_bar { + Id { "Device" }, _selected_section, Section::DEVICE }; + + Conditional_widget + _device_controls_widget { Id { "device_controls" } }; + + Conditional_widget + _device_power_widget { Id { "device_power" } }; + + /* + * Phone section + */ + + Hosted _phone_title_bar { + Id { "Phone" }, _selected_section, Section::PHONE }; + + Conditional_widget + _modem_power_widget { Id { "modem_power" } }; + + Conditional_widget + _pin_widget { Id { "pin" } }; + + Conditional_widget + _dialpad_widget { Id { "dialpad" } }; + + Conditional_widget + _current_call_widget { Id { "call" } }; + + Conditional_widget + _outbound_widget { Id { "outbound" } }; + + /* + * Storage section + */ + + Hosted _storage_title_bar { + Id { "Storage" }, _selected_section, Section::STORAGE }; + + struct Storage_widget : Widget + { + Hosted _block_devices; + + template + Storage_widget(ARGS &&... args) : _block_devices(Id { "devices" }, args...) { } + + void view(Scope &s) const { s.widget(_block_devices); } + + template + void click(ARGS &&... args) { _block_devices.propagate(args...); } + + template + void clack(ARGS &&... args) { _block_devices.propagate(args...); } + + void reset_operation() { _block_devices.reset_operation(); } + }; + + Conditional_widget _storage_widget { + Conditional_widget::Attr { .centered = true }, + Id { "storage dialog" }, _storage._storage_devices, _storage._sculpt_partition }; + + /* + * Network section + */ + + Hosted _network_title_bar { + Id { "Network" }, _selected_section, Section::NETWORK }; + + /* + * Software section + */ + + Hosted _software_title_bar { + Id { "Software" }, _selected_section, Section::SOFTWARE }; + + Conditional_widget + _software_tabs_widget { Id { "software_tabs" } }; + + Conditional_widget + _software_presets_widget { Id { "software_presets" } }; + + Conditional_widget + _software_options_widget { Id { "software_options" }, _runtime_state, _launchers }; + + Conditional_widget + _software_add_widget { Id { "software_add" }, _build_info, _sculpt_version, + _network._nic_state, _index_update_queue, + _index_rom, _download_queue, + _cached_runtime_config, + *this, _scan_rom }; + + Conditional_widget + _software_update_widget { Id { "software_update" }, _build_info, + _network._nic_state, _download_queue, + _index_update_queue, _file_operation_queue, + _scan_rom, _image_index_rom }; + + Conditional_widget + _software_version_widget { Id { "software_version" } }; + + Conditional_widget + _software_status_widget { Id { "software_status" } }; + + Conditional_widget + _graph { Id { "graph" }, + _runtime_state, _cached_runtime_config, _storage._storage_devices, + _storage._sculpt_partition, _storage._ram_fs_state, + _popup.state, _deploy._children }; + + Conditional_widget + _network_widget { + Conditional_widget::Attr { .centered = true }, + Id { "net settings" }, + _network._nic_target, _network._access_points, + _network._wifi_connection, _network._nic_state, + _network.wpa_passphrase, _network._wlan_config_policy, + _pci_info }; + + void _view_main_dialog(Scope<> &s) const + { + /* skip generating the dialog at boot time */ + if (!_gui_mode_ready) + return; + + s.sub_scope([&] (Scope &s) { + + s.widget(_device_title_bar, [&] (auto &s) { + _device_title_bar.view_status(s, _power_state.summary()); }); + + s.widget(_device_controls_widget, _device_title_bar.selected(), + _power_state, _mic_state, _audio_volume); + + s.widget(_device_power_widget, _device_title_bar.selected(), _power_state); + + if (_power_state.modem_present()) { + + s.widget(_phone_title_bar, [&] (auto &s) { + + auto phone_status_message = [&] () -> String<128> + { + if (!_modem_state.ready() || !_modem_state.pin_ok()) + return _modem_state.power_message(); + + return "ready"; + }; + _phone_title_bar.view_status(s, phone_status_message()); + }); + + s.widget(_modem_power_widget, _phone_title_bar.selected(), + _modem_state); + + s.widget(_pin_widget, _phone_title_bar.selected() + && _modem_state.ready() + && _modem_state.pin_required(), _sim_pin); + + s.widget(_outbound_widget, _phone_title_bar.selected() + && _modem_state.ready() + && _modem_state.pin_ok()); + + s.widget(_dialpad_widget, _phone_title_bar.selected() + && _modem_state.ready() + && _modem_state.pin_ok(), + _dialed_number); + + s.widget(_current_call_widget, _phone_title_bar.selected() + && _modem_state.ready() + && _modem_state.pin_ok(), + _dialed_number, _current_call); + } + + s.widget(_storage_title_bar, [&] (auto &s) { + _storage_title_bar.view_status(s, " "); }); + + s.widget(_storage_widget, _storage_title_bar.selected()); + + s.widget(_network_title_bar, [&] (auto &s) { + + auto network_status_message = [&] + { + switch (_network._nic_target.type()) { + case Nic_target::Type::UNDEFINED: + case Nic_target::Type::OFF: + break; + + case Nic_target::Type::DISCONNECTED: + return "disconnected"; + + case Nic_target::Type::WIRED: + return _network._nic_state.ready() ? "LAN" : "LAN ..."; + + case Nic_target::Type::WIFI: + return _network._nic_state.ready() ? "WLAN" : "WLAN ..."; + + case Nic_target::Type::MODEM: + return _network._nic_state.ready() ? "mobile" : "mobile ..."; + } + return "off"; + }; + _network_title_bar.view_status(s, network_status_message()); + }); + + s.widget(_network_widget, _network_title_bar.selected()); + + s.widget(_software_title_bar, [&] (auto &s) { + _software_title_bar.view_status(s, _software_status_message()); }); + + s.widget(_software_tabs_widget, _software_title_bar.selected(), + _storage._sculpt_partition, _presets, _software_status_available()); + + s.widget(_graph, _software_title_bar.selected() + && _software_tabs_widget.hosted.runtime_selected()); + + s.widget(_software_presets_widget, _software_title_bar.selected() + && _software_tabs_widget.hosted.presets_selected() + && _storage._sculpt_partition.valid(), + _presets); + + s.widget(_software_options_widget, _software_title_bar.selected() + && _software_tabs_widget.hosted.options_selected() + && _storage._sculpt_partition.valid()); + + s.widget(_software_add_widget, _software_title_bar.selected() + && _software_tabs_widget.hosted.add_selected() + && _storage._sculpt_partition.valid()); + + s.widget(_software_update_widget, _software_title_bar.selected() + && _software_tabs_widget.hosted.update_selected() + && _storage._sculpt_partition.valid(), + _image_index_rom.xml()); + + s.widget(_software_version_widget, _software_title_bar.selected() + && _software_tabs_widget.hosted.update_selected() + && !_touch_keyboard.visible, + _build_info); + + s.widget(_software_status_widget, _software_title_bar.selected() + && _software_tabs_widget.hosted.status_selected(), + *this); + + /* + * Whenever the touch keyboard is visible, enforce some space at + * the bottom of the dialog by using a vertical stack of empty + * labels. + */ + if (_touch_keyboard.visible) + s.sub_scope([&] (Scope &s) { + for (unsigned i = 0; i < 15; i++) + s.sub_scope(); }); + }); + } + + void _update_touch_keyboard_visibility() + { + /* detect need for touch keyboard */ + bool const orig_touch_keyboard_visible = _touch_keyboard.visible; + + _touch_keyboard.visible = touch_keyboard_needed(); + + if (orig_touch_keyboard_visible != _touch_keyboard.visible) + _handle_window_layout(); + } + + void _generate_dialog() + { + _update_touch_keyboard_visibility(); + + _main_view.refresh(); + } + + Attached_rom_dataspace _runtime_state_rom { _env, "report -> runtime/state" }; + + Runtime_state _runtime_state { _heap, _storage._sculpt_partition }; + + Managed_config
_runtime_config { + _env, "config", "runtime", *this, &Main::_handle_runtime }; + + /** + * Component::Construction_info interface + */ + void _with_construction(Component::Construction_info::With const &fn) const override + { + _runtime_state.with_construction([&] (Component const &c) { fn.with(c); }); + } + + /** + * Component::Construction_action interface + */ + void new_construction(Component::Path const &pkg, Verify verify, + Component::Info const &info) override + { + (void)_runtime_state.new_construction(pkg, verify, info, _affinity_space); + trigger_depot_query(); + } + + void _apply_to_construction(Component::Construction_action::Apply_to &fn) override + { + _runtime_state.apply_to_construction([&] (Component &c) { fn.apply_to(c); }); + } + + /** + * Component::Construction_action interface + */ + void trigger_pkg_download() override + { + _runtime_state.apply_to_construction([&] (Component &c) { + _download_queue.add(c.path, c.verify); }); + + /* incorporate new download-queue content into update */ + _deploy.update_installation(); + + generate_runtime_config(); + } + + /** + * Component::Construction_action interface + */ + void discard_construction() override { _runtime_state.discard_construction(); } + + /** + * Component::Construction_action interface + */ + void launch_construction() override + { + _runtime_state.launch_construction(); + + /* trigger change of the deployment */ + _deploy.update_managed_deploy_config(); + } + + bool _manually_managed_runtime = false; + + void _handle_runtime(Xml_node config) + { + _manually_managed_runtime = !config.has_type("empty"); + generate_runtime_config(); + _generate_dialog(); + } + + void _generate_runtime_config(Xml_generator &) const; + + /** + * Runtime_config_generator interface + */ + void generate_runtime_config() override + { + if (!_runtime_config.try_generate_manually_managed()) + _runtime_config.generate([&] (Xml_generator &xml) { + _generate_runtime_config(xml); }); + } + + Signal_handler
_runtime_state_handler { + _env.ep(), *this, &Main::_handle_runtime_state }; + + void _handle_runtime_state(); + + Attached_rom_dataspace const _platform { _env, "platform_info" }; + + + /******************** + ** Touch keyboard ** + ********************/ + + struct Touch_keyboard : Noncopyable + { + /* + * Spawn the leitzentrale touch keyboard only after the basic GUI is up + * beacuse the touch keyboard is not needed to pick up a call. + */ + bool started = false; + + /* + * Updated and evaluated by 'generate_dialog' + */ + bool visible = false; + + Touch_keyboard_attr attr; + + Touch_keyboard(Touch_keyboard_attr attr) : attr(attr) { }; + + void gen_start_node(Xml_generator &xml) const + { + if (started) + gen_touch_keyboard(xml, attr); + } + }; + + Touch_keyboard _touch_keyboard { + { .min_width = 720, + .min_height = 480, + .alpha = Alpha::OPAQUE, + .background = _background_color } }; + + bool _depot_user_selection_visible() const + { + if (!_software_title_bar.selected()) + return false; + + return _software_tabs_widget.hosted.update_selected() + || _software_tabs_widget.hosted.add_selected(); + } + + bool _software_add_widget_has_keyboard_focus() const + { + return _software_title_bar.selected() + && _software_tabs_widget.hosted.add_selected() + && _software_add_widget.hosted.keyboard_needed(); + } + + bool _software_update_widget_has_keyboard_focus() const + { + return _software_title_bar.selected() + && _software_tabs_widget.hosted.update_selected() + && _software_update_widget.hosted.keyboard_needed(); + } + + bool _network_widget_has_keyboard_focus() const + { + return _network_title_bar.selected() + && _network_widget.hosted.need_keyboard_focus_for_passphrase(); + } + + /** + * Condition for controlling the visibility of the touch keyboard + */ + bool touch_keyboard_needed() const + { + return _software_add_widget_has_keyboard_focus() + || _software_update_widget_has_keyboard_focus() + || _network_widget_has_keyboard_focus(); + } + + + /**************************************** + ** Cached model of the runtime config ** + ****************************************/ + + /* + * Even though the runtime configuration is generated by the sculpt + * manager, we still obtain it as a separate ROM session to keep the GUI + * part decoupled from the lower-level runtime configuration generator. + */ + Attached_rom_dataspace _runtime_config_rom { _env, "config -> managed/runtime" }; + + Signal_handler
_runtime_config_handler { + _env.ep(), *this, &Main::_handle_runtime_config }; + + Runtime_config _cached_runtime_config { _heap }; + + void _handle_runtime_config() + { + _runtime_config_rom.update(); + _cached_runtime_config.update_from_xml(_runtime_config_rom.xml()); + _generate_dialog(); /* update graph */ + } + + + /**************************** + ** Interactive operations ** + ****************************/ + + Dialog::Distant_runtime _dialog_runtime { _env }; + + struct Main_dialog : Dialog::Top_level_dialog + { + Main &_main; + + Main_dialog(Main &main) : Top_level_dialog("main"), _main(main) { } + + void view(Scope<> &s) const override { _main._view_main_dialog(s); } + + void click(Clicked_at const &at) override { _main._click(at); } + void clack(Clacked_at const &at) override { _main._clack(at); } + void drag (Dragged_at const &at) override { _main._drag(at); } + + } _main_dialog { *this }; + + Dialog::Distant_runtime::View + _main_view { _dialog_runtime, _main_dialog, + { .opaque = true, + .background = _background_color, + .initial_ram = { 12*1024*1024 } } }; + + void _click(Clicked_at const &at) + { + auto for_each_title_bar = [&] (auto const &fn) + { + fn(_device_title_bar); + fn(_phone_title_bar); + fn(_storage_title_bar); + fn(_network_title_bar); + fn(_software_title_bar); + }; + + /* toggle sections */ + for_each_title_bar([&] (auto &title_bar) { + title_bar.propagate(at, [&] { + _selected_section = title_bar.selected() + ? Section::NONE : title_bar.value; }); }); + + _device_controls_widget .propagate(at, *this); + _device_power_widget .propagate(at, *this); + _modem_power_widget .propagate(at, *this); + _pin_widget .propagate(at, _sim_pin, *this); + _dialpad_widget .propagate(at, *this); + _storage_widget .propagate(at, *this); + _network_widget .propagate(at, _network); + _software_presets_widget.propagate(at, _presets); + _software_update_widget .propagate(at, *this); + _software_add_widget .propagate(at, *this); + _current_call_widget .propagate(at, *this); + _software_options_widget.propagate(at, *this); + _graph .propagate(at, *this); + + _software_tabs_widget.propagate(at, [&] { + + /* refresh list of depot users */ + trigger_depot_query(); + }); + + _update_touch_keyboard_visibility(); + } + + void _clack(Clacked_at const &at) + { + _device_power_widget .propagate(at, *this); + _storage_widget .propagate(at, *this); + _software_presets_widget.propagate(at, _presets, *this); + _software_add_widget .propagate(at, *this); + _graph .propagate(at, *this, _storage); + + _update_touch_keyboard_visibility(); + } + + void _drag(Dragged_at const &at) + { + _device_controls_widget.propagate(at, *this); + } + + /** + * Input_event_handler interface + */ + void handle_input_event(Input::Event const &ev) override + { + Dialog::Event::Seq_number const seq_number { _global_input_seq_number.value }; + _dialog_runtime.route_input_event(seq_number, ev); + + bool need_generate_dialog = false; + + ev.handle_press([&] (Input::Keycode key, Codepoint code) { + + need_generate_dialog = true; + + if (_software_add_widget_has_keyboard_focus()) + _software_add_widget.hosted.handle_key(code, *this); + + else if (_software_update_widget_has_keyboard_focus()) + _software_update_widget.hosted.handle_key(code, *this); + + else if (_network_widget_has_keyboard_focus()) + _network.handle_key_press(code); + + /* handle volume up/down buttons */ + { + bool const volume_up = (key == Input::KEY_VOLUMEUP); + bool const volume_down = (key == Input::KEY_VOLUMEDOWN); + + unsigned level = _audio_volume.value; + + if (volume_up) level = min(level + 10, 100u); + if (volume_down) level = (level >= 10 ? level - 10 : 0); + + if (volume_up || volume_down) { + select_volume_level(level); + _selected_section = Section::DEVICE; + } + } + + if (key == Input::KEY_POWER) + _screensaver.force_toggle(); + }); + + if (need_generate_dialog) + _generate_dialog(); + } + + void _handle_window_layout(); + + template + void _with_window(Xml_node window_list, String const &match, FN const &fn) + { + window_list.for_each_sub_node("window", [&] (Xml_node win) { + if (win.attribute_value("label", String()) == match) + fn(win); }); + } + + Attached_rom_dataspace _window_list { _env, "window_list" }; + + Signal_handler
_window_list_handler { + _env.ep(), *this, &Main::_handle_window_layout }; + + Expanding_reporter _wm_focus { _env, "focus", "wm_focus" }; + + Attached_rom_dataspace _decorator_margins { _env, "decorator_margins" }; + + Signal_handler
_decorator_margins_handler { + _env.ep(), *this, &Main::_handle_window_layout }; + + Expanding_reporter _window_layout { _env, "window_layout", "window_layout" }; + + void _reset_storage_widget_operation() + { + _graph.hosted.reset_storage_operation(); + _storage_widget.hosted.reset_operation(); + } + + /* + * Fs_dialog::Action interface + */ + void toggle_inspect_view(Storage_target const &) override { } + + void use(Storage_target const &target) override + { + _software_update_widget.hosted.reset(); + _download_queue.reset(); + _storage.use(target); + } + + /* + * Storage_dialog::Action interface + */ + void format(Storage_target const &target) override + { + _storage.format(target); + } + + void cancel_format(Storage_target const &target) override + { + _storage.cancel_format(target); + _reset_storage_widget_operation(); + } + + void expand(Storage_target const &target) override + { + _storage.expand(target); + } + + void cancel_expand(Storage_target const &target) override + { + _storage.cancel_expand(target); + _reset_storage_widget_operation(); + } + + void check(Storage_target const &target) override + { + _storage.check(target); + } + + void toggle_default_storage_target(Storage_target const &target) override + { + _storage.toggle_default_storage_target(target); + } + + /* + * Graph::Action interface + */ + void remove_deployed_component(Start_name const &name) override + { + _runtime_state.abandon(name); + + /* update config/managed/deploy with the component 'name' removed */ + _deploy.update_managed_deploy_config(); + } + + /* + * Graph::Action interface + */ + void restart_deployed_component(Start_name const &name) override + { + if (name == "nic_drv") { + + _network.restart_nic_drv_on_next_runtime_cfg(); + generate_runtime_config(); + + } else if (name == "wifi_drv") { + + _network.restart_wifi_drv_on_next_runtime_cfg(); + generate_runtime_config(); + + } else if (name == "usb_net") { + + _network.restart_usb_net_on_next_runtime_cfg(); + generate_runtime_config(); + + } else { + + _runtime_state.restart(name); + + /* update config/managed/deploy with the component 'name' removed */ + _deploy.update_managed_deploy_config(); + } + } + + /* + * Graph::Action interface + */ + void open_popup_dialog(Rect) override { } + + bool _network_missing() const { + return _deploy.update_needed() && !_network._nic_state.ready(); } + + bool _diagnostics_available() const { + return _deploy.any_unsatisfied_child() || _network_missing(); } + + bool _software_status_available() const + { + return _diagnostics_available() + || _update_running() + || _download_queue.any_failed_download(); + } + + char const *_software_status_message() const + { + if (_update_running()) + return "install ..."; + + if (_diagnostics_available()) + return "!"; + + return " "; + } + + /** + * Software_presets_dialog::Action interface + */ + void load_deploy_preset(Presets::Info::Name const &name) override + { + Xml_node const listing = _launcher_listing_rom.xml(); + + listing.for_each_sub_node("dir", [&] (Xml_node const &dir) { + if (dir.attribute_value("path", Path()) == "/presets") { + dir.for_each_sub_node("file", [&] (Xml_node const &file) { + if (file.attribute_value("name", Presets::Info::Name()) == name) { + file.with_optional_sub_node("config", [&] (Xml_node const &config) { + _runtime_state.reset_abandoned_and_launched_children(); + _deploy.use_as_deploy_template(config); + _deploy.update_managed_deploy_config(); }); } }); } }); + } + + /** + * Software_options_widget::Action interface + */ + void enable_optional_component(Path const &launcher) override + { + _runtime_state.launch(launcher, launcher); + + /* trigger change of the deployment */ + _deploy.update_managed_deploy_config(); + } + + /** + * Software_options_widget::Action interface + */ + void disable_optional_component(Path const &launcher) override + { + _runtime_state.abandon(launcher); + + /* update config/managed/deploy with the component 'name' removed */ + _deploy.update_managed_deploy_config(); + } + + /** + * Depot_users_dialog::Action interface + */ + void add_depot_url(Depot_url const &depot_url) override + { + using Content = File_operation_queue::Content; + + _file_operation_queue.new_small_file(Path("/rw/depot/", depot_url.user, "/download"), + Content { depot_url.download }); + + if (!_file_operation_queue.any_operation_in_progress()) + _file_operation_queue.schedule_next_operations(); + + generate_runtime_config(); + } + + /** + * Software_update_dialog::Action interface + */ + void query_image_index(Depot::Archive::User const &user) override + { + _image_index_user = user; + trigger_depot_query(); + } + + /** + * Software_update_dialog::Action interface + */ + void trigger_image_download(Path const &path, Verify verify) override + { + _download_queue.remove_inactive_downloads(); + _download_queue.add(path, verify); + _deploy.update_installation(); + generate_runtime_config(); + } + + /** + * Software_update_dialog::Action interface + */ + void update_image_index(Depot::Archive::User const &user, Verify verify) override + { + _download_queue.remove_inactive_downloads(); + _index_update_queue.remove_inactive_updates(); + _index_update_queue.add(Path(user, "/image/index"), verify); + generate_runtime_config(); + } + + /** + * Software_update_dialog::Action interface + */ + void install_boot_image(Path const &path) override + { + _file_operation_queue.copy_all_files(Path("/rw/depot/", path), "/rw/boot"); + + if (!_file_operation_queue.any_operation_in_progress()) + _file_operation_queue.schedule_next_operations(); + + generate_runtime_config(); + } + + /** + * Software_add_dialog::Action interface + */ + void update_sculpt_index(Depot::Archive::User const &user, Verify verify) override + { + _download_queue.remove_inactive_downloads(); + _index_update_queue.remove_inactive_updates(); + _index_update_queue.add(Path(user, "/index/", _sculpt_version), verify); + generate_runtime_config(); + } + + + /*********** + ** Audio ** + ***********/ + + Mic_state _mic_state = Mic_state::PHONE; + + Audio_volume _audio_volume { .value = 75 }; + + Expanding_reporter _audio_config { _env, "config", "audio_config" }; + + struct Audio_config + { + bool earpiece, speaker, mic, modem; + + Audio_volume audio_volume; + + bool operator != (Audio_config const &other) const + { + return (earpiece != other.earpiece) + || (speaker != other.speaker) + || (mic != other.mic) + || (modem != other.modem) + || (audio_volume.value != other.audio_volume.value); + } + + void generate(Xml_generator &xml) const + { + xml.node("earpiece", [&] () { + xml.attribute("volume", earpiece ? 100 : 0); + }); + xml.node("speaker", [&] () { + xml.attribute("volume", speaker ? audio_volume.value : 0); + }); + xml.node("mic", [&] () { + xml.attribute("volume", mic ? 80 : 0); + }); + xml.node("codec", [&]() { + xml.attribute("target", modem ? "modem" : "soc"); + }); + } + }; + + Audio_config _curr_audio_config { }; + + void _generate_audio_config() + { + auto mic_enabled = [&] + { + switch (_mic_state) { + case Mic_state::OFF: return false; + case Mic_state::PHONE: return _current_call.active(); + case Mic_state::ON: return true; + } + return false; + }; + + Audio_config const new_config { + + .earpiece = true, + + /* enable speaker for the ring tone when no call is active */ + .speaker = !_current_call.active() || _current_call.speaker, + + /* enable microphone during call */ + .mic = mic_enabled(), + + /* set codec target during call */ + .modem = _current_call.active(), + + .audio_volume = _audio_volume + }; + + if (new_config != _curr_audio_config) { + _curr_audio_config = new_config; + _audio_config.generate([&] (Xml_generator &xml) { + _curr_audio_config.generate(xml); }); + } + } + + /** + * Device_controls_widget::Action interface + */ + void select_volume_level(unsigned level) override + { + _audio_volume.value = level; + _generate_audio_config(); + } + + /** + * Device_controls_widget::Action interface + */ + void select_mic_policy(Mic_state const &policy) override + { + _mic_state = policy; + _generate_audio_config(); + } + + + /********************** + ** Device functions ** + **********************/ + + Attached_rom_dataspace _power_rom { _env, "report -> drivers/power" }; + + Signal_handler
_power_handler { + _env.ep(), *this, &Main::_handle_power }; + + void _handle_power() + { + _power_rom.update(); + + Power_state const orig_power_state = _power_state; + _power_state = Power_state::from_xml(_power_rom.xml()); + + bool regenerate_dialog = false; + + /* mobile data connectivity depends on the presence of a battery */ + if (_power_state.modem_present() != _pci_info.modem_present) { + + /* update condition for the "Mobile data" network option */ + _pci_info.modem_present = _power_state.modem_present() + && _modem_state.ready(); + + regenerate_dialog = true; + } + + if (orig_power_state.summary() != _power_state.summary()) + regenerate_dialog = true; + + if (_device_title_bar.selected()) + regenerate_dialog = true; + + if (regenerate_dialog) + _generate_dialog(); + } + + /** + * Device_controls_widget::Action interface + */ + void select_brightness_level(unsigned level) override + { + _system.brightness = level; + _update_managed_system_config(); + } + + /** + * Device_power_widget::Action interface + */ + void activate_performance_power_profile() override + { + _system.power_profile = "performance"; + _update_managed_system_config(); + } + + /** + * Device_power_widget::Action interface + */ + void activate_economic_power_profile() override + { + _system.power_profile = "economic"; + _update_managed_system_config(); + } + + /** + * Device_power_widget::Action interface + */ + void trigger_device_reboot() override + { + _system.state = "reset"; + _update_managed_system_config(); + } + + /** + * Device_power_widget::Action interface + */ + void trigger_device_off() override + { + _system.state = "poweroff"; + _update_managed_system_config(); + } + + + /*********** + ** Phone ** + ***********/ + + Expanding_reporter _modem_config { _env, "config", "modem_config" }; + + enum class Modem_config_power { ANY, OFF, ON }; + + Modem_config_power _modem_config_power { Modem_config_power::ANY }; + + /* + * State that influences the modem configuration, used to detect the + * need for configuraton updates. + */ + struct Modem_config + { + Modem_config_power modem_power; + Modem_state modem_state; + Sim_pin sim_pin; + Current_call current_call; + + bool operator != (Modem_config const &other) const + { + return (modem_power != other.modem_power) + || (modem_state != other.modem_state) + || (sim_pin != other.sim_pin) + || (current_call != other.current_call); + } + + void generate(Xml_generator &xml) const + { + switch (modem_power) { + case Modem_config_power::OFF: xml.attribute("power", "off"); break; + case Modem_config_power::ON: xml.attribute("power", "on"); break; + case Modem_config_power::ANY: break; + } + + bool const supply_pin = modem_state.pin_required() + && sim_pin.suitable_for_unlock() + && sim_pin.confirmed; + if (supply_pin) + xml.attribute("pin", String<10>(sim_pin)); + + xml.node("ring", [&] { + xml.append_content("AT+QLDTMF=5,\"4,3,6,#,D,3\",1"); }); + + current_call.gen_modem_config(xml); + } + }; + + Modem_config _curr_modem_config { }; + + Attached_rom_dataspace _modem_state_rom { _env, "report -> drivers/modem/state" }; + + Signal_handler
_modem_state_handler { + _env.ep(), *this, &Main::_handle_modem_state }; + + void _handle_modem_state() + { + _modem_state_rom.update(); + + if (_verbose_modem) + log("modem state: ", _modem_state_rom.xml()); + + Modem_state const orig_modem_state = _modem_state; + + bool regenerate_dialog = false; + + _modem_state = Modem_state::from_xml(_modem_state_rom.xml()); + + /* update condition of "Mobile data" network option */ + { + bool const orig_mobile_data_ready = _pci_info.modem_present; + _pci_info.modem_present = _power_state.modem_present() + && _modem_state.ready(); + if (orig_mobile_data_ready != _pci_info.modem_present) + regenerate_dialog = true; + } + + _current_call.update(_modem_state); + + if (_modem_state.pin_rejected()) + _sim_pin = Sim_pin { }; + + bool const configured_current_call_out_of_date = + (_current_call != _curr_modem_config.current_call); + + bool const modem_state_changed = (orig_modem_state != _modem_state); + + if (configured_current_call_out_of_date || modem_state_changed) { + _generate_modem_config(); + regenerate_dialog = true; + } + + if (regenerate_dialog) + _generate_dialog(); + } + + void _generate_modem_config() + { + Modem_config const new_config { + .modem_power = _modem_config_power, + .modem_state = _modem_state, + .sim_pin = _sim_pin, + .current_call = _current_call, + }; + + if (new_config != _curr_modem_config) { + + _curr_modem_config = new_config; + + _modem_config.generate([&] (Xml_generator &xml) { + + if (_verbose_modem) + xml.attribute("verbose", "yes"); + + _curr_modem_config.generate(xml); + }); + } + + /* update audio config as it depends on the current call state */ + _generate_audio_config(); + } + + /** + * Modem_power_widget::Action interface + */ + void modem_power(bool enabled) override + { + _modem_config_power = enabled ? Modem_config_power::ON + : Modem_config_power::OFF; + + /* forget pin and call state when powering off the modem */ + if (!enabled) { + _sim_pin = { }; + _current_call = { }; + } + + _generate_modem_config(); + } + + /** + * Pin_widget::Action interface + */ + void append_sim_pin_digit(Sim_pin::Digit d) override + { + _sim_pin.append_digit(d); + } + + /** + * Pin_widget::Action interface + */ + void remove_last_sim_pin_digit() override + { + _sim_pin.remove_last_digit(); + } + + /** + * Pin_widget::Action interface + */ + void confirm_sim_pin() override + { + if (_sim_pin.suitable_for_unlock()) + _sim_pin.confirmed = true; + _generate_modem_config(); + } + + /** + * Dialpad_widget::Action interface + */ + void append_dial_digit(Dialed_number::Digit d) override + { + if (_current_call.canceled()) + _current_call = { }; + + _dialed_number.append_digit(d); + } + + /** + * Current_call_dialog::Action interface + */ + void remove_last_dial_digit() override + { + if (_current_call.canceled()) + _current_call = { }; + + _dialed_number.remove_last_digit(); + } + + /** + * Current_call_dialog::Action interface + */ + void accept_incoming_call() override + { + _current_call.accept(); + _generate_modem_config(); + } + + /** + * Current_call_dialog::Action interface + */ + void reject_incoming_call() override + { + _current_call.reject(); + _generate_modem_config(); + } + + /** + * Current_call_dialog::Action interface + */ + void hang_up() override + { + _current_call.reject(); + _generate_modem_config(); + } + + /** + * Current_call_dialog::Action interface + */ + void toggle_speaker() override + { + _current_call.toggle_speaker(); + _generate_modem_config(); + } + + /** + * Current_call_dialog::Action interface + */ + void initiate_call() override + { + if (_dialed_number.suitable_for_call()) { + _current_call.initiate(Number(_dialed_number)); + _generate_modem_config(); + } + } + + /** + * Current_call_dialog::Action interface + */ + void cancel_initiated_call() override + { + _current_call.cancel(); + _generate_modem_config(); + } + + + /******************* + ** Runtime graph ** + *******************/ + + Popup _popup { }; + + Main(Env &env) : _env(env) + { + _config.sigh(_config_handler); + _leitzentrale_rom.sigh(_leitzentrale_handler); + _manual_deploy_rom.sigh(_manual_deploy_handler); + _runtime_state_rom.sigh(_runtime_state_handler); + _runtime_config_rom.sigh(_runtime_config_handler); + _gui.input()->sigh(_input_handler); + _gui.mode_sigh(_gui_mode_handler); + + /* + * Subscribe to reports + */ + _update_state_rom .sigh(_update_state_handler); + _window_list .sigh(_window_list_handler); + _decorator_margins .sigh(_decorator_margins_handler); + _scan_rom .sigh(_scan_handler); + _launcher_listing_rom.sigh(_launcher_and_preset_listing_handler); + _blueprint_rom .sigh(_blueprint_handler); + _image_index_rom .sigh(_image_index_handler); + _power_rom .sigh(_power_handler); + _modem_state_rom .sigh(_modem_state_handler); + _index_rom .sigh(_index_handler); + + /* + * Import initial report content + */ + _handle_config(); + _handle_leitzentrale(); + _handle_gui_mode(); + _storage.handle_storage_devices_update(); + _handle_runtime_config(); + _handle_modem_state(); + + _system_config.with_manual_config([&] (Xml_node const &system) { + _system = System::from_xml(system); }); + + _update_managed_system_config(); + + /* + * Read static platform information + */ + _platform.xml().with_optional_sub_node("affinity-space", [&] (Xml_node const &node) { + _affinity_space = Affinity::Space(node.attribute_value("width", 1U), + node.attribute_value("height", 1U)); + }); + + /* + * Generate initial config/managed/deploy configuration + */ + _handle_manual_deploy(); + + _generate_modem_config(); + generate_runtime_config(); + _generate_dialog(); + } +}; + + +void Sculpt::Main::_handle_window_layout() +{ + /* skip window-layout handling (and decorator activity) while booting */ + if (!_gui_mode_ready) + return; + + struct Decorator_margins + { + unsigned top = 0, bottom = 0, left = 0, right = 0; + + Decorator_margins(Xml_node node) + { + if (!node.has_sub_node("floating")) + return; + + Xml_node const floating = node.sub_node("floating"); + + top = floating.attribute_value("top", 0U); + bottom = floating.attribute_value("bottom", 0U); + left = floating.attribute_value("left", 0U); + right = floating.attribute_value("right", 0U); + } + }; + + /* read decorator margins from the decorator's report */ + _decorator_margins.update(); + Decorator_margins const margins(_decorator_margins.xml()); + + typedef String<128> Label; + Label const main_view_label ("runtime -> leitzentrale -> main_view"); + Label const touch_keyboard_label("runtime -> leitzentrale -> touch_keyboard"); + + _window_list.update(); + Xml_node const window_list = _window_list.xml(); + + /* + * Take presence of main view as trigger for second driver stage. + * + * Once after the basic GUI is up, spawn storage drivers and touch keyboard. + */ + if (!_system.storage) { + _with_window(window_list, main_view_label, [&] (Xml_node) { + _enter_second_driver_stage(); + _touch_keyboard.started = true; + generate_runtime_config(); + }); + } + + auto win_size = [&] (Xml_node win) { + return Area(win.attribute_value("width", 0U), + win.attribute_value("height", 0U)); }; + + Framebuffer::Mode const mode = _gui.mode(); + + /* suppress intermediate boot-time states before the framebuffer driver is up */ + if (mode.area.count() <= 1) + return; + + _window_layout.generate([&] (Xml_generator &xml) { + + auto gen_window = [&] (Xml_node win, Rect rect) { + if (rect.valid()) { + xml.node("window", [&] () { + xml.attribute("id", win.attribute_value("id", 0UL)); + xml.attribute("xpos", rect.x1()); + xml.attribute("ypos", rect.y1()); + xml.attribute("width", rect.w()); + xml.attribute("height", rect.h()); + xml.attribute("title", win.attribute_value("label", Label())); + }); + } + }; + + _with_window(window_list, touch_keyboard_label, [&] (Xml_node win) { + if (!_leitzentrale_visible) + return; + + Area const size = win_size(win); + Point const pos = _touch_keyboard.visible + ? Point(0, int(mode.area.h()) - int(size.h())) + : Point(0, int(mode.area.h())); + + gen_window(win, Rect(pos, size)); + }); + + _with_window(window_list, main_view_label, [&] (Xml_node win) { + Area const size = win_size(win); + Point const pos(_leitzentrale_visible ? 0 : int(size.w()), 0); + gen_window(win, Rect(pos, size)); + }); + }); +} + + +void Sculpt::Main::_handle_gui_mode() +{ + Framebuffer::Mode const mode = _gui.mode(); + + _screensaver.display_driver_ready(mode.area.count() > 1); + + if (mode.area.count() > 1) + _gui_mode_ready = true; + + _handle_window_layout(); + + _screen_size = mode.area; + _main_view.min_width = _screen_size.w(); + _main_view.min_height = _screen_size.h(); + + generate_runtime_config(); +} + + +void Sculpt::Main::_handle_update_state() +{ + _update_state_rom.update(); + + Xml_node const update_state = _update_state_rom.xml(); + + _download_queue.apply_update_state(update_state); + bool const any_completed_download = _download_queue.any_completed_download(); + _download_queue.remove_completed_downloads(); + + _index_update_queue.apply_update_state(update_state); + + bool const installation_complete = + !update_state.attribute_value("progress", false); + + if (installation_complete) { + + Xml_node const blueprint = _blueprint_rom.xml(); + bool const new_depot_query_needed = blueprint_any_missing(blueprint) + || blueprint_any_rom_missing(blueprint) + || any_completed_download; + if (new_depot_query_needed) + trigger_depot_query(); + + _deploy.reattempt_after_installation(); + } + + _generate_dialog(); +} + + +void Sculpt::Main::_handle_runtime_state() +{ + _runtime_state_rom.update(); + + Xml_node state = _runtime_state_rom.xml(); + + _runtime_state.update_from_state_report(state); + + bool reconfigure_runtime = false; + bool regenerate_dialog = false; + + /* check for completed storage operations */ + _storage._storage_devices.for_each([&] (Storage_device &device) { + + device.for_each_partition([&] (Partition &partition) { + + Storage_target const target { device.label, partition.number }; + + if (partition.check_in_progress) { + String<64> name(target.label(), ".e2fsck"); + Child_exit_state exit_state(state, name); + + if (exit_state.exited) { + if (exit_state.code != 0) + error("file-system check failed"); + if (exit_state.code == 0) + log("file-system check succeeded"); + + partition.check_in_progress = 0; + reconfigure_runtime = true; + _reset_storage_widget_operation(); + } + } + + if (partition.format_in_progress) { + String<64> name(target.label(), ".mke2fs"); + Child_exit_state exit_state(state, name); + + if (exit_state.exited) { + if (exit_state.code != 0) + error("file-system creation failed"); + + partition.format_in_progress = false; + partition.file_system.type = File_system::EXT2; + + if (partition.whole_device()) + device.rediscover(); + + reconfigure_runtime = true; + _reset_storage_widget_operation(); + } + } + + /* respond to completion of file-system resize operation */ + if (partition.fs_resize_in_progress) { + Child_exit_state exit_state(state, Start_name(target.label(), ".resize2fs")); + if (exit_state.exited) { + partition.fs_resize_in_progress = false; + reconfigure_runtime = true; + device.rediscover(); + _reset_storage_widget_operation(); + } + } + + }); /* for each partition */ + + /* respond to failure of part_block */ + if (device.discovery_in_progress()) { + Child_exit_state exit_state(state, device.part_block_start_name()); + if (!exit_state.responsive) { + error(device.part_block_start_name(), " got stuck"); + device.state = Storage_device::RELEASED; + reconfigure_runtime = true; + } + } + + /* respond to completion of GPT relabeling */ + if (device.relabel_in_progress()) { + Child_exit_state exit_state(state, device.relabel_start_name()); + if (exit_state.exited) { + device.rediscover(); + reconfigure_runtime = true; + _reset_storage_widget_operation(); + } + } + + /* respond to completion of GPT expand */ + if (device.gpt_expand_in_progress()) { + Child_exit_state exit_state(state, device.expand_start_name()); + if (exit_state.exited) { + + /* kick off resize2fs */ + device.for_each_partition([&] (Partition &partition) { + if (partition.gpt_expand_in_progress) { + partition.gpt_expand_in_progress = false; + partition.fs_resize_in_progress = true; + } + }); + + reconfigure_runtime = true; + _reset_storage_widget_operation(); + } + } + + }); /* for each device */ + + /* handle failed initialization of USB-storage devices */ + _storage._storage_devices.usb_storage_devices.for_each([&] (Usb_storage_device &dev) { + String<64> name(dev.usb_block_drv_name()); + Child_exit_state exit_state(state, name); + if (exit_state.exited) { + dev.discard_usb_block_drv(); + reconfigure_runtime = true; + regenerate_dialog = true; + } + }); + + /* remove prepare subsystem when finished */ + { + Child_exit_state exit_state(state, "prepare"); + if (exit_state.exited) { + _prepare_completed = _prepare_version; + + /* trigger update and deploy */ + reconfigure_runtime = true; + } + } + + /* schedule pending file operations to new fs_tool instance */ + { + Child_exit_state exit_state(state, "fs_tool"); + + if (exit_state.exited) { + + Child_exit_state::Version const expected_version(_fs_tool_version.value); + + if (exit_state.version == expected_version) { + + _file_operation_queue.schedule_next_operations(); + _fs_tool_version.value++; + reconfigure_runtime = true; + + /* try to proceed after the first step of an depot-index update */ + unsigned const orig_download_count = _index_update_queue.download_count; + _index_update_queue.try_schedule_downloads(); + if (_index_update_queue.download_count != orig_download_count) + _deploy.update_installation(); + + /* update depot-user selection after adding new depot URL */ + if (_depot_user_selection_visible()) + trigger_depot_query(); + } + } + } + + /* upgrade RAM and cap quota on demand */ + state.for_each_sub_node("child", [&] (Xml_node child) { + + bool reconfiguration_needed = false; + _child_states.for_each([&] (Child_state &child_state) { + if (child_state.apply_child_state_report(child)) + reconfiguration_needed = true; }); + + if (reconfiguration_needed) { + reconfigure_runtime = true; + regenerate_dialog = true; + } + }); + + if (_deploy.update_child_conditions()) { + reconfigure_runtime = true; + regenerate_dialog = true; + } + + if (_dialog_runtime.apply_runtime_state(state)) + reconfigure_runtime = true; + + if (_software_title_bar.selected() && _software_tabs_widget.hosted.options_selected()) + regenerate_dialog = true; + + if (regenerate_dialog) + _generate_dialog(); + + if (reconfigure_runtime) + generate_runtime_config(); +} + + +void Sculpt::Main::_generate_runtime_config(Xml_generator &xml) const +{ + xml.attribute("verbose", "yes"); + + xml.attribute("prio_levels", _prio_levels.value); + + xml.node("report", [&] () { + xml.attribute("init_ram", "yes"); + xml.attribute("init_caps", "yes"); + xml.attribute("child_ram", "yes"); + xml.attribute("child_caps", "yes"); + xml.attribute("delay_ms", 4*500); + xml.attribute("buffer", "1M"); + }); + + xml.node("heartbeat", [&] () { xml.attribute("rate_ms", 2000); }); + + xml.node("parent-provides", [&] () { + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service<::File_system::Session>(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + gen_parent_service(xml); + }); + + xml.node("affinity-space", [&] () { + xml.attribute("width", _affinity_space.width()); + xml.attribute("height", _affinity_space.height()); + }); + + _dialog_runtime.gen_start_nodes(xml); + + _touch_keyboard.gen_start_node(xml); + + _storage.gen_runtime_start_nodes(xml); + + /* + * Load configuration and update depot config on the sculpt partition + */ + if (_storage._sculpt_partition.valid() && _prepare_in_progress()) + xml.node("start", [&] () { + gen_prepare_start_content(xml, _prepare_version); }); + + /* + * Spawn chroot instances for accessing '/depot' and '/public'. The + * chroot instances implicitly refer to the 'default_fs_rw'. + */ + if (_storage._sculpt_partition.valid()) { + + auto chroot = [&] (Start_name const &name, Path const &path, Writeable w) { + xml.node("start", [&] () { + gen_chroot_start_content(xml, name, path, w); }); }; + + if (_update_running()) { + chroot("depot_rw", "/depot", WRITEABLE); + chroot("public_rw", "/public", WRITEABLE); + } + + chroot("depot", "/depot", READ_ONLY); + } + + /* execute file operations */ + if (_storage._sculpt_partition.valid()) + if (_file_operation_queue.any_operation_in_progress()) + xml.node("start", [&] () { + gen_fs_tool_start_content(xml, _fs_tool_version, + _file_operation_queue); }); + + _network.gen_runtime_start_nodes(xml); + + if (_update_running()) + xml.node("start", [&] () { + gen_update_start_content(xml); }); + + if (_storage._sculpt_partition.valid() && !_prepare_in_progress()) { + xml.node("start", [&] () { + gen_launcher_query_start_content(xml); }); + + _deploy.gen_runtime_start_nodes(xml, _prio_levels, _affinity_space); + } +} + + +void Component::construct(Genode::Env &env) +{ + static Sculpt::Main main(env); +} + diff --git a/repos/gems/src/app/phone_manager/model/audio_volume.h b/repos/gems/src/app/phone_manager/model/audio_volume.h new file mode 100644 index 0000000000..87cc72d731 --- /dev/null +++ b/repos/gems/src/app/phone_manager/model/audio_volume.h @@ -0,0 +1,21 @@ +/* + * \brief Audio volume + * \author Norman Feske + * \date 2022-12-13 + */ + +/* + * Copyright (C) 2022 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MODEL__AUDIO_VOLUME_H_ +#define _MODEL__AUDIO_VOLUME_H_ + +#include + +namespace Sculpt { struct Audio_volume { unsigned value; }; } + +#endif /* _MODEL__AUDIO_VOLUME_H_ */ diff --git a/repos/gems/src/app/phone_manager/model/current_call.h b/repos/gems/src/app/phone_manager/model/current_call.h new file mode 100644 index 0000000000..161fcfd6ca --- /dev/null +++ b/repos/gems/src/app/phone_manager/model/current_call.h @@ -0,0 +1,215 @@ +/* + * \brief State of the current call + * \author Norman Feske + * \date 2022-06-29 + */ + +/* + * Copyright (C) 2022 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MODEL__CURRENT_CALL_H_ +#define _MODEL__CURRENT_CALL_H_ + +#include + +namespace Sculpt { struct Current_call; } + + +struct Sculpt::Current_call +{ + using Number = Modem_state::Number; + + Number number { }; + + enum class State { + + NONE, + + /* + * Entered by user interaction + */ + ACCEPTED, /* picked up incoming call */ + REJECTED, /* rejected incoming or active call */ + INITIATED, + CANCELED, /* canced outbound call */ + + /* + * Entered by modem activity + */ + INCOMING, + HUNG_UP, /* disconnected by callee */ + OUTBOUND, /* dialing */ + ALERTING, /* ring at the callee */ + ACTIVE, + }; + + State state = State::NONE; + + /** + * Return true if the modem state is applicable to the current state + */ + bool _applicable(Modem_state const &modem) const + { + if (state == State::NONE) + return true; + + /* accept state updates when current state already reflects the modem */ + bool const entered_by_modem_activity = (state == State::INCOMING) + || (state == State::HUNG_UP) + || (state == State::OUTBOUND) + || (state == State::ALERTING) + || (state == State::ACTIVE); + if (entered_by_modem_activity) + return true; + + /* an accepted incoming call became active */ + if ((state == State::ACCEPTED) && modem.active_call()) + return true; + + /* forget last canceled initiated call when a new call comes in */ + if ((state == State::CANCELED) && modem.incoming_call()) + return true; + + /* reset canceled state once the modem cleared the call */ + if ((state == State::CANCELED) && !modem.any_call()) + return true; + + /* reset rejected state once the modem cleared the call */ + if ((state == State::REJECTED) && !modem.any_call()) + return true; + + /* clear rejected state when called from a different number */ + if ((state == State::REJECTED) && modem.incoming_call() && (modem.number() != number)) + return true; + + /* an initiated call is reflected by the modem as outbound or alerting */ + if ((state == State::INITIATED) && modem.outbound_call()) + return true; + + /* an initiated call became active */ + if ((state == State::INITIATED) && modem.active_call()) + return true; + + return false; + } + + static State _from_modem_call_state(Modem_state::Call_state call_state) + { + switch (call_state) { + case Modem_state::Call_state::INCOMING: return State::INCOMING; + case Modem_state::Call_state::ACTIVE: return State::ACTIVE; + case Modem_state::Call_state::ALERTING: return State::ALERTING; + case Modem_state::Call_state::OUTBOUND: return State::OUTBOUND; + case Modem_state::Call_state::NONE: break; + }; + return State::NONE; + } + + bool speaker = false; + + bool connecting() const + { + return state == State::INITIATED + || state == State::OUTBOUND + || state == State::ALERTING; + } + + bool incoming() const { return state == State::INCOMING; } + bool accepted() const { return state == State::ACCEPTED; } + bool active() const { return state == State::ACTIVE; } + bool none() const { return state == State::NONE; } + bool canceled() const { return state == State::CANCELED; } + + void accept() + { + if (state == State::INCOMING) { + state = State::ACCEPTED; + speaker = false; + } + } + + void reject() + { + if (state == State::INCOMING || state == State::ACTIVE) + state = State::REJECTED; + speaker = false; + } + + void initiate(Number const &n) + { + number = n; + state = State::INITIATED; + speaker = false; + } + + void cancel() + { + state = State::CANCELED; + speaker = false; + } + + void toggle_speaker() + { + speaker = !speaker; + } + + void update(Modem_state const &modem) + { + if (_applicable(modem)) { + state = _from_modem_call_state(modem.call_state()); + number = modem.number(); + } + + if (state == State::NONE) + speaker = false; + } + + void gen_modem_config(Xml_generator &xml) const + { + xml.attribute("speaker", speaker ? "yes" : "no"); + + switch (state) { + case State::NONE: + case State::INCOMING: + break; + + case State::ACCEPTED: + xml.node("call", [&] { + xml.attribute("number", number); + xml.attribute("state", "accepted"); + }); + break; + + case State::REJECTED: + case State::HUNG_UP: + case State::CANCELED: + xml.node("call", [&] { + xml.attribute("number", number); + xml.attribute("state", "rejected"); + }); + break; + + case State::INITIATED: + case State::OUTBOUND: + case State::ALERTING: + case State::ACTIVE: + xml.node("call", [&] { + xml.attribute("number", number); + }); + break; + } + } + + bool operator != (Current_call const &other) const + { + return (number != other.number) + || (state != other.state) + || (speaker != other.speaker); + } +}; + +#endif /* _MODEL__CURRENT_CALL_H_ */ diff --git a/repos/gems/src/app/phone_manager/model/dialed_number.h b/repos/gems/src/app/phone_manager/model/dialed_number.h new file mode 100644 index 0000000000..1b37404c19 --- /dev/null +++ b/repos/gems/src/app/phone_manager/model/dialed_number.h @@ -0,0 +1,84 @@ +/* + * \brief Dialed number + * \author Norman Feske + * \date 2018-05-23 + */ + +/* + * Copyright (C) 2018 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MODEL__DIALED_NUMBER_H_ +#define _MODEL__DIALED_NUMBER_H_ + +#include +#include +#include +#include + +namespace Sculpt { struct Dialed_number; } + + +struct Sculpt::Dialed_number : Noncopyable +{ + public: + + struct Digit + { + char value; + + bool valid() const { return (value >= '0' && value <= '9') + || (value == '#') + || (value == '*'); } + + void print(Output &out) const + { + if (valid()) + Genode::print(out, Char(value)); + } + }; + + private: + + enum { CAPACITY = 32 }; + Digit _digits[CAPACITY] { }; + + unsigned _length = 0; + + public: + + void print(Output &out) const + { + for (unsigned i = 0; i < _length; i++) + Genode::print(out, _digits[i]); + } + + void append_digit(Digit d) + { + /* ignore out-of-range digit values */ + if (!d.valid()) + return; + + if (_length < CAPACITY) { + _digits[_length] = d; + _length++; + } + } + + void remove_last_digit() + { + if (_length > 0) { + _length--; + _digits[_length].value = 0; + } + } + + bool suitable_for_call() const { return _length >= 3; } + + bool at_least_one_digit() const { return _length > 0; } +}; + +#endif /* _MODEL__DIALED_NUMBER_H_ */ diff --git a/repos/gems/src/app/phone_manager/model/mic_state.h b/repos/gems/src/app/phone_manager/model/mic_state.h new file mode 100644 index 0000000000..fe58c4d59a --- /dev/null +++ b/repos/gems/src/app/phone_manager/model/mic_state.h @@ -0,0 +1,21 @@ +/* + * \brief Microphone state + * \author Norman Feske + * \date 2022-12-13 + */ + +/* + * Copyright (C) 2022 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MODEL__MIC_STATE_H_ +#define _MODEL__MIC_STATE_H_ + +#include + +namespace Sculpt { enum class Mic_state { OFF, PHONE, ON }; } + +#endif /* _MODEL__MIC_STATE_H_ */ diff --git a/repos/gems/src/app/phone_manager/model/modem_state.h b/repos/gems/src/app/phone_manager/model/modem_state.h new file mode 100644 index 0000000000..b0b71ced84 --- /dev/null +++ b/repos/gems/src/app/phone_manager/model/modem_state.h @@ -0,0 +1,195 @@ +/* + * \brief Modem state as retrieved from the modem driver + * \author Norman Feske + * \date 2022-05-20 + */ + +/* + * Copyright (C) 2022 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MODEL__MODEM_STATE_H_ +#define _MODEL__MODEM_STATE_H_ + +namespace Sculpt { struct Modem_state; } + + +struct Sculpt::Modem_state +{ + enum class Power { UNAVAILABLE, OFF, STARTING_UP, ON, SHUTTING_DOWN }; + + Power _power; + + using Number = String<128>; + + enum class Call_state { NONE, INCOMING, ACTIVE, OUTBOUND, ALERTING }; + + Call_state _call_state = Call_state::NONE; + + Number _number; + + unsigned _startup_seconds; + unsigned _shutdown_seconds; + + enum class Pin_state { UNKNOWN, REQUIRED, CHECKING, REJECTED, OK, PUK_NEEDED }; + Pin_state _pin_state; + + unsigned _pin_remaining_attempts; + + bool transient() const + { + return _power == Power::STARTING_UP + || _power == Power::SHUTTING_DOWN; + } + + bool on() const + { + return _power == Power::ON + || _power == Power::STARTING_UP; + } + + bool ready() const { return _power == Power::ON; } + + Call_state call_state() const { return _call_state; } + + Number number() const { return _number; } + + bool any_call() const { return _call_state != Call_state::NONE; } + + bool incoming_call() const { return _call_state == Call_state::INCOMING; } + + bool outbound_call() const { return _call_state == Call_state::OUTBOUND + || _call_state == Call_state::ALERTING; } + + bool active_call() const { return _call_state == Call_state::ACTIVE; } + + bool pin_required() const { return _pin_state == Pin_state::REQUIRED + || _pin_state == Pin_state::REJECTED; } + + bool pin_ok() const { return _pin_state == Pin_state::OK; } + + bool pin_rejected() const { return _pin_state == Pin_state::REJECTED; } + + using Power_message = String<128>; + + Power_message power_message() const + { + using Msg = Power_message; + + switch (_power) { + case Power::STARTING_UP: return Msg(" starting up (", _startup_seconds, ") "); + case Power::SHUTTING_DOWN: return Msg(" shutting down (", _shutdown_seconds, ") "); + case Power::ON: + + switch (_pin_state) { + + case Pin_state::REQUIRED: + return Msg(" PIN required"); + + case Pin_state::REJECTED: + return (_pin_remaining_attempts == 1) + ? Msg(" PIN rejected (one more try) ") + : Msg(" PIN rejected (", _pin_remaining_attempts, " more tries) "); + + case Pin_state::CHECKING: + return Msg(" checking PIN ... "); + + case Pin_state::OK: + return Msg(" ready "); + + case Pin_state::PUK_NEEDED: + return Msg(" PUK needed, giving up. "); + + case Pin_state::UNKNOWN: + break; + } + return Msg(" unknown PIN state "); + + case Power::OFF: return Msg(" powered off "); + case Power::UNAVAILABLE: break; + } + return Msg(" unavailable" ); + } + + bool operator != (Modem_state const &other) const + { + return other._power != _power + || other._number != _number + || other._call_state != _call_state + || other._startup_seconds != _startup_seconds + || other._shutdown_seconds != _shutdown_seconds + || other._pin_state != _pin_state + || other._pin_remaining_attempts != _pin_remaining_attempts; + } + + static Modem_state from_xml(Xml_node const &node) + { + auto power = [&] + { + auto value = node.attribute_value("power", String<20>()); + + if (value == "on") return Power::ON; + if (value == "starting up") return Power::STARTING_UP; + if (value == "shutting down") return Power::SHUTTING_DOWN; + if (value == "off") return Power::OFF; + + return Power::UNAVAILABLE; + }; + + auto pin_state = [&] + { + auto value = node.attribute_value("pin", String<20>()); + + if (value == "required") + return node.has_attribute("pin_remaining_attempts") + ? Pin_state::REJECTED : Pin_state::REQUIRED; + + if (value == "checking") return Pin_state::CHECKING; + if (value == "ok") return Pin_state::OK; + if (value == "puk needed") return Pin_state::PUK_NEEDED; + + return Pin_state::UNKNOWN; + }; + + auto number = [&] + { + Number result { }; + node.with_optional_sub_node("call", [&] (Xml_node const &call) { + result = call.attribute_value("number", Number()); }); + return result; + }; + + auto call_state = [&] + { + Call_state result = Call_state::NONE; + + node.with_optional_sub_node("call", [&] (Xml_node const &call) { + + auto const type = call.attribute_value("state", String<20>()); + + result = (type == "incoming") ? Call_state::INCOMING + : (type == "active") ? Call_state::ACTIVE + : (type == "outbound") ? Call_state::OUTBOUND + : (type == "alerting") ? Call_state::ALERTING + : Call_state::NONE; + }); + + return result; + }; + + return Modem_state { + ._power = power(), + ._call_state = call_state(), + ._number = number(), + ._startup_seconds = node.attribute_value("startup_seconds", 0u), + ._shutdown_seconds = node.attribute_value("shutdown_seconds", 0u), + ._pin_state = pin_state(), + ._pin_remaining_attempts = node.attribute_value("pin_remaining_attempts", 0u), + }; + } +}; + +#endif /* _MODEL__MODEM_STATE_H_ */ diff --git a/repos/gems/src/app/phone_manager/model/power_state.h b/repos/gems/src/app/phone_manager/model/power_state.h new file mode 100644 index 0000000000..fe39abc203 --- /dev/null +++ b/repos/gems/src/app/phone_manager/model/power_state.h @@ -0,0 +1,102 @@ +/* + * \brief Power state as provided by the power driver + * \author Norman Feske + * \date 2022-11-09 + */ + +/* + * Copyright (C) 2022 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MODEL__POWER_STATE_H_ +#define _MODEL__POWER_STATE_H_ + +#include + +namespace Sculpt { struct Power_state; } + + +struct Sculpt::Power_state +{ + bool ac_present, battery_present, charging; + + double voltage; + + struct Battery + { + double charge_current, power_draw; + + unsigned remaining_capacity; + + static Battery from_xml(Xml_node const &battery) + { + return Battery { + .charge_current = battery.attribute_value("charge_current", 0.0), + .power_draw = battery.attribute_value("power_draw", 0.0), + .remaining_capacity = battery.attribute_value("remaining_capacity", 0u) + }; + } + }; + + Battery battery; + + enum class Profile { UNKNOWN, PERFORMANCE, ECONOMIC }; + + Profile profile; + + unsigned brightness; + + static Power_state from_xml(Xml_node const &node) + { + Battery battery { }; + + node.with_optional_sub_node("battery", [&] (Xml_node const &node) { + battery = Battery::from_xml(node); + }); + + auto profile_from_xml = [] (Xml_node const &node) + { + auto value = node.attribute_value("power_profile", String<64>()); + + if (value == "performance") return Profile::PERFORMANCE; + if (value == "economic") return Profile::ECONOMIC; + + return Profile::UNKNOWN; + }; + + return Power_state { + .ac_present = node.attribute_value("ac_present", false), + .battery_present = node.has_sub_node("battery"), + .charging = node.attribute_value("charging", false), + .voltage = node.attribute_value("voltage", 0.0), + .battery = battery, + .profile = profile_from_xml(node), + .brightness = node.attribute_value("brightness", 0u), + }; + } + + using Summary = String<128>; + + Summary summary() const + { + if (!battery_present) + return "AC"; + + return Summary(battery.remaining_capacity,"%", charging ? " +": ""); + } + + bool modem_present() const + { + /* assume presence of a modem before receiving the first report */ + bool const uncertain = !ac_present && !battery_present; + if (uncertain) + return true; + + return battery_present; + } +}; + +#endif /* _MODEL__POWER_STATE_H_ */ diff --git a/repos/gems/src/app/phone_manager/model/sim_pin.h b/repos/gems/src/app/phone_manager/model/sim_pin.h new file mode 100644 index 0000000000..815f1f155d --- /dev/null +++ b/repos/gems/src/app/phone_manager/model/sim_pin.h @@ -0,0 +1,123 @@ +/* + * \brief SIM pin + * \author Norman Feske + * \date 2018-05-23 + */ + +/* + * Copyright (C) 2018 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MODEL__SIM_PIN_H_ +#define _MODEL__SIM_PIN_H_ + +#include +#include +#include +#include + + +namespace Sculpt { struct Bullet; } + +struct Sculpt::Bullet +{ + void print(Output &out) const + { + char const bullet_utf8[4] = { (char)0xe2, (char)0x80, (char)0xa2, 0 }; + Genode::print(out, bullet_utf8); + } +}; + + +namespace Sculpt { struct Blind_sim_pin; } + + +struct Sculpt::Blind_sim_pin +{ + virtual void print_bullets(Output &) const = 0; + + virtual ~Blind_sim_pin() { } + + void print(Output &out) const { print_bullets(out); } + + virtual bool suitable_for_unlock() const = 0; + + virtual bool at_least_one_digit() const = 0; +}; + + +namespace Sculpt { struct Sim_pin; } + + +struct Sculpt::Sim_pin : Blind_sim_pin +{ + public: + + struct Digit { unsigned value; }; + + private: + + enum { CAPACITY = 4 }; + Digit _digits[CAPACITY] { }; + + unsigned _length = 0; + + public: + + bool confirmed = false; + + void print(Output &out) const + { + for (unsigned i = 0; i < _length; i++) + Genode::print(out, _digits[i].value); + } + + void append_digit(Digit d) + { + /* ignore out-of-range digit values */ + if (d.value > 9) + return; + + if (_length < CAPACITY) { + _digits[_length] = d; + _length++; + } + } + + void remove_last_digit() + { + if (_length > 0) { + _length--; + _digits[_length].value = 0; + } + } + + void print_bullets(Output &out) const override + { + for (unsigned i = 0; i < _length; i++) + Genode::print(out, Bullet()); + } + + bool suitable_for_unlock() const override { return _length == 4; } + + bool at_least_one_digit() const override { return _length > 0; } + + bool operator != (Sim_pin const &other) const + { + auto any_digit_differs = [&] (auto a, auto b) + { + for (unsigned i = 0; i < _length; i++) + if (a[i].value != b[i].value) + return true; + return false; + }; + + return (_length != other._length) + || any_digit_differs(_digits, other._digits); + } +}; + +#endif /* _MODEL__SIM_PIN_H_ */ diff --git a/repos/gems/src/app/phone_manager/runtime/touch_keyboard.h b/repos/gems/src/app/phone_manager/runtime/touch_keyboard.h new file mode 100644 index 0000000000..0cf9a462d2 --- /dev/null +++ b/repos/gems/src/app/phone_manager/runtime/touch_keyboard.h @@ -0,0 +1,89 @@ +/* + * \brief XML configuration for spawning the administrative touch keyboard + * \author Norman Feske + * \date 2023-03-17 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _RUNTIME__TOUCH_KEYBOARD_H_ +#define _RUNTIME__TOUCH_KEYBOARD_H_ + +#include + +namespace Sculpt { + + enum class Alpha { OPAQUE, ALPHA }; + + struct Touch_keyboard_attr + { + unsigned min_width, min_height; + Alpha alpha; + Color background; + }; + + static inline void gen_touch_keyboard(Xml_generator &, Touch_keyboard_attr); +} + + +void Sculpt::gen_touch_keyboard(Xml_generator &xml, Touch_keyboard_attr const attr) +{ + xml.node("start", [&] () { + + gen_common_start_content(xml, "manager_keyboard", + Cap_quota{700}, Ram_quota{18*1024*1024}, + Priority::LEITZENTRALE); + + gen_named_node(xml, "binary", "touch_keyboard", [&] () { }); + + xml.node("config", [&] () { + xml.attribute("min_width", attr.min_width); + xml.attribute("min_height", attr.min_height); + + if (attr.alpha == Alpha::OPAQUE) + xml.attribute("opaque", "yes"); + + xml.attribute("background", String<20>(attr.background)); + }); + + xml.node("route", [&] () { + gen_parent_rom_route(xml, "ld.lib.so"); + gen_parent_rom_route(xml, "touch_keyboard"); + gen_parent_rom_route(xml, "layout", "touch_keyboard_layout.config"); + gen_parent_rom_route(xml, "menu_view"); + gen_parent_rom_route(xml, "ld.lib.so"); + gen_parent_rom_route(xml, "vfs.lib.so"); + gen_parent_rom_route(xml, "libc.lib.so"); + gen_parent_rom_route(xml, "libm.lib.so"); + gen_parent_rom_route(xml, "libpng.lib.so"); + gen_parent_rom_route(xml, "zlib.lib.so"); + gen_parent_rom_route(xml, "sandbox.lib.so"); + gen_parent_rom_route(xml, "menu_view_styles.tar"); + + gen_parent_route (xml); + gen_parent_route (xml); + gen_parent_route (xml); + gen_parent_route (xml); + + gen_service_node<::File_system::Session>(xml, [&] () { + xml.attribute("label", "fonts"); + xml.node("parent", [&] () { + xml.attribute("label", "leitzentrale -> fonts"); }); }); + + gen_service_node(xml, [&] () { + xml.node("parent", [&] () { + xml.attribute("label", "leitzentrale -> touch_keyboard"); }); }); + + gen_service_node(xml, [&] () { + xml.node("parent", [&] () { + xml.attribute("label", "global"); }); }); + }); + }); +} + +#endif /* _RUNTIME__TOUCH_KEYBOARD_H_ */ diff --git a/repos/gems/src/app/phone_manager/target.mk b/repos/gems/src/app/phone_manager/target.mk new file mode 100644 index 0000000000..ee8bebe1a7 --- /dev/null +++ b/repos/gems/src/app/phone_manager/target.mk @@ -0,0 +1,15 @@ +TARGET := phone_manager + +SCULPT_MANAGER_DIR := $(call select_from_repositories,src/app/sculpt_manager) +DEPOT_DEPLOY_DIR := $(call select_from_repositories,src/app/depot_deploy) + +SRC_CC += $(notdir $(wildcard $(PRG_DIR)/*.cc)) +SRC_CC += $(addprefix view/, $(notdir $(wildcard $(SCULPT_MANAGER_DIR)/view/*.cc))) +SRC_CC += $(addprefix runtime/, $(notdir $(wildcard $(SCULPT_MANAGER_DIR)/runtime/*.cc))) +SRC_CC += $(addprefix dialog/, $(notdir $(wildcard $(SCULPT_MANAGER_DIR)/dialog/*.cc))) +SRC_CC += gui.cc graph.cc deploy.cc storage.cc network.cc +LIBS += base +INC_DIR += $(PRG_DIR) $(SCULPT_MANAGER_DIR) $(DEPOT_DEPLOY_DIR) + +vpath %.cc $(PRG_DIR) +vpath %.cc $(SCULPT_MANAGER_DIR) diff --git a/repos/gems/src/app/phone_manager/view/component_add_widget.h b/repos/gems/src/app/phone_manager/view/component_add_widget.h new file mode 100644 index 0000000000..5b81fc4831 --- /dev/null +++ b/repos/gems/src/app/phone_manager/view/component_add_widget.h @@ -0,0 +1,250 @@ +/* + * \brief Widget for configuring a new component deployed from a depot package + * \author Norman Feske + * \date 2023-03-23 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _VIEW__COMPONENT_ADD_WIDGET_H_ +#define _VIEW__COMPONENT_ADD_WIDGET_H_ + +#include +#include +#include +#include +#include +#include + +namespace Sculpt { struct Component_add_widget; } + + +struct Sculpt::Component_add_widget : Widget +{ + using Name = Start_name; + using Action = Component::Construction_action; + + Runtime_config const &_runtime_config; + + using Route_entry = Hosted; + using Service_entry = Hosted; + + Hosted _back { Id { "back" } }; + Hosted _launch { Id { "Add component" } }; + Hosted _resources { Id { "resources" } }; + Hosted _pd_route { Id { "pd_route" }, _runtime_config }; + Hosted _debug { Id { "debug" } }; + + Id _selected_route { }; + + template + void _apply_to_selected_route(Action &action, FN const &fn) + { + unsigned count = 0; + action.apply_to_construction([&] (Component &component) { + component.routes.for_each([&] (Route &route) { + if (_route_selected(Route::Id(count++))) + fn(route); }); }); + } + + bool _route_selected(Route::Id const &id) const + { + return _selected_route.valid() && id == _selected_route.value; + } + + bool _resource_widget_selected() const + { + return _route_selected("resources"); + } + + void _view_pkg_elements(Scope &s, Component const &component) const + { + using Info = Component::Info; + + s.widget(_back, Name { "Add ", Pretty(component.name) }); + + s.widget(Hosted { Id { "info" } }, component); + + s.sub_scope(Info(Capacity{component.ram}, " ", + component.caps, " caps")); + s.sub_scope(); + + unsigned count = 0; + component.routes.for_each([&] (Route const &route) { + + Id const id { Id::Value { count++ } }; + + s.sub_scope([&] (Scope &s) { + s.sub_scope([&] (Scope &s) { + + bool const selected = _route_selected(id.value); + bool const defined = route.selected_service.constructed(); + + if (!selected) { + Route_entry entry { id }; + s.widget(entry, defined, + defined ? Info(route.selected_service->info) + : Info(route)); + } + + /* + * List of routing options + */ + if (selected) { + Route_entry back { Id { "back" } }; + s.widget(back, true, Info(route), "back"); + + unsigned count = 0; + _runtime_config.for_each_service([&] (Service const &service) { + + Id const service_id { Id::Value("service.", count++) }; + + bool const service_selected = + route.selected_service.constructed() && + service_id.value == route.selected_service_id; + + if (service.type == route.required) { + Service_entry entry { service_id }; + s.widget(entry, service_selected, service.info); + } + }); + } + }); + }); + }); + + /* don't show the PD menu if only the system PD service is available */ + if (_runtime_config.num_service_options(Service::Type::PD) > 1) + s.sub_scope([&] (Scope &s) { + s.widget(_pd_route, _selected_route, component); }); + + s.sub_scope(Id { "resources" }, [&] (Scope &s) { + s.sub_scope([&] (Scope &s) { + + bool const selected = _route_selected("resources"); + + if (!selected) { + Route_entry entry { Id { "resources" } }; + s.widget(entry, false, "Resource assignment ...", "enter"); + } + + if (selected) { + Route_entry entry { Id { "back" } }; + s.widget(entry, true, "Resource assignment ...", "back"); + + s.widget(_resources, component); + } + }); + }); + + s.sub_scope([&] (Scope &s) { + s.widget(_debug, component); }); + + /* + * Display "Add component" button once all routes are defined + */ + if (component.all_routes_defined()) + s.widget(_launch); + } + + Component_add_widget(Runtime_config const &runtime_config) + : + _runtime_config(runtime_config) + { } + + void view(Scope &s, Component const &component) const + { + _view_pkg_elements(s, component); + } + + void click(Clicked_at const &at, Action &action, auto const &leave_fn) + { + _back.propagate(at, [&] { leave_fn(); }); + + _launch.propagate(at); + + Id const route_id = at.matching_id(); + + /* select route to present routing options */ + if (!_selected_route.valid() && route_id.valid()) + _selected_route = route_id; + + /* + * Route selected + */ + + /* close selected route */ + if (route_id.value == "back") { + _selected_route = { }; + + } else if (_resource_widget_selected()) { + + bool const clicked_on_different_route = route_id.valid(); + if (clicked_on_different_route) { + _selected_route = route_id; + + } else { + + action.apply_to_construction([&] (Component &component) { + _resources.propagate(at, component); }); + } + + } else { + + bool clicked_on_selected_route = false; + + _apply_to_selected_route(action, [&] (Route &route) { + + unsigned count = 0; + _runtime_config.for_each_service([&] (Service const &service) { + + Id const id { Id::Value("service.", count++) }; + + if (route_id == id) { + + bool const clicked_service_already_selected = + route.selected_service.constructed() && + id.value == route.selected_service_id; + + if (clicked_service_already_selected) { + + /* clear selection */ + route.selected_service.destruct(); + route.selected_service_id = { }; + + } else { + + /* select different service */ + route.selected_service.construct(service); + route.selected_service_id = id.value; + } + + _selected_route = { }; + + clicked_on_selected_route = true; + } + }); + }); + + if (_selected_route == _pd_route.id) + action.apply_to_construction([&] (Component &component) { + _pd_route.propagate(at, component); }); + + /* select different route */ + if (!clicked_on_selected_route && route_id.valid()) + _selected_route = route_id; + } + } + + void clack(Clacked_at const &at, auto const &launch_fn) + { + _launch.propagate(at, launch_fn); + } +}; + +#endif /* _VIEW__COMPONENT_ADD_WIDGET_H_ */ diff --git a/repos/gems/src/app/phone_manager/view/component_info_widget.h b/repos/gems/src/app/phone_manager/view/component_info_widget.h new file mode 100644 index 0000000000..e3a638d652 --- /dev/null +++ b/repos/gems/src/app/phone_manager/view/component_info_widget.h @@ -0,0 +1,34 @@ +/* + * \brief Widget for displaying component information for the add menu + * \author Norman Feske + * \date 2023-11-13 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _VIEW__COMPONENT_INFO_WIDGET_H_ +#define _VIEW__COMPONENT_INFO_WIDGET_H_ + +#include + +namespace Sculpt { struct Component_info_widget; } + + +struct Sculpt::Component_info_widget : Widget +{ + void view(Scope &s, Component const &component) const + { + if (component.info.length() > 1) { + s.sub_scope