From d3ebc436571f7627d3440369285c0d924597b680 Mon Sep 17 00:00:00 2001
From: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
Date: Fri, 27 Nov 2020 11:49:34 -0500
Subject: [PATCH] dbus: handle dbus error with UI interaction

Gitlab: #160
Change-Id: Ica0aab9ba7f043c4ab56314bbd5312f75239ad51
---
 jami-qt.pro                                   |   6 +-
 qml.qrc                                       |   2 +
 src/DaemonReconnectWindow.qml                 | 225 ++++++++++++++++++
 src/MainApplicationWindow.qml                 |  26 ++
 src/commoncomponents/DaemonReconnectPopup.qml | 102 ++++++++
 src/constant/JamiStrings.qml                  |   4 +
 src/dbuserrorhandler.cpp                      |  93 ++++++++
 src/dbuserrorhandler.h                        |  56 +++++
 src/main.cpp                                  |   5 +-
 src/mainapplication.cpp                       |  43 +++-
 src/mainapplication.h                         |   4 +-
 11 files changed, 560 insertions(+), 6 deletions(-)
 create mode 100644 src/DaemonReconnectWindow.qml
 create mode 100644 src/commoncomponents/DaemonReconnectPopup.qml
 create mode 100644 src/dbuserrorhandler.cpp
 create mode 100644 src/dbuserrorhandler.h

diff --git a/jami-qt.pro b/jami-qt.pro
index 15cbafc36..69bffcb45 100644
--- a/jami-qt.pro
+++ b/jami-qt.pro
@@ -118,9 +118,11 @@ unix {
 
     # unix specific
     HEADERS += \
-               src/xrectsel.h
+            src/dbuserrorhandler.h \
+            src/xrectsel.h
     SOURCES += \
-               src/xrectsel.c
+            src/dbuserrorhandler.cpp \
+            src/xrectsel.c
 }
 
 # Input
diff --git a/qml.qrc b/qml.qrc
index b06ef1cb9..b8e16a65c 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -137,5 +137,7 @@
         <file>src/commoncomponents/PresenceIndicator.qml</file>
         <file>src/commoncomponents/AvatarImage.qml</file>
         <file>src/mainview/components/ParticipantOverlayMenu.qml</file>
+        <file>src/commoncomponents/DaemonReconnectPopup.qml</file>
+        <file>src/DaemonReconnectWindow.qml</file>
     </qresource>
 </RCC>
