diff --git a/apps.cpp b/apps.cpp index e22257d..2c8eeb8 100644 --- a/apps.cpp +++ b/apps.cpp @@ -1,8 +1,14 @@ #include "apps.h" #include "ui_apps.h" #include "mainwindow.h" +#include "userapps.h" + #include #include +#include +#include +#include +#include apps::apps(QWidget *parent) : QWidget(parent), @@ -29,6 +35,8 @@ apps::apps(QWidget *parent) : ui->reversiLaunchBtn->setStyleSheet("background: lightGrey; font-size: 9pt; padding: 8px"); ui->g2048LaunchBtn->setStyleSheet("background: lightGrey; font-size: 9pt; padding: 8px"); + ui->noUserAppsAvailableLabel->hide(); + // Hiding KoBox apps button and label if X11 isn't enabled/wasn't started if(checkconfig("/external_root/boot/flags/X11_START") == false or checkconfig("/external_root/boot/flags/X11_STARTED") == false) { ui->label_5->hide(); @@ -61,6 +69,15 @@ apps::apps(QWidget *parent) : ui->lightmapsLaunchBtn->deleteLater(); } + ui->editUserAppsBtn->setProperty("type", "borderless"); + ui->editUserAppsBtn->setIcon(QIcon(":/resources/edit.png")); + + // Refresh because Qt shows the scrollbar for one second then hides it, leaving a mark on the eInk screen + QTimer::singleShot(1750, this, SLOT(refreshScreenNative())); + + jsonParseSuccess = parseJson(); + showUserApps(false); + QFile stylesheetFile("/mnt/onboard/.adds/inkbox/eink.qss"); stylesheetFile.open(QFile::ReadOnly); this->setStyleSheet(stylesheetFile.readAll()); @@ -160,3 +177,216 @@ void apps::on_g2048LaunchBtn_clicked() void apps::showToastNative(QString messageToDisplay) { emit showToast(messageToDisplay); } + +/* + * This function opens and reads the main GUI user applications' JSON file, parses and verifies its content to avoid issues in the future. +*/ +bool apps::parseJson() { + // If the path changes, it is also used statically in showUserApps() + jsonFile.setFileName("/mnt/onboard/onboard/.apps/apps.json"); + bool jsonCheckSuccess = true; + + if(jsonFile.exists() == false) { + log("GUI user applications' main JSON file is missing", className); + jsonCheckSuccess = false; + } + else { + jsonFile.open(QIODevice::ReadOnly | QIODevice::Text); + QString fileRead = jsonFile.readAll(); + jsonFile.close(); + + jsonDocument = QJsonDocument::fromJson(fileRead.toUtf8()); + if(jsonDocument.isNull() == true) { + log("GUI user applications' main JSON file is invalid", className); + jsonCheckSuccess = false; + } + else { + QJsonObject jsonObject = jsonDocument.object(); + if(jsonObject["list"].isArray() == true) { + QJsonArray jsonArray = jsonObject["list"].toArray(); + for(QJsonValueRef refJsonObject: jsonArray) { + if(refJsonObject.isObject()) { + QJsonObject jsonMainObject = refJsonObject.toObject(); + if(jsonMainObject.size() == 9) { + if(!jsonMainObject["Name"].isString()) { + QString function = __func__; log(function + ": Invalid 'Name' type inside object", className); + jsonCheckSuccess = false; + + } + if(!jsonMainObject["Author"].isString()) { + QString function = __func__; log(function + ": Invalid 'Author' type inside object", className); + jsonCheckSuccess = false; + + } + if(!jsonMainObject["AuthorContact"].isString()) { + QString function = __func__; log(function + ": Invalid 'AuthorContact' type inside object", className); + jsonCheckSuccess = false; + + } + if(!jsonMainObject["Version"].isString()) { + QString function = __func__; log(function + ": Invalid 'Version' type inside object", className); + jsonCheckSuccess = false; + + } + if(!jsonMainObject["IconPath"].isString()) { + log("JSON: Invalid IconPath type inside object", className); + jsonCheckSuccess = false; + + } + if(!jsonMainObject["ExecPath"].isString()) { + QString function = __func__; log(function + ": Invalid 'ExecPath' type inside object", className); + jsonCheckSuccess = false; + } + else { + if(jsonMainObject["ExecPath"].toString().contains("..")) { + // Possible security risk + showToastNative("ERROR: 'ExecPath' has invalid path"); + QString function = __func__; log(function + ": 'ExecPath' contains \"..\"", className); + jsonCheckSuccess = false; + } + } + if(!jsonMainObject["Enabled"].isBool()) { + QString function = __func__; log(function + ": Invalid 'Enabled' type inside object", className); + jsonCheckSuccess = false; + + } + if(!jsonMainObject["SupportedDevices"].isArray()) { + QString function = __func__; log(function + ": Invalid 'SupportedDevices' type inside object", className); + jsonCheckSuccess = false; + + } + else { + QJsonArray jsonArray = jsonMainObject["SupportedDevices"].toArray(); + for(QJsonValueRef refJsonObject: jsonArray) { + // https://doc.qt.io/qt-5/qjsonvalue.html#toInt + if(!refJsonObject.isString()) { + QString function = __func__; log(function + ": Array from 'RequiredFeatures' contains a wrong type", className); + jsonCheckSuccess = false; + } + } + } + if(!jsonMainObject["RequiredFeatures"].isArray()) { + QString function = __func__; log(function + ": Invalid 'RequiredFeatures' type inside object", className); + jsonCheckSuccess = false; + + } + else { + QJsonArray jsonArray = jsonMainObject["RequiredFeatures"].toArray(); + for(QJsonValueRef refJsonObject: jsonArray) { + // https://doc.qt.io/qt-5/qjsonvalue.html#toInt + if(!refJsonObject.isDouble()) { + QString function = __func__; log(function + ": Array from 'RequiredFeatures' contains a wrong type", className); + jsonCheckSuccess = false; + } + } + } + } + else { + QString function = __func__; QString message = function + ": An object inside list array has too many items (or not enough): "; + message.append(QString::number(jsonMainObject.size())); + log(message, className); + jsonCheckSuccess = false; + } + } + else { + QString function = __func__; log(function + ": List array has an item of other type than object", className); + jsonCheckSuccess = false; + } + } + } + } + } + return jsonCheckSuccess; +} + +void apps::on_editUserAppsBtn_clicked() +{ + if(userAppsSecondPage == false) { + userAppsSecondPage = true; + + // Settings page + ui->editUserAppsBtn->setIcon(QIcon(":/resources/save.png")); + showUserApps(userAppsSecondPage); + emit showUserAppsEdit(userAppsSecondPage); + + QTimer::singleShot(500, this, SLOT(refreshScreenNative())); + } + else { + userAppsSecondPage = false; + userAppsAvailable = false; + + // Launch page + ui->editUserAppsBtn->setIcon(QIcon(":/resources/edit.png")); + // It changed via updateJsonFileSlot, and now it writes it only once + jsonFile.open(QFile::WriteOnly | QFile::Text | QFile::Truncate); + jsonFile.write(jsonDocument.toJson()); + jsonFile.flush(); + jsonFile.close(); + + showUserApps(userAppsSecondPage); + emit showUserAppsEdit(userAppsSecondPage); + + QTimer::singleShot(500, this, SLOT(refreshScreenNative())); + } +} + +void apps::showUserApps(bool showDisabledJson) +{ + emit clearAppsLayout(); + + if(jsonParseSuccess == true) { + QString function = __func__; log(function + ": Main user applications' JSON is valid", className); + QJsonArray jsonListArray = jsonDocument.object()["list"].toArray(); + for(QJsonValueRef refJsonObject: jsonListArray) { + QJsonObject appInfo = refJsonObject.toObject(); + if(appInfo["Enabled"].toBool() == true or showDisabledJson == true) { + userAppsAvailable = true; + userapps * newUserApp = new userapps; + newUserApp->provideInfo(appInfo); + connect(this, SIGNAL(clearAppsLayout()), newUserApp, SLOT(deleteLater())); + connect(this, SIGNAL(showUserAppsEdit(bool)), newUserApp, SLOT(changePageEnabling(bool))); + connect(this, SIGNAL(updateJsonFileSignal(QJsonDocument)), newUserApp, SLOT(updateJsonFileSlotUA(QJsonDocument))); + connect(newUserApp, SIGNAL(updateJsonFileSignalUA(QJsonDocument)), this, SLOT(updateJsonFileSlot(QJsonDocument))); + newUserApp->jsonDocument = jsonDocument; + newUserApp->jsonFilePath = jsonFile.fileName(); + ui->verticalLayout_4->addWidget(newUserApp); + } + } + + if(userAppsAvailable == false) { + ui->noUserAppsAvailableLabel->show(); + } + else { + ui->noUserAppsAvailableLabel->hide(); + } + } + else { + if(QFile::exists("/mnt/onboard/onboard/.apps/apps.json")) { + QString function = __func__; log(function + ": Main user applications' JSON file is invalid", className); + QTimer::singleShot(500, this, SLOT(showFailedToParseMainUserAppsJsonFile())); + } + else { + QString function = __func__; log(function + ": Main user applications' JSON file doesn't exist; assuming that no user applications are currently installed", className); + } + + ui->editUserAppsBtn->hide(); + ui->label_6->hide(); + ui->line_2->hide(); + ui->line_3->hide(); + ui->editUserAppsBtn->hide(); + ui->label_6->deleteLater(); + ui->horizontalLayout->deleteLater(); + ui->line_2->deleteLater(); + ui->line_3->deleteLater(); + } +} + +void apps::updateJsonFileSlot(QJsonDocument jsonDocumentFunc) +{ + jsonDocument = jsonDocumentFunc; + emit updateJsonFileSignal(jsonDocument); +} + +void apps::showFailedToParseMainUserAppsJsonFile() { + emit showToast("Failed to parse 'apps.json'"); +} diff --git a/apps.h b/apps.h index 44ed62a..b208d77 100644 --- a/apps.h +++ b/apps.h @@ -7,6 +7,8 @@ #include #include +#include + namespace Ui { class apps; } @@ -30,9 +32,14 @@ private slots: void on_koboxAppsOpenButton_clicked(); void on_vncLaunchBtn_clicked(); void on_reversiLaunchBtn_clicked(); + void on_editUserAppsBtn_clicked(); void refreshScreenNative(); void on_g2048LaunchBtn_clicked(); void showToastNative(QString messageToDisplay); + bool parseJson(); + void showUserApps(bool showDisabledJson); + void updateJsonFileSlot(QJsonDocument jsonDocument); + void showFailedToParseMainUserAppsJsonFile(); private: Ui::apps *ui; @@ -41,9 +48,18 @@ private: koboxAppsDialog *koboxAppsDialogWindow; generalDialog *generalDialogWindow; + QFile jsonFile; + QJsonDocument jsonDocument; + bool jsonParseSuccess = false; + bool userAppsSecondPage = false; + bool userAppsAvailable = false; + signals: void refreshScreen(); void showToast(QString messageToDisplay); + void updateJsonFileSignal(QJsonDocument jsonDocument); + void clearAppsLayout(); + void showUserAppsEdit(bool show); }; #endif // APPS_H diff --git a/apps.ui b/apps.ui index 6af18ea..e46429d 100644 --- a/apps.ui +++ b/apps.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 321 + 430 + 544 @@ -28,12 +28,469 @@ - - - + + 0 + + + + + QFrame::NoFrame + + + QFrame::Plain + + 0 - + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + -30 + 407 + 541 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 8 + + + 0 + + + 15 + + + 10 + + + + + + 75 + true + + + + KoBox apps + + + + + + + + 75 + true + + + + VNC viewer + + + + + + + + Inter + 75 + false + true + + + + Launch + + + + + + + + 75 + true + + + + Saved words + + + + + + + + 75 + true + + + + Scribble + + + + + + + + 75 + true + + + + Calculator + + + + + + + + Inter + 75 + true + + + + Launch + + + + + + + + Inter + 75 + true + + + + Open + + + + + + + + 75 + true + + + + Light Maps + + + + + + + + 75 + true + + + + Reversi + + + + + + + + 75 + true + + + + Launch + + + + + + + + Inter + 75 + true + + + + Launch + + + + + + + + Inter + 75 + true + + + + Launch + + + + + + + + 75 + true + + + + 2048 + + + + + + + + 75 + true + + + + Launch + + + + + + + + Inter + 75 + true + + + + Launch + + + + + + + + Inter + 75 + false + true + + + + Launch + + + + + + + + 75 + true + + + + Calendar + + + + + + + + + QFrame::Plain + + + 3 + + + Qt::Horizontal + + + + + + + 0 + + + 5 + + + 5 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 10 + + + 10 + + + + + + 75 + true + + + + User apps + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 0 + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QFrame::Plain + + + 3 + + + Qt::Horizontal + + + + + + + 12 + + + + + + + + + + + Chivo + + + + +No user apps currently available + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 0 + 0 + + + + + + + @@ -44,276 +501,13 @@ - Apps + Built-in apps Qt::AlignCenter - - - - 0 - - - - - - Inter - 75 - true - - - - Launch - - - - - - - - Inter - 75 - true - - - - Launch - - - - - - - - 75 - true - - - - Light Maps - - - - - - - - 75 - true - - - - Scribble - - - - - - - - Inter - 75 - true - - - - Launch - - - - - - - - 75 - true - - - - Reversi - - - - - - - - 75 - true - - - - Saved words - - - - - - - - 75 - true - - - - KoBox apps - - - - - - - - 75 - true - - - - Calendar - - - - - - - - Inter - 75 - false - true - - - - Launch - - - - - - - - 75 - true - - - - Launch - - - - - - - - Inter - 75 - true - - - - Launch - - - - - - - - Inter - 75 - true - - - - Open - - - - - - - - 75 - true - - - - Calculator - - - - - - - - 75 - true - - - - VNC viewer - - - - - - - - Inter - 75 - false - true - - - - Launch - - - - - - - - 75 - true - - - - Launch - - - - - - - - 75 - true - - - - 2048 - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/eink.qrc b/eink.qrc index 0cf1bd9..ca60ca3 100644 --- a/eink.qrc +++ b/eink.qrc @@ -74,5 +74,7 @@ resources/clock.png resources/eink-square-encfs.qss resources/tzlist + resources/edit.png + resources/save.png diff --git a/functions.h b/functions.h index d2934b2..7ccbfb9 100644 --- a/functions.h +++ b/functions.h @@ -13,6 +13,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include @@ -122,6 +128,13 @@ namespace global { namespace logger { inline bool status; } + namespace userApps { + inline bool appCompatibilityDialog; + inline QString appCompatibilityText; + inline bool appCompatibilityLastContinueStatus = true; // This is for RequiredFeatures to show only one dialog if 'Cancel' is clicked. + inline bool appInfoDialog; + inline bool launchApp; + } inline QString systemInfoText; inline bool forbidOpenSearchDialog; inline bool isN705 = false; @@ -846,6 +859,90 @@ namespace { return false; } } + void updateUserAppsMainJsonFile() { + QDirIterator appsDir("/mnt/onboard/onboard/.apps", QDirIterator::NoIteratorFlags); + QFile newJsonFile = QFile{"/mnt/onboard/onboard/.apps/apps.json"}; + QJsonDocument newJsonDocument; + QJsonArray array; + + while (appsDir.hasNext()) + { + QDir dir(appsDir.next()); + if(dir.exists() == true) { + if(dir.path().split("/").last().contains(".") == false) { + QFile jsonSmall = QFile{dir.path() + "/app.json"}; + if(jsonSmall.exists() == true) { + jsonSmall.open(QIODevice::ReadOnly | QIODevice::Text); + QString jsonString = jsonSmall.readAll(); + jsonSmall.close(); + + QJsonDocument jsonSmallDoc = QJsonDocument::fromJson(jsonString.toUtf8()); + if(jsonSmallDoc["app"].isObject() == true) { + QJsonObject jsonSmallMainObj = jsonSmallDoc["app"].toObject(); + array.append(jsonSmallMainObj); + + } + else { + log("Error: User application '" + appsDir.path() + "''s JSON file descriptor is missing main object 'app'", "main"); + } + + } + else { + QString message = "User application '" + appsDir.path() + "' does not contain any 'app.json' file: "; + message.append(jsonSmall.fileName()); + log(message, "main"); + } + } + } + } + // https://forum.qt.io/topic/104791/how-i-can-create-json-format-in-qt/5 + QJsonObject root; + root["list"] = array; + newJsonDocument.setObject(root); + + newJsonFile.open(QFile::WriteOnly | QFile::Text | QFile::Truncate); + newJsonFile.write(newJsonDocument.toJson()); + newJsonFile.flush(); + newJsonFile.close(); + } + void updateUserAppsSmallJsonFiles() { + QFile jsonFile = QFile{"/mnt/onboard/onboard/.apps/apps.json"}; + + jsonFile.open(QIODevice::ReadOnly | QIODevice::Text); + QString fileRead = jsonFile.readAll(); + jsonFile.close(); + QJsonDocument jsonDocument = QJsonDocument::fromJson(fileRead.toUtf8()); + + if(jsonDocument["list"].isArray() == true) { + QJsonArray jsonArray = jsonDocument["list"].toArray(); + for(QJsonValueRef refJsonObject: jsonArray) { + QJsonObject jsonMainObject = refJsonObject.toObject(); + QString appName = jsonMainObject["Name"].toString(); + + // This needs to be here and not at the beggining of this function because it is an iterator + QDirIterator appsDir("/mnt/onboard/onboard/.apps", QDirIterator::NoIteratorFlags); + while (appsDir.hasNext()) { + QDir dir(appsDir.next()); + if(dir.exists() == true) { + if(dir.path().split("/").last().toLower().contains(appName.toLower()) == true) { + QJsonObject root; + root["app"] = refJsonObject.toObject();; + QJsonDocument newJsonDocument; + newJsonDocument.setObject(root); + + QFile newSmallJson = QFile{dir.path() + "/" + "app.json"}; + + newSmallJson.open(QIODevice::ReadWrite); + QTextStream stream(&newSmallJson); + stream << newJsonDocument.toJson() << Qt::endl; + newSmallJson.flush(); + newSmallJson.close(); + } + } + } + } + } + } } #endif // FUNCTIONS_H diff --git a/generaldialog.cpp b/generaldialog.cpp index 3d86b44..dc6a504 100644 --- a/generaldialog.cpp +++ b/generaldialog.cpp @@ -13,6 +13,7 @@ #include #include #include +#include generalDialog::generalDialog(QWidget *parent) : QDialog(parent), @@ -131,6 +132,7 @@ generalDialog::generalDialog(QWidget *parent) : } else if(global::usbms::usbmsDialog == true) { usbmsDialog = true; + ui->stackedWidget->setCurrentIndex(0); ui->okBtn->setText("Connect"); ui->cancelBtn->setText("Not now"); ui->bodyLabel->setText("Do you want to connect your device to a computer to manage books?"); @@ -187,9 +189,29 @@ generalDialog::generalDialog(QWidget *parent) : ui->bodyLabel->setText("New files have been found in 'encfs-dropbox'. Would you want to repack your encrypted storage?"); QTimer::singleShot(50, this, SLOT(adjust_size())); } + else if(global::userApps::appCompatibilityDialog == true) { + appCompatibilityDialog = true; + global::userApps::appCompatibilityLastContinueStatus = true; + ui->okBtn->setText("Continue"); + ui->cancelBtn->setText("Cancel"); + ui->bodyLabel->setText(global::userApps::appCompatibilityText); + ui->headerLabel->setText("Compatibility warning"); + QTimer::singleShot(50, this, SLOT(adjust_size())); + } + else if(global::userApps::appInfoDialog == true) { + appInfoDialog = true; + textwidgetWindow = new textwidget(); + + ui->headerLabel->setText("App info"); + ui->stackedWidget->setCurrentIndex(1); + ui->mainStackedWidget->insertWidget(1, textwidgetWindow); + ui->mainStackedWidget->setCurrentIndex(1); + yIncrease = 1.8; + QTimer::singleShot(50, this, SLOT(increaseSize())); + } else { // We shouldn't be there ;) - ; + log("Launched without settings", className); } // Centering dialog @@ -248,6 +270,12 @@ void generalDialog::on_cancelBtn_clicked() else if(global::encfs::repackDialog == true) { global::encfs::repackDialog = false; } + else if(global::userApps::appCompatibilityDialog == true) { + global::userApps::launchApp = false; + global::userApps::appCompatibilityLastContinueStatus = false; + global::userApps::appCompatibilityText = ""; + global::userApps::appCompatibilityDialog = false; + } generalDialog::close(); } } @@ -507,6 +535,13 @@ void generalDialog::on_okBtn_clicked() string_writeconfig("/external_root/run/encfs_repack", "true"); quit_restart(); } + else if(global::userApps::appCompatibilityDialog == true) { + global::userApps::launchApp = true; + global::userApps::appCompatibilityText = ""; + global::userApps::appCompatibilityLastContinueStatus = true; // Not really necceserry, only if something fails horibly + global::userApps::appCompatibilityDialog = false; + generalDialog::close(); + } } void generalDialog::on_acceptBtn_clicked() { @@ -533,6 +568,11 @@ void generalDialog::on_acceptBtn_clicked() global::text::textBrowserDialog = false; } + if(appInfoDialog == true) { + global::text::textBrowserContents = ""; + global::userApps::appInfoDialog = false; + } + // We don't have any other option ;p generalDialog::close(); } @@ -552,6 +592,7 @@ void generalDialog::restartSearchDialog() { } void generalDialog::setupKeyboardDialog() { + ui->stackedWidget->setCurrentIndex(0); keyboardDialog = true; ui->stackedWidget->setVisible(true); if(global::keyboard::searchDialog == true) { @@ -732,3 +773,25 @@ void generalDialog::waitForGutenbergSearchDone() { } } } + +void generalDialog::increaseSize() +{ + log("Resizing generalDialog", className); + + ui->topStackedWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + ui->mainStackedWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); + ui->stackedWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + QRect screenGeometry = QGuiApplication::screens()[0]->geometry(); + int wx = screenGeometry.width(); + + int x = wx - 25; + int y = this->height() * yIncrease; + this->setFixedWidth(x); + this->setFixedHeight(y); + + ui->bodyLabel->sizePolicy().setVerticalPolicy(QSizePolicy::Expanding); + ui->bodyLabel->sizePolicy().setHorizontalPolicy(QSizePolicy::Expanding); + + adjust_size(); +} diff --git a/generaldialog.h b/generaldialog.h index bb9bd24..08d1001 100644 --- a/generaldialog.h +++ b/generaldialog.h @@ -32,6 +32,8 @@ public: bool lowBatteryDialog = false; bool usbmsDialog = false; bool textBrowserDialog = false; + bool appCompatibilityDialog = false; + bool appInfoDialog = false; bool resetKoboxDialog = false; bool keyboardDialog = false; bool keypadDialog = false; @@ -49,6 +51,10 @@ public: QString wifiPassphrase; void setupKeyboardDialog(); void startVNC(QString server, QString password, QString port); + float yIncrease; + +public slots: + void increaseSize(); private slots: void on_cancelBtn_clicked(); diff --git a/inkbox.pro b/inkbox.pro index 000f92d..f41890e 100644 --- a/inkbox.pro +++ b/inkbox.pro @@ -48,6 +48,7 @@ SOURCES += \ textwidget.cpp \ toast.cpp \ usbms_splash.cpp \ + userapps.cpp \ virtualkeyboard.cpp \ virtualkeypad.cpp \ wifidialog.cpp @@ -77,6 +78,7 @@ HEADERS += \ textwidget.h \ toast.h \ usbms_splash.h \ + userapps.h \ virtualkeyboard.h \ virtualkeypad.h \ wifidialog.h @@ -105,6 +107,7 @@ FORMS += \ textwidget.ui \ toast.ui \ usbms_splash.ui \ + userapps.ui \ virtualkeyboard.ui \ virtualkeypad.ui \ wifidialog.ui diff --git a/main.cpp b/main.cpp index fa64d7f..9402c7c 100644 --- a/main.cpp +++ b/main.cpp @@ -38,6 +38,9 @@ int main(int argc, char *argv[]) global::deviceID = readFile("/opt/inkbox_device"); log("Running on device " + global::deviceID, "main", true); + // Tell the OS that we're currently running + string_writeconfig("/tmp/inkbox_running", "true"); + setDefaultWorkDir(); if(checkconfig("/run/wifi_able") == true) { log("Device has Wi-Fi capabilities", "main"); @@ -47,6 +50,31 @@ int main(int argc, char *argv[]) log("Device does not have Wi-Fi capabilities", "main"); global::device::isWifiAble = false; } + + if(QFile::exists("/tmp/rescan_userapps")) { + QFile::remove("/tmp/rescan_userapps"); + log("Re-scanning user applications from explicit request", "main"); + string_writeconfig("/opt/ibxd", "gui_apps_stop\n"); + QThread::msleep(1000); + string_writeconfig("/opt/ibxd", "gui_apps_start\n"); + while(true) { + if(QFile::exists("/tmp/gui_apps_started")) { + if(checkconfig("/tmp/gui_apps_started") == true) { + log("GUI apps service started successfully", "main"); + QFile::remove("/tmp/gui_apps_started"); + updateUserAppsMainJsonFile(); + break; + } + else { + log("GUI apps service failed to start", "main"); + QFile::remove("/tmp/gui_apps_started"); + break; + } + } + } + updateUserAppsMainJsonFile(); + } + if(checkconfig(".config/18-encrypted_storage/status") == true and checkconfig("/external_root/run/encfs_mounted") == false) { // Open Encryption Manager to unlock encrypted storage QApplication a(argc, argv); @@ -66,9 +94,6 @@ int main(int argc, char *argv[]) return a.exec(); } else { - // Tell scripts that we're currently running - string_writeconfig("/tmp/inkbox_running", "true"); - // Variables global::reader::startBatteryWatchdog = false; global::reader::startUsbmsPrompt = false; diff --git a/quit.cpp b/quit.cpp index 1114929..63b242f 100644 --- a/quit.cpp +++ b/quit.cpp @@ -51,6 +51,9 @@ void quit::on_pushButton_clicked() global::battery::showCriticalBatteryAlert = false; global::battery::showLowBatteryDialog = false; + // GUI apps + updateUserAppsMainJsonFile(); + poweroff(true); qApp->quit(); } @@ -60,6 +63,9 @@ void quit::on_pushButton_2_clicked() global::battery::showCriticalBatteryAlert = false; global::battery::showLowBatteryDialog = false; + // GUI apps + updateUserAppsMainJsonFile(); + reboot(true); qApp->quit(); } diff --git a/resources/edit.png b/resources/edit.png new file mode 100644 index 0000000..a0fe6c5 Binary files /dev/null and b/resources/edit.png differ diff --git a/resources/save.png b/resources/save.png new file mode 100644 index 0000000..7ea0cd1 Binary files /dev/null and b/resources/save.png differ diff --git a/settings.cpp b/settings.cpp index 5104761..21cb8a9 100644 --- a/settings.cpp +++ b/settings.cpp @@ -822,6 +822,10 @@ void settings::on_showSystemInfoBtn_clicked() // Show a system info dialog log("Showing system info dialog", className); generalDialogWindow = new generalDialog(); + if(global::deviceID == "n306\n") { + generalDialogWindow->yIncrease = 2.6; + } + generalDialogWindow->increaseSize(); generalDialogWindow->setAttribute(Qt::WA_DeleteOnClose); } diff --git a/usbms_splash.cpp b/usbms_splash.cpp index ea49d70..692e256 100644 --- a/usbms_splash.cpp +++ b/usbms_splash.cpp @@ -85,6 +85,9 @@ void usbms_splash::usbms_launch() string_writeconfig("/opt/ibxd", "usbnet_stop\n"); QThread::msleep(1000); + string_writeconfig("/opt/ibxd", "gui_apps_stop\n"); + QThread::msleep(1000); + if(global::deviceID == "n306\n" or global::deviceID == "n873\n") { QProcess::execute("insmod", QStringList() << "/external_root/lib/modules/fs/configfs/configfs.ko"); QProcess::execute("insmod", QStringList() << "/external_root/lib/modules/drivers/usb/gadget/libcomposite.ko"); @@ -197,6 +200,22 @@ void usbms_splash::restartServices() { string_writeconfig("/opt/ibxd", "update_inkbox_restart\n"); QThread::msleep(2500); string_writeconfig("/tmp/in_usbms", "false"); + // GUI apps: update main JSON file + string_writeconfig("/opt/ibxd", "gui_apps_start\n"); + while(true) { + if(QFile::exists("/tmp/gui_apps_started")) { + if(checkconfig("/tmp/gui_apps_started") == true) { + QFile::remove("/tmp/gui_apps_started"); + updateUserAppsMainJsonFile(); + break; + } + else { + log("GUI apps service failed to start", className); + QFile::remove("/tmp/gui_apps_started"); + break; + } + } + } quit_restart(); } diff --git a/userapps.cpp b/userapps.cpp new file mode 100644 index 0000000..37b40f8 --- /dev/null +++ b/userapps.cpp @@ -0,0 +1,274 @@ +#include "userapps.h" +#include "ui_userapps.h" +#include "mainwindow.h" +#include "generaldialog.h" + +userapps::userapps(QWidget *parent) : + QWidget(parent), + ui(new Ui::userapps) +{ + ui->setupUi(this); + ui->launchBtn->setProperty("type", "borderless"); + ui->launchBtn->setStyleSheet("background: lightGrey; font-size: 9pt; padding: 8px"); + + ui->launchBtn->setProperty("type", "borderless"); + + ui->iconBtn->setProperty("type", "borderless"); + ui->iconBtn->setStyleSheet("QPushButton[type='borderless']:pressed { background: white; color: white; border: none; }"); + + ui->infoBtn->setProperty("type", "borderless"); + ui->statusBtn->setProperty("type", "borderless"); + ui->statusBtn->setStyleSheet("background: lightGrey; font-size: 9pt; padding: 8px"); +} + +userapps::~userapps() +{ + delete ui; +} + +void userapps::provideInfo(QJsonObject jsonInfo) +{ + QString name = jsonInfo["Name"].toString(); + appName = name; // It is for searching for json entry while disabling / enabling + jsonObject = jsonInfo; + // Limit name size to avoid breaking the GUI + if(name.size() > 20) { + // If someone wants to break the GUI, they will do it ¯\^-^/¯ + name.remove(16, 100); + } + ui->appNameLabel->setText(name); + + appDir.setPath("/mnt/onboard/onboard/.apps/" + name + "/" + name); + + QFile iconPath = QFile{appDir.path() + "/" + jsonInfo["IconPath"].toString()}; + if(iconPath.exists() == true) { + QIcon appIcon = QIcon(iconPath.fileName()); + ui->iconBtn->setIconSize(QSize(40, 40)); + ui->iconBtn->setIcon(appIcon); + } + else { + QString function = __func__; + QString message = ": Warning: Icon not found: "; + message.append(iconPath.fileName()); + log(function + message, className); + ui->iconBtn->deleteLater(); + ui->gridLayout->deleteLater(); + } + + execPath.setFileName(jsonInfo["ExecPath"].toString()); + + userAppEnabled = jsonInfo["Enabled"].toBool(); + if(userAppEnabled == true) { + ui->statusBtn->setText("Disable"); + } + else { + ui->statusBtn->setText("Enable"); + } +} + +// This function is needed when we dont want to repaint all widgets, but only change the the page (when no changes to the main JSON file were applied) +void userapps::changePageEnabling(bool SecondPage) +{ + if(SecondPage == true){ + ui->stackedWidget->setCurrentIndex(1); + } + else { + ui->stackedWidget->setCurrentIndex(0); + } +} + +void userapps::on_infoBtn_clicked() +{ + log("Launching user application information dialog", className); + // https://stackoverflow.com/questions/28181627/how-to-convert-a-qjsonobject-to-qstring + QString jsonStringParsed = parseJsonShow(jsonObject); + global::text::textBrowserContents = jsonStringParsed; + + global::userApps::appInfoDialog = true; + generalDialogWindow = new generalDialog(); + generalDialogWindow->setAttribute(Qt::WA_DeleteOnClose); + generalDialogWindow->show(); +} + +void userapps::on_statusBtn_clicked() +{ + ui->statusBtn->setEnabled(false); + + // Here the text on this button is used as a boolean; no need to create a new one + // Disable and Enable + if(userAppEnabled == false) { + userAppEnabled = true; + ui->statusBtn->setText("Disable"); + } + else { + userAppEnabled = false; + ui->statusBtn->setText("Enable"); + } + + QJsonObject jsonRootObject = jsonDocument.object(); + QJsonArray jsonArrayList = jsonRootObject["list"].toArray(); + + int arraySize = jsonArrayList.size(); + + for(int i = 0; i < arraySize; i++) { + QJsonObject jsonObject = jsonArrayList.at(i).toObject(); + QString entryName = jsonObject["Name"].toString(); + + if(entryName == appName) { + jsonObject.insert("Enabled", QJsonValue(userAppEnabled)); + + QJsonArray sonArrayListNew = jsonDocument.object()["list"].toArray(); + sonArrayListNew.replace(i, jsonObject); + + jsonRootObject["list"] = sonArrayListNew; + + jsonDocument.setObject(jsonRootObject); + emit updateJsonFileSignalUA(jsonDocument); + } + } + ui->statusBtn->setEnabled(true); +} + +void userapps::updateJsonFileSlotUA(QJsonDocument jsonDocumentProvided) +{ + jsonDocument = jsonDocumentProvided; +} + +void userapps::on_launchBtn_clicked() +{ + // Some command to execute script or binary at "ExecPath" + QJsonArray supportedDevices = jsonObject["SupportedDevices"].toArray(); + + // This will work even if we are looking for 'n306' and there is a device named 'n306b' because QJsonArray::contains() works that way + if(supportedDevices.contains("all") == false and supportedDevices.contains(global::deviceID.trimmed()) == false) { + log("Warning: User application '" + appName + "' does not support this device", className); + global::userApps::appCompatibilityDialog = true; + global::userApps::appCompatibilityText = "Your device is not compatible with this app.
Continue anyway
?"; + generalDialogWindow = new generalDialog(); + generalDialogWindow->setAttribute(Qt::WA_DeleteOnClose); + + generalDialogWindow->exec(); + } + else { + global::userApps::launchApp = true; + } + + if(manageRequiredFeatures() == true) { + if(global::userApps::launchApp == true) { + global::userApps::launchApp = false; + QString message = "Launching user application at: "; + message.append("[" + appDir.path() + "]" + execPath.fileName()); + log(message, className); + // Tell the OS that we're not running anymore + string_writeconfig("/tmp/inkbox_running", "false"); + + QProcess process; + QStringList args; + args << appDir.path() << execPath.fileName(); + process.startDetached("launch_user_application.sh", args); + qApp->quit(); + } + } +} + +bool userapps::manageRequiredFeatures() +{ + // This should be already set to 'true', but just in case + global::userApps::appCompatibilityLastContinueStatus = true; + QJsonArray jsonArray = jsonObject["RequiredFeatures"].toArray(); + for(QJsonValueRef refJsonObject: jsonArray) { + bool launchDialog = false; + int featureId = refJsonObject.toInt(); + // Wi-Fi connection required + if(featureId == 0) { + // Double 'if' conditions to avoid launching unnecesery testPing() in emu + if(global::deviceID != "emu\n") { + if(testPing(true) != 0) { + global::userApps::appCompatibilityText = "This app needs a Wi-Fi connection, continue anyway?"; + launchDialog = true; + } + } + } + // Rooted kernel required + if(featureId == 1) { + if(checkconfig("/external_root/opt/root/rooted") == true) { + global::userApps::appCompatibilityText = "This app needs a rooted kernel, continue anyway?"; + launchDialog = true; + } + } + // Pseudoterminal support (ID: 2) is managed by the 'gui_apps' service (https://github.com/Kobo-InkBox/rootfs/blob/master/etc/init.d/gui_apps) + + if(launchDialog == true) { + global::userApps::appCompatibilityDialog = true; + generalDialogWindow = new generalDialog(); + generalDialogWindow->setAttribute(Qt::WA_DeleteOnClose); + generalDialogWindow->exec(); + } + + if(global::userApps::appCompatibilityLastContinueStatus == false) { + return false; + } + } + return true; +} + +QString userapps::parseJsonShow(QJsonObject json) +{ + QString mainString; + foreach(const QString& key, json.keys()) { + QJsonValue value = json.value(key); + + QString appendString; + + appendString.append(""); + appendString.append(key); + appendString.append(": "); + + if(value.isString()) { + appendString.append(value.toString()); + } + else if(value.isBool()) { + appendString.append(QVariant(value.toBool()).toString()); + } + else if(value.isArray()) { + QJsonArray array = value.toArray(); + if(key == "RequiredFeatures") { + bool foundRequiredFeature = false; + appendString.append("None"); + + for(QJsonValueRef ref: array) { + foundRequiredFeature = true; + appendString.remove(appendString.size() - 4, 4); + int id = ref.toInt(); + if(id == 0) { + appendString.append("Wi-Fi connection"); + } + else if(id == 1) { + appendString.append("Rooted kernel"); + } + else if(id == 2) { + appendString.append("Pseudoterminal support"); + } + appendString.append(", "); + } + + if(foundRequiredFeature == true) { + appendString.remove(appendString.size() - 2, 2); + } + } + else if(key == "SupportedDevices") { + for(QJsonValueRef ref: array) { + QString name = ref.toString(); + appendString.append(name); + appendString.append(", "); + } + appendString.remove(appendString.size() - 2, 2); + } + + } + appendString.append("
"); + mainString.append(appendString); + } + + return mainString; +} diff --git a/userapps.h b/userapps.h new file mode 100644 index 0000000..3063844 --- /dev/null +++ b/userapps.h @@ -0,0 +1,58 @@ +#ifndef USERAPPS_H +#define USERAPPS_H + +#include "generaldialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Ui { + class userapps; +} + +class userapps : public QWidget +{ + Q_OBJECT + +public: + QString className = this->metaObject()->className(); + explicit userapps(QWidget * parent = nullptr); + ~userapps(); + QJsonDocument jsonDocument; // TODO: Optimize for RAM usage + QJsonObject jsonObject; // Needed for 'App info' button + QString jsonFilePath; + +signals: + // This is needed, because user application have their own JSON file. If the user changes it too fast it won't read it and overwrite previous changes + void updateJsonFileSignalUA(QJsonDocument jsonDocument); + + +public slots: + void provideInfo(QJsonObject jsonObject); + void changePageEnabling(bool goThere); + void updateJsonFileSlotUA(QJsonDocument jsonDocument); + bool manageRequiredFeatures(); + QString parseJsonShow(QJsonObject json); + +private slots: + void on_infoBtn_clicked(); + void on_statusBtn_clicked(); + void on_launchBtn_clicked(); + +private: + Ui::userapps * ui; + generalDialog * generalDialogWindow; + + QDir appDir; + QFile execPath; + bool userAppEnabled; + QString appName; +}; + +#endif // USERAPPS_H diff --git a/userapps.ui b/userapps.ui new file mode 100644 index 0000000..8a075b0 --- /dev/null +++ b/userapps.ui @@ -0,0 +1,215 @@ + + + userapps + + + + 0 + 0 + 461 + 79 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 75 + true + + + + Launch + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + :/resources/info.png:/resources/info.png + + + + 34 + 34 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Disable + + + + + + + + + + + + + 5 + + + 5 + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + 0 + + + AppName + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 13 + 13 + + + + + + + + + + + + +