From 5bebf83d52d39e833d0e299663165145ab940a92 Mon Sep 17 00:00:00 2001
From: dsc <xmrdsc@protonmail.com>
Date: Thu, 13 Dec 2018 19:02:02 +0100
Subject: [PATCH] Lock wallet on inactivity

---
 filter.cpp                        |  4 ++
 filter.h                          |  1 +
 main.cpp                          |  2 +-
 main.qml                          | 39 ++++++++++++++
 pages/settings/Settings.qml       | 13 +++--
 pages/settings/SettingsLayout.qml | 84 +++++++++++++++++++++++++++++++
 6 files changed, 135 insertions(+), 8 deletions(-)

diff --git a/filter.cpp b/filter.cpp
index 3ba011bf..9129d92b 100644
--- a/filter.cpp
+++ b/filter.cpp
@@ -38,6 +38,10 @@ filter::filter(QObject *parent) :
 }
 
 bool filter::eventFilter(QObject *obj, QEvent *ev) {
+    if(ev->type() == QEvent::KeyPress || ev->type() == QEvent::MouseButtonRelease){
+        emit userActivity();
+    }
+
     switch(ev->type()) {
     case QEvent::KeyPress: {
         QKeyEvent *ke = static_cast<QKeyEvent*>(ev);
diff --git a/filter.h b/filter.h
index 4c998a29..bb3a78b7 100644
--- a/filter.h
+++ b/filter.h
@@ -48,6 +48,7 @@ signals:
     void sequenceReleased(const QVariant &o, const QVariant &seq);
     void mousePressed(const QVariant &o, const QVariant &x, const QVariant &y);
     void mouseReleased(const QVariant &o, const QVariant &x, const QVariant &y);
+    void userActivity();
 };
 
 #endif // FILTER_H
diff --git a/main.cpp b/main.cpp
index 5ebaff50..82229c23 100644
--- a/main.cpp
+++ b/main.cpp
@@ -324,6 +324,6 @@ int main(int argc, char *argv[])
     QObject::connect(eventFilter, SIGNAL(sequenceReleased(QVariant,QVariant)), rootObject, SLOT(sequenceReleased(QVariant,QVariant)));
     QObject::connect(eventFilter, SIGNAL(mousePressed(QVariant,QVariant,QVariant)), rootObject, SLOT(mousePressed(QVariant,QVariant,QVariant)));
     QObject::connect(eventFilter, SIGNAL(mouseReleased(QVariant,QVariant,QVariant)), rootObject, SLOT(mouseReleased(QVariant,QVariant,QVariant)));
-
+    QObject::connect(eventFilter, SIGNAL(userActivity()), rootObject, SLOT(userActivity()));
     return app.exec();
 }
diff --git a/main.qml b/main.qml
index 84f7d38d..d8f9a699 100644
--- a/main.qml
+++ b/main.qml
@@ -75,6 +75,7 @@ ApplicationWindow {
     property var cameraUi
     property bool remoteNodeConnected: false
     property bool androidCloseTapped: false;
+    property int userLastActive;  // epoch
     // Default daemon addresses
     readonly property string localDaemonAddress : persistentSettings.nettype == NetworkType.MAINNET ? "localhost:18081" : persistentSettings.nettype == NetworkType.TESTNET ? "localhost:28081" : "localhost:38081"
     property string currentDaemonAddress;
@@ -219,6 +220,8 @@ ApplicationWindow {
         // Local daemon settings
         walletManager.setDaemonAddress(localDaemonAddress)
 
+        // enable user inactivity timer
+        userInActivityTimer.running = true;
 
         // wallet already opened with wizard, we just need to initialize it
         if (typeof wizard.m_wallet !== 'undefined') {
@@ -932,6 +935,8 @@ ApplicationWindow {
         rootItem.state = "wizard"
         // reset balance
         leftPanel.balanceText = leftPanel.unlockedBalanceText = walletManager.displayAmount(0);
+        // disable inactivity timer
+        userInActivityTimer.running = false;
     }
 
     function hideMenu() {
@@ -1041,6 +1046,8 @@ ApplicationWindow {
         property int segregationHeight: 0
         property int kdfRounds: 1
         property bool hideBalance: false
+        property bool lockOnUserInActivity: true
+        property int lockOnUserInActivityInterval: 10  // minutes
     }
 
     // Information dialog
@@ -1696,6 +1703,12 @@ ApplicationWindow {
         triggeredOnStart: false
     }
 
+    Timer {
+        id: userInActivityTimer
+        interval: 2000; running: false; repeat: true
+        onTriggered: checkInUserActivity()
+    }
+
     Rectangle {
         id: statusMessage
         z: 99
@@ -1823,6 +1836,32 @@ ApplicationWindow {
         leftPanel.balanceLabelText = qsTr("Balance")
     }
 
+    function userActivity() {
+        // register user activity
+        var epoch = Math.floor((new Date).getTime()/1000);
+        appWindow.userLastActive = epoch;
+    }
+
+    function checkInUserActivity() {
+        if(!persistentSettings.lockOnUserInActivity) return;
+
+        // prompt password after X seconds of inactivity
+        var epoch = Math.floor((new Date).getTime() / 1000);
+        var inactivity = epoch - appWindow.userLastActive;
+        if(inactivity < (persistentSettings.lockOnUserInActivityInterval * 60)) return;
+
+        passwordDialog.onAcceptedCallback = function() {
+            if(walletPassword === passwordDialog.password){
+                passwordDialog.close();
+            } else {
+                passwordDialog.showError(qsTr("Wrong password"));
+            }
+        }
+
+        passwordDialog.onRejectedCallback = function() { appWindow.showWizard(); }
+        passwordDialog.open();
+    }
+
     // Daemon console
     DaemonConsole {
         id: daemonConsolePopup
diff --git a/pages/settings/Settings.qml b/pages/settings/Settings.qml
index 5aabcccd..9aa44461 100644
--- a/pages/settings/Settings.qml
+++ b/pages/settings/Settings.qml
@@ -65,17 +65,16 @@ ColumnLayout {
 
         onCurrentViewChanged: {
             if (previousView) {
-//                if (typeof previousView.onPageClosed === "function") {
-//                    previousView.onPageClosed();
-//                }
+                if (typeof previousView.onPageClosed === "function") {
+                    previousView.onPageClosed();
+                }
             }
             previousView = currentView
             if (currentView) {
                 stackView.replace(currentView)
-                // Component.onCompleted is called before wallet is initilized
-//                if (typeof currentView.onPageCompleted === "function") {
-//                    currentView.onPageCompleted();
-//                }
+                if (typeof currentView.onPageCompleted === "function") {
+                    currentView.onPageCompleted();
+                }
             }
         }
 
diff --git a/pages/settings/SettingsLayout.qml b/pages/settings/SettingsLayout.qml
index 974da5e3..6914a438 100644
--- a/pages/settings/SettingsLayout.qml
+++ b/pages/settings/SettingsLayout.qml
@@ -40,6 +40,14 @@ Rectangle {
     height: 1400
     Layout.fillWidth: true
 
+    function onPageCompleted() {
+        userInactivitySliderTimer.running = true;
+    }
+
+    function onPageClosed() {
+        userInactivitySliderTimer.running = false;
+    }
+
     ColumnLayout {
         id: settingsUI
         property int itemHeight: 60 * scaleRatio
@@ -70,6 +78,82 @@ Rectangle {
             text: qsTr("Hide balance") + translationManager.emptyString
         }
 
+        MoneroComponents.CheckBox {
+            visible: !isMobile
+            id: userInActivityCheckbox
+            checked: persistentSettings.lockOnUserInActivity
+            onClicked: persistentSettings.lockOnUserInActivity = !persistentSettings.lockOnUserInActivity
+            text: qsTr("Lock wallet on inactivity") + translationManager.emptyString
+        }
+
+        ColumnLayout {
+            visible: userInActivityCheckbox.checked
+            Layout.fillWidth: true
+            Layout.topMargin: 6 * scaleRatio
+            Layout.leftMargin: 42 * scaleRatio
+            spacing: 0
+
+            MoneroComponents.TextBlock {
+                font.pixelSize: 14 * scaleRatio
+                Layout.fillWidth: true
+                text: {
+                    var val = userInactivitySlider.value;
+                    var minutes = val > 1 ? qsTr("minutes") : qsTr("minute");
+
+                    qsTr("After ") + val + " " + minutes + translationManager.emptyString;
+                }
+            }
+
+            Slider {
+                id: userInactivitySlider
+                from: 1
+                value: persistentSettings.lockOnUserInActivityInterval
+                to: 60
+                leftPadding: 0
+                stepSize: 1
+                snapMode: Slider.SnapAlways
+
+                background: Rectangle {
+                    x: parent.leftPadding
+                    y: parent.topPadding + parent.availableHeight / 2 - height / 2
+                    implicitWidth: 200 * scaleRatio
+                    implicitHeight: 4 * scaleRatio
+                    width: parent.availableWidth
+                    height: implicitHeight
+                    radius: 2
+                    color: MoneroComponents.Style.grey
+
+                    Rectangle {
+                        width: parent.visualPosition * parent.width
+                        height: parent.height
+                        color: MoneroComponents.Style.green
+                        radius: 2
+                    }
+                }
+
+                handle: Rectangle {
+                    x: parent.leftPadding + parent.visualPosition * (parent.availableWidth - width)
+                    y: parent.topPadding + parent.availableHeight / 2 - height / 2
+                    implicitWidth: 18 * scaleRatio
+                    implicitHeight: 18 * scaleRatio
+                    radius: 8
+                    color: parent.pressed ? "#f0f0f0" : "#f6f6f6"
+                    border.color: MoneroComponents.Style.grey
+                }
+            }
+
+            Timer {
+                // @TODO: Slider.onMoved{} is available in Qt > 5.9, use a hacky timer for now
+                id: userInactivitySliderTimer
+                interval: 1000; running: false; repeat: true
+                onTriggered: {
+                    if(persistentSettings.lockOnUserInActivityInterval != userInactivitySlider.value) {
+                        persistentSettings.lockOnUserInActivityInterval = userInactivitySlider.value;
+                    }
+                }
+            }
+        }
+
         MoneroComponents.TextBlock {
             visible: isMobile
             font.pixelSize: 14