diff --git a/src/DaemonReconnectWindow.qml b/src/DaemonReconnectWindow.qml
new file mode 100644
index 000000000..ad6fd0564
--- /dev/null
+++ b/src/DaemonReconnectWindow.qml
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.14
+import QtQuick.Window 2.14
+import QtQuick.Controls 2.14
+import QtQuick.Layouts 1.14
+import QtQuick.Controls.Universal 2.14
+import QtGraphicalEffects 1.14
+
+// Should not import anything other than this
+// to make sure that it is self-dependent
+import net.jami.Models 1.0
+
+import "commoncomponents"
+
+ApplicationWindow {
+    id: root
+
+    property bool connectionFailed: false
+    property int preferredMargin: 15
+
+    Universal.theme: Universal.Light
+
+    title: "Jami"
+
+    width: 600
+    height: 500
+    minimumWidth: 600
+    minimumHeight: 500
+
+    visible: true
+
+    TextMetrics {
+        id: textMetrics
+    }
+
+    function getTextBoundingRect(font, text) {
+        textMetrics.font = font
+        textMetrics.text = text
+
+        return textMetrics.boundingRect
+    }
+
+    ResponsiveImage {
+        id: jamiLogoImage
+
+        anchors.fill: parent
+
+        smooth: true
+        antialiasing: true
+        source: "qrc:/images/logo-jami-standard-coul.svg"
+    }
+
+    Popup {
+        id: popup
+
+        // center in parent
+        x: Math.round((root.width - width) / 2)
+        y: Math.round((root.height - height) / 2)
+
+        modal: true
+        visible: false
+        closePolicy: Popup.NoAutoClose
+
+        contentItem: Rectangle {
+            id: contentRect
+
+            implicitHeight: daemonReconnectPopupColumnLayout.implicitHeight + 50
+
+            ColumnLayout {
+                id: daemonReconnectPopupColumnLayout
+
+                anchors.fill: parent
+
+                spacing: 0
+
+                Text {
+                    id: daemonReconnectPopupTextLabel
+
+                    Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
+                    Layout.topMargin: preferredMargin
+
+                    text: connectionFailed ?
+                              qsTr("Could not re-connect to the Jami daemon (dring).\nJami will now quit.") :
+                              qsTr("Trying to reconnect to the Jami daemon (dring)…")
+                    font.pointSize: 11
+                    horizontalAlignment: Text.AlignHCenter
+                    verticalAlignment: Text.AlignVCenter
+
+                    Component.onCompleted: {
+                        contentRect.implicitWidth = getTextBoundingRect(
+                                    font, text).width + 100
+                    }
+                }
+
+                AnimatedImage {
+                    Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
+                    Layout.preferredHeight: 30
+                    Layout.preferredWidth: 30
+                    Layout.bottomMargin: preferredMargin
+
+                    visible: !connectionFailed
+
+                    source: "qrc:/images/jami_rolling_spinner.gif"
+
+                    playing: true
+                    paused: false
+                    mipmap: true
+                    smooth: true
+                    fillMode: Image.PreserveAspectFit
+                }
+
+                Button {
+                    id: btnOk
+
+                    Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
+                    Layout.preferredWidth: 128
+                    Layout.preferredHeight: 32
+                    Layout.bottomMargin: preferredMargin
+                    visible: connectionFailed
+
+                    property color hoveredColor: "#0e81c5"
+                    property color pressedColor: "#273261"
+                    property color normalColor: "#00aaff"
+
+                    contentItem: Item {
+                        Rectangle {
+                            anchors.fill: parent
+                            color: "transparent"
+
+                            Text {
+                                id: buttonText
+
+                                anchors.centerIn: parent
+
+                                width: {
+                                    return (parent.width / 2 - 18) * 2
+                                }
+
+                                text: qsTr("Ok")
+
+                                color: {
+                                    if (btnOk.hovered)
+                                        return btnOk.hoveredColor
+                                    if (btnOk.checked)
+                                        return btnOk.pressedColor
+                                    return btnOk.normalColor
+                                }
+                                font: root.font
+                                horizontalAlignment: Text.AlignHCenter
+                            }
+                        }
+                    }
+
+                    onClicked: Qt.quit()
+
+                    background: Rectangle {
+                        id: backgroundRect
+                        anchors.fill: parent
+                        color: "transparent"
+                        border.color: {
+                            if (btnOk.hovered)
+                                return btnOk.hoveredColor
+                            if (btnOk.checked)
+                                return btnOk.pressedColor
+                            return btnOk.normalColor
+                        }
+                        radius: 4
+                    }
+                }
+            }
+        }
+    }
+
+    Connections {
+        target: DBusErrorHandler
+
+        function onShowDaemonReconnectPopup(visible) {
+            if (visible)
+                popup.open()
+            else {
+                popup.close()
+                Qt.quit()
+            }
+        }
+
+        function onDaemonReconnectFailed() {
+            root.connectionFailed = true
+        }
+    }
+
+    overlay.modal: ColorOverlay {
+        source: root.contentItem
+        color: "transparent"
+
+        // Color animation for overlay when pop up is shown.
+        ColorAnimation on color {
+            to: Qt.rgba(0, 0, 0, 0.33)
+            duration: 500
+        }
+    }
+
+    Component.onCompleted: {
+        DBusErrorHandler.setActive(true)
+
+        x = Screen.width / 2 - width / 2
+        y = Screen.height / 2 - height / 2
+    }
+}
diff --git a/src/MainApplicationWindow.qml b/src/MainApplicationWindow.qml
index 32319ce29..d92f9972b 100644
--- a/src/MainApplicationWindow.qml
+++ b/src/MainApplicationWindow.qml
@@ -116,6 +116,9 @@ ApplicationWindow {
         onAccountMigrationFinished: startClient()
     }
 
