Files
Upsilon/apps/apps_container.cpp
2023-02-17 19:32:50 +01:00

499 lines
19 KiB
C++

#include "apps_container.h"
#include "apps_container_storage.h"
#include "global_preferences.h"
#include "exam_mode_configuration.h"
#include <ion.h>
#include <poincare/init.h>
#include <poincare/exception_checkpoint.h>
#include <ion/backlight.h>
#include <poincare/preferences.h>
#include <algorithm>
extern "C" {
#include <assert.h>
}
using namespace Poincare;
using namespace Shared;
AppsContainer * AppsContainer::sharedAppsContainer() {
static AppsContainerStorage appsContainerStorage;
return &appsContainerStorage;
}
AppsContainer::AppsContainer() :
Container(),
m_window(),
m_emptyBatteryWindow(),
m_globalContext(),
m_variableBoxController(),
m_examPopUpController(this),
m_promptController(k_promptMessages, k_promptFGColors, k_promptBGColors, k_promptNumberOfMessages),
m_batteryTimer(),
m_suspendTimer(),
m_backlightDimmingTimer(),
m_clockTimer(ClockTimer(this)),
m_homeSnapshot(),
m_onBoardingSnapshot(),
m_hardwareTestSnapshot(),
m_usbConnectedSnapshot(),
m_startAppSnapshot()
{
m_emptyBatteryWindow.setFrame(KDRect(0, 0, Ion::Display::Width, Ion::Display::Height), false);
// #if __EMSCRIPTEN__
/* AppsContainer::poincareCircuitBreaker uses Ion::Keyboard::scan(), which
* calls emscripten_sleep. If we set the poincare circuit breaker, we would
* need to whitelist all the methods that might be in the call stack when
* poincareCircuitBreaker is run. This means either whitelisting all Epsilon
* (which makes bigger files to download and slower execution), or
* whitelisting all the symbols (that's a big amount of symbols to find and
* quite paint to maintain).
* We just remove the circuit breaker for now.
* TODO: Put the Poincare circuit breaker back on epsilon's web emulator */
/*
* This can be run in Omega, since it uses WebASM.
*/
// #else
Poincare::Expression::SetCircuitBreaker(AppsContainer::poincareCircuitBreaker);
// #endif
Ion::Storage::sharedStorage()->setDelegate(this);
addTimer(&m_batteryTimer);
addTimer(&m_suspendTimer);
addTimer(&m_backlightDimmingTimer);
addTimer(&m_clockTimer);
}
bool AppsContainer::poincareCircuitBreaker() {
constexpr uint64_t minimalPressDuration = 20;
static uint64_t beginningOfInterruption = 0;
Ion::Keyboard::State state = Ion::Keyboard::scan();
bool interrupt = state.keyDown(Ion::Keyboard::Key::Back) || state.keyDown(Ion::Keyboard::Key::Home) || state.keyDown(Ion::Keyboard::Key::OnOff);
if (!interrupt) {
beginningOfInterruption = 0;
return false;
}
if (beginningOfInterruption == 0) {
beginningOfInterruption = Ion::Timing::millis();
return false;
}
if (Ion::Timing::millis() - beginningOfInterruption > minimalPressDuration) {
beginningOfInterruption = 0;
return true;
}
return false;
}
App::Snapshot * AppsContainer::hardwareTestAppSnapshot() {
return &m_hardwareTestSnapshot;
}
App::Snapshot * AppsContainer::onBoardingAppSnapshot() {
return &m_onBoardingSnapshot;
}
App::Snapshot * AppsContainer::usbConnectedAppSnapshot() {
return &m_usbConnectedSnapshot;
}
void AppsContainer::reset() {
// Empty storage (delete functions, variables, python scripts)
Ion::Storage::sharedStorage()->destroyAllRecords();
// Empty clipboard
Clipboard::sharedClipboard()->reset();
for (int i = 0; i < numberOfApps(); i++) {
appSnapshotAtIndex(i)->reset();
}
}
Poincare::Context * AppsContainer::globalContext() {
return &m_globalContext;
}
MathToolbox * AppsContainer::mathToolbox() {
return &m_mathToolbox;
}
MathVariableBoxController * AppsContainer::variableBoxController() {
return &m_variableBoxController;
}
void AppsContainer::suspend(bool checkIfOnOffKeyReleased) {
resetShiftAlphaStatus();
GlobalPreferences * globalPreferences = GlobalPreferences::sharedGlobalPreferences();
// Display the prompt if it has a message to display
if (promptController() != nullptr && s_activeApp->snapshot()!= onBoardingAppSnapshot() && s_activeApp->snapshot() != hardwareTestAppSnapshot() && globalPreferences->showPopUp()) {
s_activeApp->displayModalViewController(promptController(), 0.f, 0.f);
}
Ion::Power::suspend(checkIfOnOffKeyReleased);
/* Ion::Power::suspend() completely shuts down the LCD controller. Therefore
* the frame memory is lost. That's why we need to force a window redraw
* upon wakeup, otherwise the screen is filled with noise. */
Ion::Backlight::setBrightness(globalPreferences->brightnessLevel());
m_backlightDimmingTimer.reset();
window()->redraw(true);
}
bool AppsContainer::dispatchEvent(Ion::Events::Event event) {
bool alphaLockWantsRedraw = updateAlphaLock();
bool didProcessEvent = false;
if (event == Ion::Events::USBEnumeration || event == Ion::Events::USBPlug || event == Ion::Events::BatteryCharging) {
Ion::LED::updateColorWithPlugAndCharge();
}
if (event == Ion::Events::USBEnumeration) {
if (Ion::USB::isPlugged()) {
App::Snapshot * activeSnapshot = (s_activeApp == nullptr ? appSnapshotAtIndex(0) : s_activeApp->snapshot());
/* Just after a software update, the battery timer does not have time to
* fire before the calculator enters DFU mode. As the DFU mode blocks the
* event loop, we update the battery state "manually" here.
* We do it before switching to USB application to redraw the battery
* pictogram. */
updateBatteryState();
if (switchTo(usbConnectedAppSnapshot())) {
Ion::USB::DFU(true);
// Update LED when exiting DFU mode
Ion::LED::updateColorWithPlugAndCharge();
bool switched = switchTo(activeSnapshot);
assert(switched);
(void) switched; // Silence compilation warning about unused variable.
didProcessEvent = true;
} else {
/* We could not switch apps, which means that the current app needs
* another event loop to prepare for being switched off.
* Discard the current enumeration interruption.
* The USB host tries a few times in a row to enumerate the device, so
* hopefully the device will get another enumeration event soon and this
* time the device will be ready to go in DFU mode. Otherwise, the user
* needs to re-plug the device to go into DFU mode. */
Ion::USB::clearEnumerationInterrupt();
}
} else {
/* Sometimes, the device gets an ENUMDNE interrupts when being unplugged
* from a non-USB communicating host (e.g. a USB charger). The interrupt
* must me cleared: if not the next enumeration attempts will not be
* detected. */
Ion::USB::clearEnumerationInterrupt();
}
} else {
if (KDIonContext::sharedContext()->zoomEnabled) {
bool changedZoom = true;
if (event == Ion::Events::ShiftOne) {
KDIonContext::sharedContext()->zoomPosition = 0;
} else if (event == Ion::Events::ShiftTwo) {
KDIonContext::sharedContext()->zoomPosition = 1;
} else if (event == Ion::Events::ShiftThree) {
KDIonContext::sharedContext()->zoomPosition = 2;
} else if (event == Ion::Events::ShiftFour) {
KDIonContext::sharedContext()->zoomPosition = 3;
} else if (event == Ion::Events::ShiftFive) {
KDIonContext::sharedContext()->zoomPosition = 4;
} else if (event == Ion::Events::ShiftSix) {
KDIonContext::sharedContext()->zoomPosition = 5;
} else if (event == Ion::Events::ShiftSeven) {
KDIonContext::sharedContext()->zoomPosition = 6;
} else if (event == Ion::Events::ShiftEight) {
KDIonContext::sharedContext()->zoomPosition = 7;
} else if (event == Ion::Events::ShiftNine) {
KDIonContext::sharedContext()->zoomPosition = 8;
} else {
changedZoom = false;
}
if (changedZoom) {
KDIonContext::sharedContext()->updatePostProcessingEffects();
redrawWindow(true);
return true;
}
}
didProcessEvent = Container::dispatchEvent(event);
}
if (!didProcessEvent) {
didProcessEvent = processEvent(event);
}
if (event.isKeyboardEvent()) {
m_backlightDimmingTimer.reset();
m_suspendTimer.reset();
Ion::Backlight::setBrightness(GlobalPreferences::sharedGlobalPreferences()->brightnessLevel());
}
if (!didProcessEvent && alphaLockWantsRedraw) {
window()->redraw();
return true;
}
return didProcessEvent || alphaLockWantsRedraw;
}
// List of keys that are used to switch between apps, in order of app to go (eg. 0 : First App, 1 : Second App, 2 : Third App, ...)
static constexpr Ion::Events::Event switch_events[] = {
Ion::Events::ShiftSeven, Ion::Events::ShiftEight, Ion::Events::ShiftNine,
Ion::Events::ShiftFour, Ion::Events::ShiftFive, Ion::Events::ShiftSix,
Ion::Events::ShiftOne, Ion::Events::ShiftTwo, Ion::Events::ShiftThree,
Ion::Events::ShiftZero, Ion::Events::ShiftDot, Ion::Events::ShiftEE
};
bool AppsContainer::processEvent(Ion::Events::Event event) {
// Warning: if the window is dirtied, you need to call window()->redraw()
if (event == Ion::Events::USBPlug) {
if (Ion::USB::isPlugged()) {
// If the exam mode is enabled, we ask to disable it, else, we enable USB
if (GlobalPreferences::sharedGlobalPreferences()->isInExamMode()) {
displayExamModePopUp(GlobalPreferences::ExamMode::Off);
window()->redraw();
} else {
Ion::USB::enable();
}
// Update brightness when USB is plugged
Ion::Backlight::setBrightness(GlobalPreferences::sharedGlobalPreferences()->brightnessLevel());
} else {
// If the USB isn't plugged in USBPlug event, we disable USB
Ion::USB::disable();
}
return true;
}
// If key home or key back is pressed, we switch to the home app.
if (event == Ion::Events::Home || event == Ion::Events::Back) {
switchTo(appSnapshotAtIndex(0));
return true;
}
// If shift + Home are pressed, we switch to the first app.
if (event == Ion::Events::ShiftHome) {
switchTo(appSnapshotAtIndex(1));
return true;
}
// Iterate through the switch events to find the one that matches the event, if one match, switch to the app at the index of the switch event.
for(int i = 0; i < std::min((int) (sizeof(switch_events) / sizeof(Ion::Events::Event)), APPS_CONTAINER_SNAPSHOT_COUNT); i++) {
if (event == switch_events[i]) {
m_window.redraw(true);
switchTo(appSnapshotAtIndex(i+1));
return true;
}
}
// Add EE shortcut to go to the settings (app number 12)
if (event == Ion::Events::EE) {
switchTo(appSnapshotAtIndex(12));
return true;
}
// Add Shift + Ans shortcut to go to the previous app
if (event == Ion::Events::ShiftAns) {
switchTo(appSnapshotAtIndex(m_lastAppIndex));
return true;
}
// If the event is the OnOff key, we suspend the calculator.
if (event == Ion::Events::OnOff) {
suspend(true);
return true;
}
// If the event is a brightness event, we update the brightness according to the event.
if (event == Ion::Events::BrightnessPlus || event == Ion::Events::BrightnessMinus) {
int delta = Ion::Backlight::MaxBrightness/GlobalPreferences::NumberOfBrightnessStates;
int NumberOfStepsPerShortcut = GlobalPreferences::sharedGlobalPreferences()->brightnessShortcut();
int direction = (event == Ion::Events::BrightnessPlus) ? NumberOfStepsPerShortcut*delta : -delta*NumberOfStepsPerShortcut;
GlobalPreferences::sharedGlobalPreferences()->setBrightnessLevel(GlobalPreferences::sharedGlobalPreferences()->brightnessLevel()+direction);
}
// Else, the event was not processed.
return false;
}
bool AppsContainer::switchTo(App::Snapshot * snapshot) {
// Get app index of the snapshot
int m_appIndexToSwitch = appIndexFromSnapshot(snapshot);
// If the app is home, skip app index saving
if (m_appIndexToSwitch != 0) {
// Save last app index
m_lastAppIndex = m_currentAppIndex;
// Save current app index
m_currentAppIndex = m_appIndexToSwitch;
}
if (s_activeApp && snapshot != s_activeApp->snapshot()) {
resetShiftAlphaStatus();
}
if (snapshot == hardwareTestAppSnapshot() || snapshot == onBoardingAppSnapshot()) {
m_window.hideTitleBarView(true);
} else {
m_window.hideTitleBarView(false);
}
if (snapshot) {
m_window.setTitle(snapshot->descriptor()->upperName());
}
return Container::switchTo(snapshot);
}
void AppsContainer::run() {
KDRect screenRect = KDRect(0, 0, Ion::Display::Width, Ion::Display::Height);
window()->setFrame(screenRect, false);
/* We push a white screen here, because fetching the exam mode takes some time
* and it is visible when reflashing a N0100 (there is some noise on the
* screen before the logo appears). */
Ion::Display::pushRectUniform(screenRect, KDColorWhite);
if (GlobalPreferences::sharedGlobalPreferences()->isInExamMode()) {
activateExamMode(GlobalPreferences::sharedGlobalPreferences()->examMode());
}
refreshPreferences();
/* ExceptionCheckpoint stores the value of the stack pointer when setjump is
* called. During a longjump, the stack pointer is set to this stored stack
* pointer value, so the method where we call setjump must remain in the call
* tree for the jump to work. */
Poincare::ExceptionCheckpoint ecp;
if (ExceptionRun(ecp)) {
/* Normal execution. The exception checkpoint must be created before
* switching to the first app, because the first app might create nodes on
* the pool. */
bool switched;
if (m_startAppSnapshot != nullptr) {
switched = switchTo(m_startAppSnapshot);
} else {
switched = switchTo(initialAppSnapshot());
}
assert(switched);
(void) switched; // Silence compilation warning about unused variable.
} else {
// Exception
if (s_activeApp != nullptr) {
/* The app models can reference layouts or expressions that have been
* destroyed from the pool. To avoid using them before packing the app
* (in App::willBecomeInactive for instance), we tidy them early on. */
s_activeApp->snapshot()->tidy();
/* When an app encountered an exception due to a full pool, the next time
* the user enters the app, the same exception could happen again which
* would prevent from reopening the app. To avoid being stuck outside the
* app causing the issue, we reset its snapshot when leaving it due to
* exception. For instance, the calculation app can encounter an
* exception when displaying too many huge layouts, if we don't clean the
* history here, we will be stuck outside the calculation app. */
s_activeApp->snapshot()->reset();
}
bool switched = switchTo(appSnapshotAtIndex(0));
assert(switched);
(void) switched; // Silence compilation warning about unused variable.
Poincare::Tidy();
s_activeApp->displayWarning(I18n::Message::PoolMemoryFull1, I18n::Message::PoolMemoryFull2, true);
}
Container::run();
switchTo(nullptr);
}
bool AppsContainer::updateClock() {
return m_window.updateClock();
}
bool AppsContainer::updateBatteryState() {
bool batteryLevelUpdated = m_window.updateBatteryLevel();
bool pluggedStateUpdated = m_window.updatePluggedState();
bool chargingStateUpdated = m_window.updateIsChargingState();
if (batteryLevelUpdated || pluggedStateUpdated || chargingStateUpdated) {
return true;
}
return false;
}
void AppsContainer::refreshPreferences() {
m_suspendTimer.reset(GlobalPreferences::sharedGlobalPreferences()->idleBeforeSuspendSeconds()*1000/Timer::TickDuration);
m_backlightDimmingTimer.reset(GlobalPreferences::sharedGlobalPreferences()->idleBeforeDimmingSeconds()*1000/Timer::TickDuration);
m_window.refreshPreferences();
}
void AppsContainer::reloadTitleBarView() {
m_window.reloadTitleBarView();
}
void AppsContainer::displayExamModePopUp(GlobalPreferences::ExamMode mode) {
m_examPopUpController.setTargetExamMode(mode);
s_activeApp->displayModalViewController(&m_examPopUpController, 0.f, 0.f, Metric::ExamPopUpTopMargin, Metric::PopUpRightMargin, Metric::ExamPopUpBottomMargin, Metric::PopUpLeftMargin);
}
void AppsContainer::shutdownDueToLowBattery() {
if (Ion::Battery::level() != Ion::Battery::Charge::EMPTY) {
/* We early escape here. When the battery switches from LOW to EMPTY, it
* oscillates a few times before stabilizing to EMPTY. So we might call
* 'shutdownDueToLowBattery' but the battery level still answers LOW instead
* of EMPTY. We want to avoid uselessly redrawing the whole window in that
* case. */
return;
}
while (Ion::Battery::level() == Ion::Battery::Charge::EMPTY && !Ion::USB::isPlugged()) {
Ion::Backlight::setBrightness(0);
if (!GlobalPreferences::sharedGlobalPreferences()->isInExamMode()) {
/* Unless the LED is lit up for the exam mode, switch off the LED. IF the
* low battery event happened during the Power-On Self-Test, a LED might
* have stayed lit up. */
Ion::LED::setColor(KDColorBlack);
}
m_emptyBatteryWindow.redraw(true);
Ion::Timing::msleep(3000);
Ion::Power::suspend();
}
window()->redraw(true);
}
void AppsContainer::setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus newStatus) {
Ion::Events::setShiftAlphaStatus(newStatus);
updateAlphaLock();
}
bool AppsContainer::updateAlphaLock() {
return m_window.updateAlphaLock();
}
OnBoarding::PromptController * AppsContainer::promptController() {
if (k_promptNumberOfMessages == 0) {
return nullptr;
}
return &m_promptController;
}
void AppsContainer::redrawWindow(bool force) {
m_window.redraw(force);
}
void AppsContainer::activateExamMode(GlobalPreferences::ExamMode examMode) {
assert(examMode != GlobalPreferences::ExamMode::Off && examMode != GlobalPreferences::ExamMode::Unknown);
reset();
Ion::LED::setColor(KDColorRed);
/* The Dutch exam mode LED is supposed to be orange but we can only make
* blink "pure" colors: with RGB leds on or off (as the PWM is used for
* blinking). The closest "pure" color is Yellow. Moreover, Orange LED is
* already used when the battery is charging. Using yellow, we can assert
* that the yellow LED only means that Dutch exam mode is on and avoid
* confusing states when the battery is charging and states when the Dutch
* exam mode is on. */
// Ion::LED::setColor(examMode == GlobalPreferences::ExamMode::Dutch ? KDColorYellow : KDColorRed);
Ion::LED::setBlinking(1000, 0.1f);
}
void AppsContainer::examDeactivatingPopUpIsDismissed() {
if (Ion::USB::isPlugged()) {
Ion::USB::enable();
}
}
void AppsContainer::storageDidChangeForRecord(const Ion::Storage::Record record) {
if (s_activeApp) {
s_activeApp->snapshot()->storageDidChangeForRecord(record);
}
}
void AppsContainer::storageIsFull() {
if (s_activeApp) {
s_activeApp->displayWarning(I18n::Message::StorageMemoryFull1, I18n::Message::StorageMemoryFull2, true);
}
}
Window * AppsContainer::window() {
return &m_window;
}
void AppsContainer::resetShiftAlphaStatus() {
Ion::Events::setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::Default);
updateAlphaLock();
}