From b37dd810790bb3ca650a145c2648641392a42fa0 Mon Sep 17 00:00:00 2001 From: Martchus Date: Sat, 11 Jan 2025 22:54:28 +0100 Subject: [PATCH] Handle destruction of activity more gracefully Since Qt will put the app in a state where it can't recover from (the UI can never be shown again) we have to terminate everything else as well. Otherwise Go threads and the service/notification would still be around but one could never access the UI again. This is of course not ideal. Ideally, the activity lifecycle should be independent from the service livecycle and the service should be able to outlive the activity. The activity should be able to re-create itself once destroyed. --- .../martchus/syncthingtray/Activity.java | 11 +++- tray/gui/quick/app.cpp | 53 ++++++++++++------- tray/gui/quick/app.h | 3 +- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/tray/android/src/io/github/martchus/syncthingtray/Activity.java b/tray/android/src/io/github/martchus/syncthingtray/Activity.java index 6af74f15..06446b26 100644 --- a/tray/android/src/io/github/martchus/syncthingtray/Activity.java +++ b/tray/android/src/io/github/martchus/syncthingtray/Activity.java @@ -148,7 +148,13 @@ public void onStop() { } public void onDestroy() { + // stop service and libsyncthing + // note: QtActivity will exit the main thread in super.onDestroy() so we cannot keep the service running. + // It would not stop the service and Go threads but it would be impossible to re-enter the UI leaving + // the app in some kind of zombie state. Log.i(TAG, "Destroying"); + stopLibSyncthing(); + stopSyncthingService(); super.onDestroy(); } @@ -161,11 +167,12 @@ protected void onNewIntent(Intent intent) { private void sendAndroidIntentToQtQuickApp(String page, boolean fromNotification) { try { - onAndroidIntent(page, fromNotification); + handleAndroidIntent(page, fromNotification); } catch (java.lang.UnsatisfiedLinkError e) { showToast("Unable to open in Syncthing Tray app right now."); } } - private static native void onAndroidIntent(String page, boolean fromNotification); + private static native void handleAndroidIntent(String page, boolean fromNotification); + private static native void stopLibSyncthing(); } diff --git a/tray/gui/quick/app.cpp b/tray/gui/quick/app.cpp index 8ea51c9d..2de01278 100644 --- a/tray/gui/quick/app.cpp +++ b/tray/gui/quick/app.cpp @@ -81,14 +81,21 @@ static void deletePipelineCache() } #ifdef Q_OS_ANDROID -// define functions called from Java +/// \brief The JniFn namespace defines functions called from Java. +namespace JniFn { static App *appObjectForJava = nullptr; -static void onAndroidIntent(JNIEnv *, jobject, jstring page, jboolean fromNotification) +static void stopLibSyncthing(JNIEnv *, jobject) +{ + QMetaObject::invokeMethod(appObjectForJava, "stopLibSyncthing", Qt::QueuedConnection); +} + +static void handleAndroidIntent(JNIEnv *, jobject, jstring page, jboolean fromNotification) { QMetaObject::invokeMethod(appObjectForJava, "handleAndroidIntent", Qt::QueuedConnection, Q_ARG(QString, QJniObject::fromLocalRef(page).toString()), Q_ARG(bool, fromNotification)); } +} #endif App::App(bool insecure, QObject *parent) @@ -178,14 +185,15 @@ App::App(bool insecure, QObject *parent) #ifdef Q_OS_ANDROID // register native methods of Android activity - if (!appObjectForJava) { - appObjectForJava = this; + if (!JniFn::appObjectForJava) { + JniFn::appObjectForJava = this; auto env = QJniEnvironment(); auto registeredMethods = true; static const JNINativeMethod activityMethods[] = { - { "onAndroidIntent", "(Ljava/lang/String;Z)V", reinterpret_cast(onAndroidIntent) }, + { "handleAndroidIntent", "(Ljava/lang/String;Z)V", reinterpret_cast(JniFn::handleAndroidIntent) }, + { "stopLibSyncthing", "()V", reinterpret_cast(JniFn::stopLibSyncthing) }, }; - registeredMethods = env.registerNativeMethods("io/github/martchus/syncthingtray/Activity", activityMethods, 1) && registeredMethods; + registeredMethods = env.registerNativeMethods("io/github/martchus/syncthingtray/Activity", activityMethods, 2) && registeredMethods; if (!registeredMethods) { qWarning() << "Unable to register all native methods in JNI environment."; } @@ -695,6 +703,11 @@ void App::handleNewErrors(const std::vector &errors) { Q_UNUSED(errors) invalidateStatus(); +#ifdef Q_OS_ANDROID + if (errors.empty()) { + clearSyncthingErrorsNotification(); + } +#endif } void App::handleStateChanged(Qt::ApplicationState state) @@ -766,19 +779,14 @@ void App::clearAndroidExtraNotifications(int firstId, int lastId) void App::updateSyncthingErrorsNotification(CppUtilities::DateTime when, const QString &message) { - auto whenString = when.toString(); - m_syncthingErrors.reserve(m_syncthingErrors.size() + 1 + whenString.size() + 2 + message.size()); - if (!m_syncthingErrors.isEmpty()) { - m_syncthingErrors += QChar('\n'); - } - m_syncthingErrors += std::move(whenString); - m_syncthingErrors += QChar(':'); - m_syncthingErrors += QChar(' '); - m_syncthingErrors += message; - - const auto title = QJniObject::fromString(tr("Syncthing errors/notifications")); - static const auto text = QJniObject::fromString(QString()); - const auto subText = QJniObject::fromString(m_syncthingErrors); + ++m_syncthingErrors; + const auto title = QJniObject::fromString(m_syncthingErrors == 1 + ? tr("Syncthing error/notification") + : tr("%1 Syncthing errors/notifications").arg(m_syncthingErrors)); + const auto text = QJniObject::fromString(m_syncthingErrors == 1 + ? message + : tr("most recent: ") + message); + const auto subText = QJniObject::fromString(QString::fromStdString(when.toString())); static const auto page = QJniObject::fromString(QStringLiteral("connectionErrors")); const auto &icon = makeAndroidIcon(commonForkAwesomeIcons().exclamation); updateExtraAndroidNotification(title, text, subText, page, icon, 2); @@ -786,7 +794,7 @@ void App::updateSyncthingErrorsNotification(CppUtilities::DateTime when, const Q void App::clearSyncthingErrorsNotification() { - m_syncthingErrors.clear(); + m_syncthingErrors = 0; clearAndroidExtraNotifications(2, -1); } @@ -857,6 +865,11 @@ void App::handleAndroidIntent(const QString &data, bool fromNotification) emit newDirTriggered(folderRef.deviceId.toString(), folderRef.folderId.toString(), folderRef.folderLabel.toString()); } } + +void App::stopLibSyncthing() +{ + m_launcher.stopLibSyncthing(); +} #endif void App::clearInternalErrors() diff --git a/tray/gui/quick/app.h b/tray/gui/quick/app.h index 2c7f20e2..ad9b5ae9 100644 --- a/tray/gui/quick/app.h +++ b/tray/gui/quick/app.h @@ -292,6 +292,7 @@ private Q_SLOTS: void showNewDevice(const QString &devId, const QString &message); void showNewDir(const QString &devId, const QString &dirId, const QString &dirLabel, const QString &message); void handleAndroidIntent(const QString &page, bool fromNotification); + void stopLibSyncthing(); #endif private: @@ -315,7 +316,7 @@ private Q_SLOTS: QVariantList m_internalErrors; StatusInfo m_statusInfo; #ifdef Q_OS_ANDROID - QString m_syncthingErrors; + int m_syncthingErrors = 0; QHash m_androidIconCache; int m_androidNotificationId = 100000000; #endif