+    DaemonReconnectPopup {
+        id: daemonReconnectPopup
+    }
 
     Loader {
         id: mainApplicationLoader
@@ -167,6 +170,26 @@ ApplicationWindow {
         }
     }
 
+    Connections {
+        target: {
+            if (Qt.platform.os !== "windows")
+                return DBusErrorHandler
+            return null
+        }
+        ignoreUnknownSignals: true
+
+        function onShowDaemonReconnectPopup(visible) {
+            if (visible)
+                daemonReconnectPopup.open()
+            else
+                daemonReconnectPopup.close()
+        }
+
+        function onDaemonReconnectFailed() {
+            daemonReconnectPopup.connectionFailed = true
+        }
+    }
+
     onClosing: root.close()
 
     onScreenChanged: JamiQmlUtils.mainApplicationScreen = root.screen
@@ -176,5 +199,8 @@ ApplicationWindow {
             startClient()
         }
         JamiQmlUtils.mainApplicationScreen = root.screen
+
+        if (Qt.platform.os !== "windows")
+            DBusErrorHandler.setActive(true)
     }
 }
diff --git a/src/commoncomponents/DaemonReconnectPopup.qml b/src/commoncomponents/DaemonReconnectPopup.qml
new file mode 100644
index 000000000..631f9318a
--- /dev/null
+++ b/src/commoncomponents/DaemonReconnectPopup.qml
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+import QtQuick.Layouts 1.14
+import net.jami.Models 1.0
+import net.jami.Constants 1.0
+
+ModalPopup {
+    id: root
+
+    property bool connectionFailed: false
+    property int preferredMargin: 15
+
+    autoClose: false
+
+    contentItem: Rectangle {
+        id: contentRect
+
+        implicitHeight: daemonReconnectPopupColumnLayout.implicitHeight + 50
+
+        color: JamiTheme.secondaryBackgroundColor
+
+        ColumnLayout {
+            id: daemonReconnectPopupColumnLayout
+
+            anchors.fill: parent
+
+            spacing: 0
+
+            Text {
+                id: daemonReconnectPopupTextLabel
+
+                Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
+                Layout.topMargin: preferredMargin
+
+                text: connectionFailed ? JamiStrings.reconnectionFailed :
+                                         JamiStrings.reconnectDaemon
+                font.pointSize: JamiTheme.textFontSize + 2
+                horizontalAlignment: Text.AlignHCenter
+                verticalAlignment: Text.AlignVCenter
+                color: JamiTheme.textColor
+
+                Component.onCompleted: {
+                    contentRect.implicitWidth = JamiQmlUtils.getTextBoundingRect(
+                                font, text).width + 100
+                }
+            }
+
+            AnimatedImage {
+                Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
+                Layout.preferredHeight: 30
+                Layout.preferredWidth: 30
+                Layout.bottomMargin: preferredMargin
+
+                visible: !connectionFailed
+
+                source: "qrc:/images/jami_rolling_spinner.gif";
+
+                playing: true
+                paused: false
+                mipmap: true
+                smooth: true
+                fillMode: Image.PreserveAspectFit
+            }
+
+            MaterialButton {
+                id: btnOk
+
+                Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
+                Layout.preferredWidth: JamiTheme.preferredFieldWidth / 2
+                Layout.preferredHeight: JamiTheme.preferredFieldHeight
+                Layout.bottomMargin: preferredMargin
+                visible: connectionFailed
+
+                text: qsTr("Ok")
+                color: JamiTheme.buttonTintedBlue
+                hoveredColor: JamiTheme.buttonTintedBlueHovered
+                pressedColor: JamiTheme.buttonTintedBluePressed
+                outlined: true
+
+                onClicked: Qt.quit()
+            }
+        }
+    }
+}
diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml
index 517fa28b9..30b13236b 100644
--- a/src/constant/JamiStrings.qml
+++ b/src/constant/JamiStrings.qml
@@ -406,5 +406,9 @@ Item {
     property string maximizeParticipant: qsTr("Maximize")
     property string minimizeParticipant: qsTr("Minimize")
     property string hangupParticipant: qsTr("Hangup")
+
+    // Daemon reconnection
+    property string reconnectDaemon: qsTr("Trying to reconnect to the Jami daemon (dring)…")
+    property string reconnectionFailed: qsTr("Could not re-connect to the Jami daemon (dring).\nJami will now quit.")
 }
 
diff --git a/src/dbuserrorhandler.cpp b/src/dbuserrorhandler.cpp
new file mode 100644
index 000000000..33d6bf331
--- /dev/null
+++ b/src/dbuserrorhandler.cpp
@@ -0,0 +1,93 @@
+/*!
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "dbuserrorhandler.h"
+
+#include "api/lrc.h"
+#include "globalinstances.h"
+
+#include <QTimer>
+
+namespace Interfaces {
+
+void
+DBusErrorHandler::errorCallback()
+{
+    qDebug() << "Dring has possibly crashed, "
+                "or has been killed... will wait 2.5 seconds and try to reconnect";
+
+    emit showDaemonReconnectPopup(true);
+
+    QTimer::singleShot(2500, [this]() {
+        if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid())) {
+            qDebug() << "Could not reconnect to the daemon";
+            emit daemonReconnectFailed();
+        } else {
+            static_cast<DBusErrorHandler&>(GlobalInstances::dBusErrorHandler())
+                .finishedHandlingError();
+        }
+    });
+}
+
+void
+DBusErrorHandler::setActive(bool active)
+{
+    handlerActive_ = active;
+
+    if (active) {
+        if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid()))
+            connectionError(QString());
+    }
+}
+
+void
+DBusErrorHandler::connectionError(const QString& error)
+{
+    qDebug() << error;
+
+    if (!handlerActive_)
+        return;
+
+    if (!handlingError) {
+        handlingError = true;
+        errorCallback();
+    }
+}
+
+void
+DBusErrorHandler::invalidInterfaceError(const QString& error)
+{
+    qDebug() << error;
+
+    if (!handlerActive_)
+        return;
+
+    if (!handlingError) {
+        handlingError = true;
+        errorCallback();
+    }
+}
+
+void
+DBusErrorHandler::finishedHandlingError()
+{
+    handlingError = false;
+    emit showDaemonReconnectPopup(false);
+}
+
+} // namespace Interfaces
diff --git a/src/dbuserrorhandler.h b/src/dbuserrorhandler.h
new file mode 100644
index 000000000..15231bc97
--- /dev/null
+++ b/src/dbuserrorhandler.h
@@ -0,0 +1,56 @@
+/*!
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <interfaces/dbuserrorhandleri.h>
+
+namespace Interfaces {
+
+class DBusErrorHandler : public QObject, public DBusErrorHandlerI
+{
+    Q_OBJECT
+public:
+    DBusErrorHandler() {};
+    ~DBusErrorHandler() {};
+
+    Q_INVOKABLE void setActive(bool active);
+
+    void connectionError(const QString& error) override;
+    void invalidInterfaceError(const QString& error) override;
+
+    void finishedHandlingError();
+
+signals:
+    void showDaemonReconnectPopup(bool visible);
+    void daemonReconnectFailed();
+
+private:
+    void errorCallback();
+
+    // Keeps track if we're in the process of handling an error already,
+    // so that we don't keep displaying error dialogs;
+    // we use an atomic in case the errors come from multiple threads
+    std::atomic_bool handlingError {false};
+
+    bool handlerActive_ {false};
+};
+
+} // namespace Interfaces
+Q_DECLARE_METATYPE(Interfaces::DBusErrorHandler*)
diff --git a/src/main.cpp b/src/main.cpp
index fe63dbf10..51bd99c18 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -80,7 +80,10 @@ main(int argc, char* argv[])
         return 0;
     }
 
-    app.init();
+    if (!app.init()) {
+        guard.release();
+        return 0;
+    }
 
     /*
      * Exec the application.
diff --git a/src/mainapplication.cpp b/src/mainapplication.cpp
index 6b7b365a2..744913ea5 100644
--- a/src/mainapplication.cpp
+++ b/src/mainapplication.cpp
@@ -24,6 +24,7 @@
 #include "appsettingsmanager.h"
 #include "connectivitymonitor.h"
 #include "globalsystemtray.h"
+#include "namedirectory.h"
 #include "qmlregister.h"
 #include "qrimageprovider.h"
 #include "tintedbuttonimageprovider.h"
@@ -43,6 +44,11 @@
 #include <windows.h>
 #endif
 
+#ifdef Q_OS_UNIX
+#include "globalinstances.h"
+#include "dbuserrorhandler.h"
+#endif
+
 #if defined _MSC_VER && !COMPILE_ONLY
 #include <gnutls/gnutls.h>
 #endif
@@ -122,7 +128,7 @@ MainApplication::MainApplication(int& argc, char** argv)
     QObject::connect(this, &QApplication::aboutToQuit, [this] { cleanup(); });
 }
 
-void
+bool
 MainApplication::init()
 {
     setWindowIcon(QIcon(":images/jami.ico"));
@@ -146,6 +152,33 @@ MainApplication::init()
     gnutls_global_init();
 #endif
 
+#ifdef Q_OS_UNIX
+    GlobalInstances::setDBusErrorHandler(std::make_unique<Interfaces::DBusErrorHandler>());
+    auto dBusErrorHandlerQObject = dynamic_cast<QObject*>(&GlobalInstances::dBusErrorHandler());
+    qmlRegisterSingletonType<Interfaces::DBusErrorHandler>("net.jami.Models",
+                                                           1,
+                                                           0,
+                                                           "DBusErrorHandler",
+                                                           [dBusErrorHandlerQObject](QQmlEngine* e,
+                                                                                     QJSEngine* se)
+                                                               -> QObject* {
+                                                               Q_UNUSED(e)
+                                                               Q_UNUSED(se)
+                                                               return dBusErrorHandlerQObject;
+                                                           });
+    engine_->setObjectOwnership(dBusErrorHandlerQObject, QQmlEngine::CppOwnership);
+
+    if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid())) {
+        engine_->load(QUrl(QStringLiteral("qrc:/src/DaemonReconnectWindow.qml")));
+        exec();
+
+        if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid()))
+            return false;
+        else
+            engine_.reset(new QQmlApplicationEngine());
+    }
+#endif
+
     initLrc(results[opts::UPDATEURL].toString(), connectivityMonitor_);
 
     connect(connectivityMonitor_, &ConnectivityMonitor::connectivityChanged, [] {
@@ -173,6 +206,8 @@ MainApplication::init()
     initSettings();
     initSystray();
     initQmlEngine();
+
+    return true;
 }
 
 void
@@ -318,6 +353,12 @@ MainApplication::initQmlEngine()
     engine_->addImageProvider(QLatin1String("tintedPixmap"), new TintedButtonImageProvider());
     engine_->addImageProvider(QLatin1String("avatarImage"), new AvatarImageProvider());
 
+    engine_->setObjectOwnership(&LRCInstance::avModel(), QQmlEngine::CppOwnership);
+    engine_->setObjectOwnership(&LRCInstance::pluginModel(), QQmlEngine::CppOwnership);
+    engine_->setObjectOwnership(LRCInstance::getUpdateManager(), QQmlEngine::CppOwnership);
+    engine_->setObjectOwnership(&LRCInstance::instance(), QQmlEngine::CppOwnership);
+    engine_->setObjectOwnership(&NameDirectory::instance(), QQmlEngine::CppOwnership);
+
     engine_->load(QUrl(QStringLiteral("qrc:/src/MainApplicationWindow.qml")));
 }
 
diff --git a/src/mainapplication.h b/src/mainapplication.h
index 4343ec6b3..41269c9b3 100644
--- a/src/mainapplication.h
+++ b/src/mainapplication.h
@@ -37,7 +37,7 @@ public:
     explicit MainApplication(int& argc, char** argv);
     ~MainApplication() = default;
 
-    void init();
+    bool init();
 
 private:
     void loadTranslations();
@@ -51,6 +51,6 @@ private:
 
 private:
     QScopedPointer<QFile> debugFile_;
-    QQmlApplicationEngine* engine_;
+    QScopedPointer<QQmlApplicationEngine> engine_;
     ConnectivityMonitor* connectivityMonitor_;
 };
-- 
GitLab