From 98ab794f13e281edad5e7a2c2a1c9241b92e10e3 Mon Sep 17 00:00:00 2001 From: Alexander Bilyak Date: Tue, 10 Nov 2020 08:09:17 +0000 Subject: [PATCH] [GODT-274] GUI changes for autoupdates [GODT-275] Add enable/disable auto updates GUI option Refactor Updater module GODT-805 Changed manual update information bar layout GODT-806, GODT-875 Change update dialogs Refactor InformationBar --- Changelog.md | 2 +- internal/app/bridge/bridge.go | 56 +++- internal/app/ie/ie.go | 57 +++- internal/config/settings/settings.go | 2 +- internal/frontend/cli-ie/frontend.go | 9 +- internal/frontend/cli/frontend.go | 9 +- internal/frontend/frontend.go | 30 +- .../frontend/qml/BridgeUI/DialogYesNo.qml | 2 +- internal/frontend/qml/BridgeUI/HelpView.qml | 6 +- internal/frontend/qml/BridgeUI/MainWindow.qml | 45 +-- .../frontend/qml/BridgeUI/SettingsView.qml | 19 ++ internal/frontend/qml/Gui.qml | 75 +++-- internal/frontend/qml/GuiIE.qml | 60 ++-- .../qml/ImportExportUI/DialogYesNo.qml | 2 +- .../frontend/qml/ImportExportUI/HelpView.qml | 6 +- .../qml/ImportExportUI/MainWindow.qml | 37 +-- .../qml/ImportExportUI/SettingsView.qml | 19 ++ .../frontend/qml/ProtonUI/DialogUpdate.qml | 129 +++++--- .../frontend/qml/ProtonUI/InformationBar.qml | 284 +++++++++++++++--- internal/frontend/qml/ProtonUI/Style.qml | 1 - internal/frontend/qml/tst_Gui.qml | 98 +++--- internal/frontend/qml/tst_GuiIE.qml | 142 +++++---- internal/frontend/qt-ie/frontend.go | 90 +++++- internal/frontend/qt-ie/frontend_nogui.go | 11 +- internal/frontend/qt-ie/ui.go | 30 +- internal/frontend/qt/frontend.go | 84 +++++- internal/frontend/qt/frontend_nogui.go | 9 +- internal/frontend/qt/ui.go | 28 +- internal/frontend/types/types.go | 3 + internal/updater/install_darwin.go | 8 +- internal/updater/install_default.go | 8 +- internal/updater/updater.go | 105 +++---- internal/updater/updater_test.go | 199 +++++------- unreleased.md | 16 + 34 files changed, 1069 insertions(+), 612 deletions(-) diff --git a/Changelog.md b/Changelog.md index 3fc5cdf0..f97ff037 100644 --- a/Changelog.md +++ b/Changelog.md @@ -193,7 +193,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) * GODT-682 Persistent anonymous API cookies for Import-Export. * GODT-357 Use go-message to make a better message parser. * GODT-720 Time measurement of progress for Import-Export. -* GODT-693 Launcher +* GODT-693 Launcher. ### Changed * GODT-511 User agent format changed. diff --git a/internal/app/bridge/bridge.go b/internal/app/bridge/bridge.go index a9aa22fc..4df51c49 100644 --- a/internal/app/bridge/bridge.go +++ b/internal/app/bridge/bridge.go @@ -27,6 +27,7 @@ import ( "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/frontend" + "github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/imap" "github.com/ProtonMail/proton-bridge/internal/smtp" "github.com/ProtonMail/proton-bridge/internal/updater" @@ -119,19 +120,50 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen] b, ) - b.Updater.Watch( - time.Hour, - func(update updater.VersionInfo) error { - if !b.Settings.GetBool(settings.AutoUpdateKey) { - return f.NotifyManualUpdate(update) - } + // Watch for updates routine + go func() { + ticker := time.NewTicker(time.Hour) - return b.Updater.InstallUpdate(update) - }, - func(err error) { - logrus.WithError(err).Error("An error occurred while watching for updates") - }, - ) + for { + checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey)) + <-ticker.C + } + }() return f.Loop() } + +func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) { + version, err := u.Check() + if err != nil { + logrus.WithError(err).Error("An error occurred while checking for updates") + f.NotifySilentUpdateError(err) + return + } + + if !u.IsUpdateApplicable(version) { + logrus.Debug("No need to update") + return + } + + logrus.WithField("version", version.Version).Info("An update is available") + + if !autoUpdate { + f.NotifyManualUpdate(version, u.CanInstall(version)) + return + } + + if !u.CanInstall(version) { + logrus.Info("A manual update is required") + f.NotifySilentUpdateError(updater.ErrManualUpdateRequired) + return + } + + if err := u.InstallUpdate(version); err != nil { + logrus.WithError(err).Error("An error occurred while silent installing updates") + f.NotifySilentUpdateError(err) + return + } + + f.NotifySilentUpdateInstalled() +} diff --git a/internal/app/ie/ie.go b/internal/app/ie/ie.go index 83d733f7..152ced72 100644 --- a/internal/app/ie/ie.go +++ b/internal/app/ie/ie.go @@ -26,6 +26,7 @@ import ( "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/frontend" + "github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/importexport" "github.com/ProtonMail/proton-bridge/internal/updater" "github.com/sirupsen/logrus" @@ -59,25 +60,57 @@ func run(b *base.Base, c *cli.Context) error { frontendMode, b.CrashHandler, b.Locations, + b.Settings, b.Listener, b.Updater, ie, b, ) - b.Updater.Watch( - time.Hour, - func(update updater.VersionInfo) error { - if !b.Settings.GetBool(settings.AutoUpdateKey) { - return f.NotifyManualUpdate(update) - } + // Watch for updates routine + go func() { + ticker := time.NewTicker(time.Hour) - return b.Updater.InstallUpdate(update) - }, - func(err error) { - logrus.WithError(err).Error("An error occurred while watching for updates") - }, - ) + for { + checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey)) + <-ticker.C + } + }() return f.Loop() } + +func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) { + version, err := u.Check() + if err != nil { + logrus.WithError(err).Error("An error occurred while checking for updates") + f.NotifySilentUpdateError(err) + return + } + + if !u.IsUpdateApplicable(version) { + logrus.Debug("No need to update") + return + } + + logrus.WithField("version", version.Version).Info("An update is available") + + if !autoUpdate { + f.NotifyManualUpdate(version, u.CanInstall(version)) + return + } + + if !u.CanInstall(version) { + logrus.Info("A manual update is required") + f.NotifySilentUpdateError(updater.ErrManualUpdateRequired) + return + } + + if err := u.InstallUpdate(version); err != nil { + logrus.WithError(err).Error("An error occurred while silent installing updates") + f.NotifySilentUpdateError(err) + return + } + + f.NotifySilentUpdateInstalled() +} diff --git a/internal/config/settings/settings.go b/internal/config/settings/settings.go index c46e3dcc..f09258e2 100644 --- a/internal/config/settings/settings.go +++ b/internal/config/settings/settings.go @@ -72,7 +72,7 @@ func (s *Settings) setDefaultValues() { s.setDefault(NextHeartbeatKey, fmt.Sprintf("%v", time.Now().Unix())) s.setDefault(AllowProxyKey, "true") s.setDefault(AutostartKey, "true") - s.setDefault(AutoUpdateKey, "false") + s.setDefault(AutoUpdateKey, "true") s.setDefault(ReportOutgoingNoEncKey, "false") s.setDefault(LastVersionKey, "") s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64())) diff --git a/internal/frontend/cli-ie/frontend.go b/internal/frontend/cli-ie/frontend.go index eedc9e53..20f12a5b 100644 --- a/internal/frontend/cli-ie/frontend.go +++ b/internal/frontend/cli-ie/frontend.go @@ -226,7 +226,12 @@ WARNING: The CLI is an experimental feature and does not yet cover all functiona return nil } -func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo) error { +func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) { // NOTE: Save the update somewhere so that it can be installed when user chooses "install now". - return nil +} + +func (f *frontendCLI) NotifySilentUpdateInstalled() { +} + +func (f *frontendCLI) NotifySilentUpdateError(err error) { } diff --git a/internal/frontend/cli/frontend.go b/internal/frontend/cli/frontend.go index ed3c3ff7..d1f00d59 100644 --- a/internal/frontend/cli/frontend.go +++ b/internal/frontend/cli/frontend.go @@ -253,7 +253,12 @@ func (f *frontendCLI) Loop() error { return nil } -func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo) error { +func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) { // NOTE: Save the update somewhere so that it can be installed when user chooses "install now". - return nil +} + +func (f *frontendCLI) NotifySilentUpdateInstalled() { +} + +func (f *frontendCLI) NotifySilentUpdateError(err error) { } diff --git a/internal/frontend/frontend.go b/internal/frontend/frontend.go index cca77eaa..44c83a1d 100644 --- a/internal/frontend/frontend.go +++ b/internal/frontend/frontend.go @@ -40,7 +40,9 @@ var ( // Frontend is an interface to be implemented by each frontend type (cli, gui, html). type Frontend interface { Loop() error - NotifyManualUpdate(update updater.VersionInfo) error + NotifyManualUpdate(update updater.VersionInfo, canInstall bool) + NotifySilentUpdateInstalled() + NotifySilentUpdateError(error) } // New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`. @@ -123,8 +125,8 @@ func NewImportExport( buildVersion, frontendType string, panicHandler types.PanicHandler, - locations *locations.Locations, + settings *settings.Settings, eventListener listener.Listener, updater types.Updater, ie *importexport.ImportExport, @@ -137,6 +139,7 @@ func NewImportExport( frontendType, panicHandler, locations, + settings, eventListener, updater, ieWrap, @@ -149,8 +152,8 @@ func newIEFrontend( buildVersion, frontendType string, panicHandler types.PanicHandler, - locations *locations.Locations, + settings *settings.Settings, eventListener listener.Listener, updater types.Updater, ie types.ImportExporter, @@ -158,8 +161,25 @@ func newIEFrontend( ) Frontend { switch frontendType { case "cli": - return cliie.New(panicHandler, locations, eventListener, updater, ie, restarter) + return cliie.New( + panicHandler, + locations, + eventListener, + updater, + ie, + restarter, + ) default: - return qtie.New(version, buildVersion, panicHandler, locations, eventListener, updater, ie, restarter) + return qtie.New( + version, + buildVersion, + panicHandler, + locations, + settings, + eventListener, + updater, + ie, + restarter, + ) } } diff --git a/internal/frontend/qml/BridgeUI/DialogYesNo.qml b/internal/frontend/qml/BridgeUI/DialogYesNo.qml index ca1470bf..82e38491 100644 --- a/internal/frontend/qml/BridgeUI/DialogYesNo.qml +++ b/internal/frontend/qml/BridgeUI/DialogYesNo.qml @@ -368,7 +368,7 @@ Dialog { if ( state == "quit" ) { Qt.quit () } if ( state == "instance exists" ) { Qt.quit () } if ( state == "noKeychain" ) { Qt.quit () } - if ( state == "checkUpdates" ) { go.runCheckVersion (true) } + if ( state == "checkUpdates" ) { } } } diff --git a/internal/frontend/qml/BridgeUI/HelpView.qml b/internal/frontend/qml/BridgeUI/HelpView.qml index e422df80..e854d568 100644 --- a/internal/frontend/qml/BridgeUI/HelpView.qml +++ b/internal/frontend/qml/BridgeUI/HelpView.qml @@ -74,9 +74,7 @@ Item { rightIcon.text : Style.fa.chevron_circle_right rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt onClicked: { - dialogGlobal.state="checkUpdates" - dialogGlobal.show() - dialogGlobal.confirmed() + go.checkForUpdates() } } @@ -138,7 +136,7 @@ Item { fontSize : Style.main.fontSize textUnderline : true onClicked : { - Qt.openUrlExternally(go.releaseNotesLink) + Qt.openUrlExternally(go.updateReleaseNotesLink) } } } diff --git a/internal/frontend/qml/BridgeUI/MainWindow.qml b/internal/frontend/qml/BridgeUI/MainWindow.qml index 6006141e..c2012408 100644 --- a/internal/frontend/qml/BridgeUI/MainWindow.qml +++ b/internal/frontend/qml/BridgeUI/MainWindow.qml @@ -314,50 +314,7 @@ Window { DialogUpdate { id: dialogUpdate - - property string manualLinks : { - var out = "" - var links = go.downloadLink.split("\n") - var l; - for (l in links) { - out += '%1
'.arg(links[l]) - } - return out - } - - title: root.isOutdateVersion ? - qsTr("%1 is outdated", "title of outdate dialog").arg(go.programTitle): - qsTr("%1 update to %2", "title of update dialog").arg(go.programTitle).arg(go.newversion) - introductionText: { - if (root.isOutdateVersion) { - if (go.goos=="linux") { - return qsTr('You are using an outdated version of our software.
- Please download and install the latest version to continue using %1.

- %2', - "Message for force-update in Linux").arg(go.programTitle).arg(dialogUpdate.manualLinks) - } else { - return qsTr('You are using an outdated version of our software.
- Please download and install the latest version to continue using %1.

- You can continue with the update or download and install the new version manually from

- %2', - "Message for force-update in Win/Mac").arg(go.programTitle).arg(go.landingPage) - } - } else { - if (go.goos=="linux") { - return qsTr('A new version of Bridge is available.
- Check release notes to learn what is new in %2.
- Use your package manager to update or download and install the new version manually from

- %3', - "Message for update in Linux").arg(go.releaseNotesLink).arg(go.newversion).arg(dialogUpdate.manualLinks) - } else { - return qsTr('A new version of Bridge is available.
- Check release notes to learn what is new in %2.
- You can continue with the update or download and install the new version manually from

- %3', - "Message for update in Win/Mac").arg(go.releaseNotesLink).arg(go.newversion).arg(go.landingPage) - } - } - } + forceUpdate: root.isOutdateVersion } diff --git a/internal/frontend/qml/BridgeUI/SettingsView.qml b/internal/frontend/qml/BridgeUI/SettingsView.qml index dab29122..d5980644 100644 --- a/internal/frontend/qml/BridgeUI/SettingsView.qml +++ b/internal/frontend/qml/BridgeUI/SettingsView.qml @@ -97,6 +97,25 @@ Item { } } + ButtonIconText { + id: autoUpdates + text: qsTr("Keep the application up to date", "label for toggle that activates and disables the automatic updates") + leftIcon.text : Style.fa.download + rightIcon { + font.pointSize : Style.settings.toggleSize * Style.pt + text : go.isAutoUpdate!=false ? Style.fa.toggle_on : Style.fa.toggle_off + color : go.isAutoUpdate!=false ? Style.main.textBlue : Style.main.textDisabled + } + Accessible.description: ( + go.isAutoUpdate == false ? + qsTr("Enable" , "Click to enable the automatic update of Bridge") : + qsTr("Disable" , "Click to disable the automatic update of Bridge") + ) + " " + text + onClicked: { + go.toggleAutoUpdate() + } + } + ButtonIconText { id: advancedSettings property bool isAdvanced : !go.isDefaultPort diff --git a/internal/frontend/qml/Gui.qml b/internal/frontend/qml/Gui.qml index d57bfe7d..b1d80a33 100644 --- a/internal/frontend/qml/Gui.qml +++ b/internal/frontend/qml/Gui.qml @@ -54,13 +54,15 @@ Item { onWarningFlagsChanged : { if (gui.warningFlags==Style.okInfoBar) { go.normalSystray() - } else { - if ((gui.warningFlags & Style.errorInfoBar) == Style.errorInfoBar) { - go.errorSystray() - } else { - go.highlightSystray() - } + return } + + if ((gui.warningFlags & Style.errorInfoBar) == Style.errorInfoBar) { + go.errorSystray() + return + } + + go.highlightSystray() } // Signals from Go @@ -112,14 +114,6 @@ Item { } } - onRunCheckVersion : { - gui.openMainWindow(false) - go.setUpdateState("upToDate") - winMain.dialogGlobal.state="checkUpdates" - winMain.dialogGlobal.show() - go.isNewVersionAvailable(showMessage) - } - onSetUpdateState : { // once app is outdated prevent from state change if (winMain.updateState != "forceUpdate") { @@ -134,15 +128,50 @@ Item { go.silentBubble(2,qsTr("You have the latest version!", "notification", -1)) } - onNotifyUpdate : { + onNotifyManualUpdate: { + go.setUpdateState("oldVersion") + } + + onNotifyManualUpdateRestartNeeded: { + if (!winMain.dialogUpdate.visible) { + gui.openMainWindow(true) + winMain.dialogUpdate.show() + } + go.setUpdateState("updateRestart") + winMain.dialogUpdate.finished(false) + + // after manual update - just retart immidiatly + go.setToRestart() + Qt.quit() + } + + onNotifyManualUpdateError: { + if (!winMain.dialogUpdate.visible) { + gui.openMainWindow(true) + winMain.dialogUpdate.show() + } + go.setUpdateState("updateError") + winMain.dialogUpdate.finished(true) + } + + onNotifyForceUpdate : { go.setUpdateState("forceUpdate") if (!winMain.dialogUpdate.visible) { gui.openMainWindow(true) - go.runCheckVersion(false) winMain.dialogUpdate.show() } } + onNotifySilentUpdateRestartNeeded: { + go.setUpdateState("updateRestart") + gui.openMainWindow(true) + } + + onNotifySilentUpdateError: { + go.setUpdateState("updateError") + gui.openMainWindow(true) + } + onNotifyLogout : { go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Bridge with this account.").arg(accname) ) } @@ -229,10 +258,6 @@ Item { outgoingNoEncPopup.y = y } - onUpdateFinished : { - winMain.dialogUpdate.finished(hasError) - } - onShowCertIssue : { winMain.tlsBarState="notOK" } @@ -240,14 +265,6 @@ Item { } - Timer { - id: checkVersionTimer - repeat : true - triggeredOnStart: false - interval : Style.main.verCheckRepeatTime - onTriggered : go.runCheckVersion(false) - } - function openMainWindow(showAndRise) { // wait and check until font is loaded while(true){ @@ -301,10 +318,8 @@ Item { // start window gui.openMainWindow(false) - checkVersionTimer.start() if (go.isShownOnStart) { gui.winMain.showAndRise() } - go.runCheckVersion(false) } } diff --git a/internal/frontend/qml/GuiIE.qml b/internal/frontend/qml/GuiIE.qml index 1cc23ac1..309be54e 100644 --- a/internal/frontend/qml/GuiIE.qml +++ b/internal/frontend/qml/GuiIE.qml @@ -38,7 +38,7 @@ Item { property var allMonths : getMonthList(1,12) property var allDays : getDayList(1,31) - property var enums : JSON.parse('{"pathOK":1,"pathEmptyPath":2,"pathWrongPath":4,"pathNotADir":8,"pathWrongPermissions":16,"pathDirEmpty":32,"errUnknownError":0,"errEventAPILogout":1,"errUpdateAPI":2,"errUpdateJSON":3,"errUserAuth":4,"errQApplication":18,"errEmailExportFailed":6,"errEmailExportMissing":7,"errNothingToImport":8,"errEmailImportFailed":12,"errDraftImportFailed":13,"errDraftLabelFailed":14,"errEncryptMessageAttachment":15,"errEncryptMessage":16,"errNoInternetWhileImport":17,"errUnlockUser":5,"errSourceMessageNotSelected":19,"errCannotParseMail":5000,"errWrongLoginOrPassword":5001,"errWrongServerPathOrPort":5002,"errWrongAuthMethod":5003,"errIMAPFetchFailed":5004,"errLocalSourceLoadFailed":1000,"errPMLoadFailed":1001,"errRemoteSourceLoadFailed":1002,"errLoadAccountList":1005,"errExit":1006,"errRetry":1007,"errAsk":1008,"errImportFailed":1009,"errCreateLabelFailed":1010,"errCreateFolderFailed":1011,"errUpdateLabelFailed":1012,"errUpdateFolderFailed":1013,"errFillFolderName":1014,"errSelectFolderColor":1015,"errNoInternet":1016,"folderTypeSystem":"system","folderTypeLabel":"label","folderTypeFolder":"folder","folderTypeExternal":"external","progressInit":"init","progressLooping":"looping","statusNoInternet":"noInternet","statusCheckingInternet":"internetCheck","statusNewVersionAvailable":"oldVersion","statusUpToDate":"upToDate","statusForceUpdate":"forceupdate"}') + property var enums : JSON.parse('{"pathOK":1,"pathEmptyPath":2,"pathWrongPath":4,"pathNotADir":8,"pathWrongPermissions":16,"pathDirEmpty":32,"errUnknownError":0,"errEventAPILogout":1,"errUpdateAPI":2,"errUpdateJSON":3,"errUserAuth":4,"errQApplication":18,"errEmailExportFailed":6,"errEmailExportMissing":7,"errNothingToImport":8,"errEmailImportFailed":12,"errDraftImportFailed":13,"errDraftLabelFailed":14,"errEncryptMessageAttachment":15,"errEncryptMessage":16,"errNoInternetWhileImport":17,"errUnlockUser":5,"errSourceMessageNotSelected":19,"errCannotParseMail":5000,"errWrongLoginOrPassword":5001,"errWrongServerPathOrPort":5002,"errWrongAuthMethod":5003,"errIMAPFetchFailed":5004,"errLocalSourceLoadFailed":1000,"errPMLoadFailed":1001,"errRemoteSourceLoadFailed":1002,"errLoadAccountList":1005,"errExit":1006,"errRetry":1007,"errAsk":1008,"errImportFailed":1009,"errCreateLabelFailed":1010,"errCreateFolderFailed":1011,"errUpdateLabelFailed":1012,"errUpdateFolderFailed":1013,"errFillFolderName":1014,"errSelectFolderColor":1015,"errNoInternet":1016,"folderTypeSystem":"system","folderTypeLabel":"label","folderTypeFolder":"folder","folderTypeExternal":"external","progressInit":"init","progressLooping":"looping","statusNoInternet":"noInternet","statusCheckingInternet":"internetCheck","statusNewVersionAvailable":"oldVersion","statusUpToDate":"upToDate","statusForceUpdate":"forceUpdate"}') IEStyle{} @@ -103,16 +103,9 @@ Item { } } - onRunCheckVersion : { - go.setUpdateState(gui.enums.statusUpToDate) - winMain.dialogGlobal.state=gui.enums.statusCheckingInternet - winMain.dialogGlobal.show() - go.isNewVersionAvailable(showMessage) - } - onSetUpdateState : { // once app is outdated prevent from state change - if (winMain.updateState != gui.enums.statusForceUpdate) { + if (winMain.updateState != "forceUpdate") { winMain.updateState = updateState } } @@ -213,13 +206,43 @@ Item { } } - onNotifyUpdate : { - go.setUpdateState("forceUpdate") + onNotifyManualUpdate: { + go.setUpdateState("oldVersion") + } + + onNotifyManualUpdateRestartNeeded: { if (!winMain.dialogUpdate.visible) { - gui.openMainWindow(true) - go.runCheckVersion(false) winMain.dialogUpdate.show() } + go.setUpdateState("updateRestart") + winMain.dialogUpdate.finished(false) + + // after manual update - just retart immidiatly + go.setToRestart() + Qt.quit() + } + + onNotifyManualUpdateError: { + if (!winMain.dialogUpdate.visible) { + winMain.dialogUpdate.show() + } + go.setUpdateState("updateError") + winMain.dialogUpdate.finished(true) + } + + onNotifyForceUpdate : { + go.setUpdateState("forceUpdate") + if (!winMain.dialogUpdate.visible) { + winMain.dialogUpdate.show() + } + } + + onNotifySilentUpdateRestartNeeded: { + go.setUpdateState("updateRestart") + } + + onNotifySilentUpdateError: { + go.setUpdateState("updateError") } onNotifyLogout : { @@ -382,14 +405,6 @@ Item { } */ - Timer { - id: checkVersionTimer - repeat : true - triggeredOnStart: false - interval : Style.main.verCheckRepeatTime - onTriggered : go.runCheckVersion(false) - } - property string areYouSureYouWantToQuit : qsTr("There are incomplete processes - some items are not yet transferred. Do you really want to stop and quit?") // On start Component.onCompleted : { @@ -402,9 +417,6 @@ Item { go.bugNotSent = qsTr("Unable to submit bug report." , "notification", -1) go.bugReportSent = qsTr("Bug report successfully sent." , "notification", -1) - go.runCheckVersion(false) - checkVersionTimer.start() - gui.allMonths = getMonthList(1,12) gui.allMonthsChanged() } diff --git a/internal/frontend/qml/ImportExportUI/DialogYesNo.qml b/internal/frontend/qml/ImportExportUI/DialogYesNo.qml index f7abd3e5..164d4892 100644 --- a/internal/frontend/qml/ImportExportUI/DialogYesNo.qml +++ b/internal/frontend/qml/ImportExportUI/DialogYesNo.qml @@ -341,7 +341,7 @@ Dialog { if ( state == "toggleAutoStart" ) { go.toggleAutoStart () } if ( state == "quit" ) { Qt.quit () } if ( state == "instance exists" ) { Qt.quit () } - if ( state == "checkUpdates" ) { go.runCheckVersion (true) } + if ( state == "checkUpdates" ) { } } } diff --git a/internal/frontend/qml/ImportExportUI/HelpView.qml b/internal/frontend/qml/ImportExportUI/HelpView.qml index 47b7b076..6f06c2b5 100644 --- a/internal/frontend/qml/ImportExportUI/HelpView.qml +++ b/internal/frontend/qml/ImportExportUI/HelpView.qml @@ -55,9 +55,7 @@ Item { rightIcon.text : Style.fa.chevron_circle_right rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt onClicked: { - dialogGlobal.state="checkUpdates" - dialogGlobal.show() - dialogGlobal.confirmed() + go.checkForUpdates() } } @@ -130,7 +128,7 @@ Item { MouseArea { anchors.fill: parent onClicked : { - Qt.openUrlExternally(go.releaseNotesLink) + Qt.openUrlExternally(go.updateReleaseNotesLink) } cursorShape: Qt.PointingHandCursor } diff --git a/internal/frontend/qml/ImportExportUI/MainWindow.qml b/internal/frontend/qml/ImportExportUI/MainWindow.qml index 613c41c7..96cfa672 100644 --- a/internal/frontend/qml/ImportExportUI/MainWindow.qml +++ b/internal/frontend/qml/ImportExportUI/MainWindow.qml @@ -55,7 +55,7 @@ Window { minimumWidth : Style.main.width minimumHeight : Style.main.height - property bool isOutdateVersion : root.updateState == "forceUpgrade" + property bool isOutdateVersion : root.updateState == "forceUpdate" property bool activeContent : !dialogAddUser .visible && @@ -252,40 +252,7 @@ Window { DialogUpdate { id: dialogUpdate - - title: root.isOutdateVersion ? - qsTr("%1 is outdated", "title of outdate dialog").arg(go.programTitle): - qsTr("%1 update to %2", "title of update dialog").arg(go.programTitle).arg(go.newversion) - introductionText: { - if (root.isOutdateVersion) { - if (go.goos=="linux") { - return qsTr('You are using an outdated version of our software.
- Please dowload and install the latest version to continue using %1.

- %2', - "Message for force-update in Linux").arg(go.programTitle).arg(go.landingPage) - } else { - return qsTr('You are using an outdated version of our software.
- Please dowload and install the latest version to continue using %1.

- You can continue with update or download and install the new version manually from

- %2', - "Message for force-update in Win/Mac").arg(go.programTitle).arg(go.landingPage) - } - } else { - if (go.goos=="linux") { - return qsTr('A new version of %1 is available.
- Check release notes to learn what is new in %3.
- Use your package manager to update or download and install new the version manually from

- %4', - "Message for update in Linux").arg(go.programTitle).arg(go.releaseNotesLink).arg(go.newversion).arg(go.landingPage) - } else { - return qsTr('A new version of %1 is available.
- Check release notes to learn what is new in %3.
- You can continue with update or download and install new the version manually from

- %4', - "Message for update in Win/Mac").arg(go.programTitle).arg(go.releaseNotesLink).arg(go.newversion).arg(go.landingPage) - } - } - } + forceUpdate: root.isOutdateVersion } diff --git a/internal/frontend/qml/ImportExportUI/SettingsView.qml b/internal/frontend/qml/ImportExportUI/SettingsView.qml index 33d7dbdb..5e115fea 100644 --- a/internal/frontend/qml/ImportExportUI/SettingsView.qml +++ b/internal/frontend/qml/ImportExportUI/SettingsView.qml @@ -75,6 +75,25 @@ Item { onClicked: bugreportWin.show() } + ButtonIconText { + id: autoUpdates + text: qsTr("Keep the application up to date", "label for toggle that activates and disables the automatic updates") + leftIcon.text : Style.fa.download + rightIcon { + font.pointSize : Style.settings.toggleSize * Style.pt + text : go.isAutoUpdate!=false ? Style.fa.toggle_on : Style.fa.toggle_off + color : go.isAutoUpdate!=false ? Style.main.textBlue : Style.main.textDisabled + } + Accessible.description: ( + go.isAutoUpdate == false ? + qsTr("Enable" , "Click to enable the automatic update of Bridge") : + qsTr("Disable" , "Click to disable the automatic update of Bridge") + ) + " " + text + onClicked: { + go.toggleAutoUpdate() + } + } + /* ButtonIconText { diff --git a/internal/frontend/qml/ProtonUI/DialogUpdate.qml b/internal/frontend/qml/ProtonUI/DialogUpdate.qml index fc78c19b..3752ceec 100644 --- a/internal/frontend/qml/ProtonUI/DialogUpdate.qml +++ b/internal/frontend/qml/ProtonUI/DialogUpdate.qml @@ -25,16 +25,17 @@ import ProtonUI 1.0 Dialog { id: root - title: "Bridge update "+go.newversion - - property alias introductionText : introduction.text property bool hasError : false + property bool forceUpdate : false signal cancel() signal okay() + title: forceUpdate ? + qsTr("Update %1 now", "title of force update dialog").arg(go.programTitle): + qsTr("Update to %1 %2", "title of normal update dialog").arg(go.programTitle).arg(go.updateVersion) - isDialogBusy: currentIndex==1 + isDialogBusy: currentIndex==1 || forceUpdate Rectangle { // 0: Release notes and confirm width: parent.width @@ -51,18 +52,45 @@ Dialog { color: Style.dialog.text linkColor: Style.dialog.textBlue font { - pointSize: 0.8 * Style.dialog.fontSize * Style.pt + pointSize: Style.dialog.fontSize * Style.pt } width: 2*root.width/3 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap - // customize message per application - text: ' Release notes
New version %2


%3' + text: { + if (forceUpdate) { + if (go.updateCanInstall) { + return qsTr('You need to update this app to continue using it.
+ Update now or manually download the most recent version here:
+ %1
+ Learn why you need to update', + "Message for force-update").arg(go.updateLandingPage) + } else { + return qsTr('You need to update this app to continue using it.
+ Download the most recent version here:
+ %1
+ Learn why you need to update', + "Message for force-update").arg(go.updateLandingPage) + } + } + + if (go.updateCanInstall) { + return qsTr('Update to the newest version or download it from:
+ %1
+ View release notes', + "Message for manual update").arg(go.updateLandingPage).arg(go.updateReleaseNotesLink) + } else { + return qsTr('Update to the newest version from:
+ %1
+ View release notes', + "Message for manual update").arg(go.updateLandingPage).arg(go.updateReleaseNotesLink) + } + } + onLinkActivated : { console.log("clicked link:", link) - root.hide() Qt.openUrlExternally(link) } @@ -73,21 +101,30 @@ Dialog { } } + CheckBoxLabel { + id: autoUpdate + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Automatically update in the future", "Checkbox label for using autoupdates later on") + checked: go.isAutoUpdate + onToggled: go.toggleAutoUpdate() + visible: !root.forceUpdate + } + Row { anchors.horizontalCenter: parent.horizontalCenter spacing: Style.dialog.spacing ButtonRounded { fa_icon: Style.fa.times - text: (go.goos=="linux" ? qsTr("Okay") : qsTr("Cancel")) + text: root.forceUpdate ? qsTr("Quit") : qsTr("Cancel") color_main: Style.dialog.text - onClicked: root.cancel() + onClicked: root.forceUpdate ? Qt.quit() : root.cancel() } ButtonRounded { fa_icon: Style.fa.check text: qsTr("Update") - visible: go.goos!="linux" + visible: go.updateCanInstall color_main: Style.dialog.text color_minor: Style.main.textBlue isOpaque: true @@ -97,7 +134,7 @@ Dialog { } } - Rectangle { // 0: Check / download / unpack / prepare + Rectangle { // 1: Installing update id: updateStatus width: parent.width height: parent.height @@ -116,35 +153,29 @@ Dialog { width: 2*root.width/3 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap - text: { - switch (go.progressDescription) { - case "1": return qsTr("Checking the current version.") - case "2": return qsTr("Downloading the update files.") - case "3": return qsTr("Verifying the update files.") - case "4": return qsTr("Unpacking the update files.") - case "5": return qsTr("Starting the update.") - case "6": return qsTr("Quitting the application.") - default: return "" - } - } + text: qsTr("Updating...") } ProgressBar { - id: progressbar - implicitWidth : 2*updateStatus.width/3 - implicitHeight : Style.exporting.rowHeight - visible: go.progress!=0 // hack hide animation when clearing out progress bar - value: go.progress - property int current: go.total * go.progress - property bool isFinished: finishedPartBar.width == progressbar.width + id: updateProgressBar + width: 2*updateStatus.width/3 + height: Style.exporting.rowHeight + //implicitWidth : 2*updateStatus.width/3 + //implicitHeight : Style.exporting.rowHeight + indeterminate: true + //value: 0.5 + //property int current: go.total * go.progress + //property bool isFinished: finishedPartBar.width == progressbar.width background: Rectangle { radius : Style.exporting.boxRadius color : Style.exporting.progressBackground } + contentItem: Item { + clip: true Rectangle { - id: finishedPartBar - width : parent.width * progressbar.visualPosition + id: progressIndicator + width : updateProgressBar.indeterminate ? 50 : parent.width * updateProgressBar.visualPosition height : parent.height radius : Style.exporting.boxRadius gradient : Gradient { @@ -156,6 +187,27 @@ Dialog { Behavior on width { NumberAnimation { duration:300; easing.type: Easing.InOutQuad } } + + SequentialAnimation { + running: updateProgressBar.visible && updateProgressBar.indeterminate + loops: Animation.Infinite + + SmoothedAnimation { + target: progressIndicator + property: "x" + from: 0 + to: updateProgressBar.width - progressIndicator.width + duration: 2000 + } + + SmoothedAnimation { + target: progressIndicator + property: "x" + from: updateProgressBar.width - progressIndicator.width + to: 0 + duration: 2000 + } + } } Text { anchors.centerIn: parent @@ -170,7 +222,7 @@ Dialog { } } - Rectangle { // 1: Something went wrong / All ok, closing bridge + Rectangle { // 2: Something went wrong / All ok, closing bridge width: parent.width height: parent.height color: Style.transparent @@ -188,8 +240,8 @@ Dialog { width: 2*root.width/3 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap - text: !root.hasError ? qsTr('Application will quit now to finish the update.', "message after successful update") : - qsTr('The update procedure was not successful!
Please follow the download link and update manually.

%1').arg(go.downloadLink) + text: !root.hasError ? qsTr('%1 will restart now to finish the update.', "message after successful update").arg(go.programTitle) : + qsTr('The update procedure was not successful!
Please follow the download link and update manually.

%1').arg(go.updateLandingPage) onLinkActivated : { console.log("clicked link:", link) @@ -220,7 +272,7 @@ Dialog { function finished(hasError) { root.hasError = hasError - root.incrementCurrentIndex() + root.currentIndex = 2 } onShow: { @@ -234,9 +286,10 @@ Dialog { onOkay: { switch (root.currentIndex) { case 0: - go.startUpdate() + go.startManualUpdate() + root.currentIndex = 1 + break } - root.incrementCurrentIndex() } onCancel: { diff --git a/internal/frontend/qml/ProtonUI/InformationBar.qml b/internal/frontend/qml/ProtonUI/InformationBar.qml index 3e77463a..87271787 100644 --- a/internal/frontend/qml/ProtonUI/InformationBar.qml +++ b/internal/frontend/qml/ProtonUI/InformationBar.qml @@ -53,6 +53,7 @@ Rectangle { } Row { + id: messageRow anchors.centerIn: root visible: root.isVisible spacing: Style.main.leftMarginButton @@ -63,80 +64,74 @@ Rectangle { } ClickIconText { + id: linkText anchors.verticalCenter : message.verticalCenter - text : "("+go.newversion+" " + qsTr("release notes", "display the release notes from the new version")+")" - visible : root.state=="oldVersion" iconText : "" - onClicked : { - Qt.openUrlExternally(go.releaseNotesLink) - } fontSize : root.fontSize } ClickIconText { + id: actionText anchors.verticalCenter : message.verticalCenter - text : root.state=="oldVersion" || root.state == "forceUpdate" ? - qsTr("Update", "click to update to a new version when one is available") : - qsTr("Retry now", "click to try to connect to the internet when the app is disconnected from the internet") - visible : root.state!="internetCheck" iconText : "" - onClicked : { - if (root.state=="oldVersion" || root.state=="forceUpdate" ) { - winMain.dialogUpdate.show() - } else { - go.checkInternet() - } - } fontSize : root.fontSize textUnderline: true } Text { + id: separatorText anchors.baseline : message.baseline color: Style.main.text font { pointSize : root.fontSize * Style.pt bold : true } - visible: root.state=="oldVersion" || root.state=="noInternet" - text : "|" } ClickIconText { + id: action2Text anchors.verticalCenter : message.verticalCenter iconText : "" - text : root.state == "noInternet" ? - qsTr("Troubleshoot", "Show modal screen with additional tips for troubleshooting connection issues") : - qsTr("Remind me later", "Do not install new version and dismiss a notification") - visible : root.state=="oldVersion" || root.state=="noInternet" - onClicked : { - if (root.state == "oldVersion") { - root.state = "upToDate" - } - if (root.state == "noInternet") { - dialogConnectionTroubleshoot.show() - } - } fontSize : root.fontSize textUnderline: true } } + ClickIconText { + id: closeSign + anchors.verticalCenter : messageRow.verticalCenter + anchors.right: root.right + iconText : Style.fa.close + fontSize : root.fontSize + textUnderline: true + } + onStateChanged : { switch (root.state) { - case "forceUpdate" : - gui.warningFlags |= Style.errorInfoBar - break; - case "upToDate" : - gui.warningFlags &= ~Style.warnInfoBar - iTry = 0 - secLeft=checkInterval[iTry] - break; + case "internetCheck": + break; case "noInternet" : - gui.warningFlags |= Style.warnInfoBar - retryInternet.start() - secLeft=checkInterval[iTry] - break; + gui.warningFlags |= Style.warnInfoBar + retryInternet.start() + secLeft=checkInterval[iTry] + break; + case "oldVersion": + gui.warningFlags |= Style.warnInfoBar + break; + case "forceUpdate": + gui.warningFlags |= Style.errorInfoBar + break; + case "upToDate": + gui.warningFlags &= ~Style.warnInfoBar + iTry = 0 + secLeft=checkInterval[iTry] + break; + case "updateRestart": + gui.warningFlags |= Style.warnInfoBar + break; + case "updateError": + gui.warningFlags |= Style.errorInfoBar + break; default : - gui.warningFlags |= Style.warnInfoBar + break; } if (root.state!="noInternet") { @@ -172,6 +167,26 @@ Rectangle { color: Style.main.background text: qsTr("Checking connection. Please wait...", "displayed after user retries internet connection") } + PropertyChanges { + target: linkText + visible: false + } + PropertyChanges { + target: actionText + visible: false + } + PropertyChanges { + target: separatorText + visible: false + } + PropertyChanges { + target: action2Text + visible: false + } + PropertyChanges { + target: closeSign + visible: false + } }, State { name: "noInternet" @@ -186,6 +201,35 @@ Rectangle { color: Style.main.line text: qsTr("Cannot contact server. Retrying in ", "displayed when the app is disconnected from the internet or server has problems")+timeToRetry()+"." } + PropertyChanges { + target: linkText + visible: false + } + PropertyChanges { + target: actionText + visible: true + text: qsTr("Retry now", "click to try to connect to the internet when the app is disconnected from the internet") + onClicked: { + go.checkInternet() + } + } + PropertyChanges { + target: separatorText + visible: true + text: "|" + } + PropertyChanges { + target: action2Text + visible: true + text: qsTr("Troubleshoot", "Show modal screen with additional tips for troubleshooting connection issues") + onClicked: { + dialogConnectionTroubleshoot.show() + } + } + PropertyChanges { + target: closeSign + visible: false + } }, State { name: "oldVersion" @@ -198,7 +242,38 @@ Rectangle { PropertyChanges { target: message color: Style.main.background - text: qsTr("An update is available.", "displayed in a notification when an app update is available") + text: qsTr("Update available", "displayed in a notification when an app update is available") + } + PropertyChanges { + target: linkText + visible: true + text: "(" + qsTr("view release notes", "display the release notes from the new version") + ")" + onClicked: { + Qt.openUrlExternally(go.updateReleaseNotesLink) + } + } + PropertyChanges { + target: actionText + visible: true + text: qsTr("Update", "click to update to a new version when one is available") + onClicked: { + winMain.dialogUpdate.show() + } + } + PropertyChanges { + target: separatorText + visible: false + } + PropertyChanges { + target: action2Text + visible: false + } + PropertyChanges { + target: closeSign + visible: true + onClicked: { + root.state = "upToDate" + } } }, State { @@ -214,6 +289,30 @@ Rectangle { color: Style.main.line text: qsTr("%1 is outdated.", "displayed in a notification when app is outdated").arg(go.programTitle) } + PropertyChanges { + target: linkText + visible: false + } + PropertyChanges { + target: actionText + visible: true + text: qsTr("Update", "click to update to a new version when one is available") + onClicked: { + winMain.dialogUpdate.show() + } + } + PropertyChanges { + target: separatorText + visible: false + } + PropertyChanges { + target: action2Text + visible: false + } + PropertyChanges { + target: closeSign + visible: false + } }, State { name: "upToDate" @@ -228,6 +327,103 @@ Rectangle { color: Style.main.background text: "" } + PropertyChanges { + target: linkText + visible: false + } + PropertyChanges { + target: actionText + visible: false + } + PropertyChanges { + target: separatorText + visible: false + } + PropertyChanges { + target: action2Text + visible: false + } + PropertyChanges { + target: closeSign + visible: false + } + }, + State { + name: "updateRestart" + PropertyChanges { + target: root + height: 2* Style.main.fontSize + isVisible: true + color: Style.main.textBlue + } + PropertyChanges { + target: message + color: Style.main.background + text: qsTr("%1 update is ready", "displayed in a notification when an app update is installed and restart is needed").arg(go.programTitle) + } + PropertyChanges { + target: linkText + visible: false + } + PropertyChanges { + target: actionText + visible: true + text: qsTr("Restart now", "click to restart application as new version was installed") + onClicked: { + go.setToRestart() + Qt.quit() + } + } + PropertyChanges { + target: separatorText + visible: false + } + PropertyChanges { + target: action2Text + visible: false + } + PropertyChanges { + target: closeSign + visible: false + } + }, + State { + name: "updateError" + PropertyChanges { + target: root + height: 2* Style.main.fontSize + isVisible: true + color: Style.main.textRed + } + PropertyChanges { + target: message + color: Style.main.line + text: qsTr("Sorry, %1 couldn't update.", "displayed in a notification when app failed to autoupdate").arg(go.programTitle) + } + PropertyChanges { + target: linkText + visible: false + } + PropertyChanges { + target: actionText + visible: true + text: qsTr("Please update manually", "click to open download page to update manally") + onClicked: { + Qt.openUrlExternally(go.updateLandingPage) + } + } + PropertyChanges { + target: separatorText + visible: false + } + PropertyChanges { + target: action2Text + visible: false + } + PropertyChanges { + target: closeSign + visible: false + } } ] } diff --git a/internal/frontend/qml/ProtonUI/Style.qml b/internal/frontend/qml/ProtonUI/Style.qml index 2ae32db9..205fa905 100644 --- a/internal/frontend/qml/ProtonUI/Style.qml +++ b/internal/frontend/qml/ProtonUI/Style.qml @@ -59,7 +59,6 @@ QtObject { property real fontSize : 12 * px property real iconSize : 15 * px property real leftMarginButton : 9 * px - property real verCheckRepeatTime : 15*60*60*1000 // milliseconds property real topMargin : fontSize property real bottomMargin : fontSize property real border : 1 * px diff --git a/internal/frontend/qml/tst_Gui.qml b/internal/frontend/qml/tst_Gui.qml index 0afd738e..8790c96a 100644 --- a/internal/frontend/qml/tst_Gui.qml +++ b/internal/frontend/qml/tst_Gui.qml @@ -108,9 +108,14 @@ Window { ListElement { title: "Logout bridge" } ListElement { title: "Internet on" } ListElement { title: "Internet off" } - ListElement { title: "NeedUpdate" } ListElement { title: "UpToDate" } - ListElement { title: "ForceUpdate" } + ListElement { title: "NotifyManualUpdate(CanInstall)" } + ListElement { title: "NotifyManualUpdate(CantInstall)" } + ListElement { title: "NotifyManualUpdateRestart" } + ListElement { title: "NotifyManualUpdateError" } + ListElement { title: "ForceUpdate" } + ListElement { title: "NotifySilentUpdateRestartNeeded" } + ListElement { title: "NotifySilentUpdateError" } ListElement { title: "Linux" } ListElement { title: "Windows" } ListElement { title: "Macos" } @@ -196,12 +201,29 @@ Window { case "UpToDate" : testroot.newVersion = false break; - case "NeedUpdate" : - testroot.newVersion = true - break; + case "NotifyManualUpdate(CanInstall)" : + go.notifyManualUpdate() + go.updateCanInstall = true + break; + case "NotifyManualUpdate(CantInstall)" : + go.notifyManualUpdate() + go.updateCanInstall = false + break; + case "NotifyManualUpdateRestart": + go.notifyManualUpdateRestartNeeded() + break; + case "NotifyManualUpdateError": + go.notifyManualUpdateError() + break; case "ForceUpdate" : - go.notifyUpdate() - break; + go.notifyForceUpdate() + break; + case "NotifySilentUpdateRestartNeeded" : + go.notifySilentUpdateRestartNeeded() + break; + case "NotifySilentUpdateError" : + go.notifySilentUpdateError() + break; case "SendAlertPopup" : go.showOutgoingNoEncPopup("Alert sending unencrypted!") break; @@ -244,6 +266,7 @@ Window { id: go property bool isAutoStart : true + property bool isAutoUpdate : false property bool isProxyAllowed : false property bool isFirstStart : false property bool isFreshVersion : false @@ -269,17 +292,35 @@ Window { property string genericErrSeeLogs property string programTitle : "ProtonMail Bridge" - property string newversion : "QA.1.0" property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00" - property string landingPage : "https://landing.page" - //property string downloadLink: "https://landing.page/download/link" property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;" - property string releaseNotesLink : "https://protonmail.com/download/bridge/release_notes.html" + + property string updateVersion : "QA.1.0" + property bool updateCanInstall: true + property string updateLandingPage : "https://protonmail.com/bridge/download/" + property string updateReleaseNotesLink : "https://protonmail.com/download/bridge/release_notes.html" + signal notifyManualUpdate() + signal notifyManualUpdateRestartNeeded() + signal notifyManualUpdateError() + signal notifyForceUpdate() + signal notifySilentUpdateRestartNeeded() + signal notifySilentUpdateError() + function checkForUpdates() { + console.log("checkForUpdates") + } + function startManualUpdate() { + console.log("startManualUpdate") + } + + property string credits : "here;goes;list;;of;;used;packages;" property real progress: 0.3 property int progressDescription: 2 + function setToRestart() { + console.log("setting to restart") + } signal toggleMainWin(int systX, int systY, int systW, int systH) @@ -295,12 +336,11 @@ Window { signal processFinished() signal toggleAutoStart() + signal toggleAutoUpdate() signal notifyBubble(int tabIndex, string message) signal silentBubble(int tabIndex, string message) - signal runCheckVersion(bool showMessage) signal setAddAccountWarning(string message) - - signal notifyUpdate() + signal notifyFirewall() signal notifyLogout(string accname) signal notifyAddressChanged(string accname) @@ -463,9 +503,6 @@ Window { switch (timer.work) { case "wait": break - case "startUpdate": - go.animateProgressBar.start() - go.updateFinished(true) default: go.processFinished() } @@ -476,11 +513,6 @@ Window { timer.start() } - function startUpdate() { - timer.work="startUpdate" - timer.start() - } - function loadAccounts() { console.log("Test: Account loaded") } @@ -500,21 +532,7 @@ Window { } function getLocalVersionInfo(){ - go.newversion = "QA.1.0" - } - - function isNewVersionAvailable(showMessage){ - if (testroot.newVersion) { - go.newversion = "QA.2.0" - setUpdateState("oldVersion") - } else { - go.newversion = "QA.1.0" - setUpdateState("upToDate") - if(showMessage) { - notifyVersionIsTheLatest() - } - } - workAndClose() + go.updateVersion = "QA.1.0" } function getBackendVersion() { @@ -603,6 +621,12 @@ Window { isAutoStart = (isAutoStart!=false) ? false : true console.log (" Test: toggleAutoStart "+isAutoStart) } + + onToggleAutoUpdate: { + workAndClose() + isAutoUpdate = (isAutoUpdate!=false) ? false : true + console.log (" Test: onToggleAutoUpdate "+isAutoUpdate) + } } } diff --git a/internal/frontend/qml/tst_GuiIE.qml b/internal/frontend/qml/tst_GuiIE.qml index 8f9d1635..54094c68 100644 --- a/internal/frontend/qml/tst_GuiIE.qml +++ b/internal/frontend/qml/tst_GuiIE.qml @@ -98,24 +98,29 @@ Window { ListModel { id: buttons - ListElement { title : "Show window" } - ListElement { title : "Logout" } - ListElement { title : "Internet on" } - ListElement { title : "Internet off" } - ListElement { title : "Macos" } - ListElement { title : "Windows" } - ListElement { title : "Linux" } - ListElement { title : "New Version" } - ListElement { title : "ForceUpgrade" } - ListElement { title : "ImportStructure" } - ListElement { title : "DraftImpFailed" } - ListElement { title : "NoInterImp" } - ListElement { title : "ReportImp" } - ListElement { title : "NewFolder" } - ListElement { title : "EditFolder" } - ListElement { title : "EditLabel" } - ListElement { title : "ExpProgErr" } - ListElement { title : "ImpProgErr" } + ListElement { title : "Show window" } + ListElement { title : "Logout" } + ListElement { title : "Internet on" } + ListElement { title : "Internet off" } + ListElement { title : "Macos" } + ListElement { title : "Windows" } + ListElement { title : "Linux" } + ListElement { title: "NotifyManualUpdate(CanInstall)" } + ListElement { title: "NotifyManualUpdate(CantInstall)" } + ListElement { title: "NotifyManualUpdateRestart" } + ListElement { title: "NotifyManualUpdateError" } + ListElement { title: "ForceUpdate" } + ListElement { title: "NotifySilentUpdateRestartNeeded" } + ListElement { title: "NotifySilentUpdateError" } + ListElement { title : "ImportStructure" } + ListElement { title : "DraftImpFailed" } + ListElement { title : "NoInterImp" } + ListElement { title : "ReportImp" } + ListElement { title : "NewFolder" } + ListElement { title : "EditFolder" } + ListElement { title : "EditLabel" } + ListElement { title : "ExpProgErr" } + ListElement { title : "ImpProgErr" } } ListView { @@ -161,13 +166,29 @@ Window { case "Linux" : go.goos = "linux"; break; - case "New Version" : - testroot.newVersion = !testroot.newVersion - systrText.text = testroot.newVersion ? "new version" : "uptodate" - break - case "ForceUpgrade" : - go.notifyUpgrade() - break; + case "NotifyManualUpdate(CanInstall)" : + go.notifyManualUpdate() + go.updateCanInstall = true + break; + case "NotifyManualUpdate(CantInstall)" : + go.notifyManualUpdate() + go.updateCanInstall = false + break; + case "NotifyManualUpdateRestart": + go.notifyManualUpdateRestartNeeded() + break; + case "NotifyManualUpdateError": + go.notifyManualUpdateError() + break; + case "ForceUpdate" : + go.notifyForceUpdate() + break; + case "NotifySilentUpdateRestartNeeded" : + go.notifySilentUpdateRestartNeeded() + break; + case "NotifySilentUpdateError" : + go.notifySilentUpdateError() + break; case "ImportStructure" : testgui.winMain.dialogImport.address = "cuto@pm.com" testgui.winMain.dialogImport.show() @@ -815,6 +836,7 @@ Window { id: go property int isAutoStart : 1 + property bool isAutoUpdate : false property bool isFirstStart : false property string currentAddress : "none" //property string goos : "windows" @@ -831,9 +853,25 @@ Window { property string bugReportSent property string programTitle : "ProtonMail Import-Export app" - property string newversion : "q0.1.0" - property string landingPage : "https://landing.page" - property string releaseNotesLink : "https://protonmail.com/download/ie/release_notes.html" + property string fullversion : "QA.1.0 (d9f8sdf9) 2020-02-19T10:57:23+01:00" + property string downloadLink: "https://protonmail.com/download/beta/protonmail-bridge-1.1.5-1.x86_64.rpm;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;https://www.protonmail.com/downloads/beta/Desktop-Bridge-link1.exe;" + + property string updateVersion : "q0.1.0" + property bool updateCanInstall: true + property string updateLandingPage : "https://protonmail.com/import-export/download/" + property string updateReleaseNotesLink : "https://protonmail.com/download/ie/release_notes.html" + signal notifyManualUpdate() + signal notifyManualUpdateRestartNeeded() + signal notifyManualUpdateError() + signal notifyForceUpdate() + signal notifySilentUpdateRestartNeeded() + signal notifySilentUpdateError() + function checkForUpdates() { + console.log("checkForUpdates") + } + function startManualUpdate() { + console.log("startManualUpdate") + } property real progress: 0.0 property int progressFails: 0 @@ -846,13 +884,10 @@ Window { signal toggleMainWin(int systX, int systY, int systW, int systH) - - signal notifyHasNoKeychain() signal notifyKeychainRebuild() signal notifyAddressChangedLogout() signal notifyAddressChanged() - signal notifyUpdate() signal showWindow() signal showHelp() @@ -871,10 +906,10 @@ Window { signal processFinished() signal toggleAutoStart() + signal toggleAutoUpdate() signal notifyBubble(int tabIndex, string message) - signal runCheckVersion(bool showMessage) signal setAddAccountWarning(string message) - signal notifyUpgrade() + signal notifyUpdate() signal updateFinished(bool hasError) signal notifyLogout(string accname) @@ -882,6 +917,10 @@ Window { signal notifyError(int errCode) property string errorDescription : "" + function setToRestart() { + console.log("setting to restart") + } + function delay(duration) { var timeStart = new Date().getTime(); @@ -955,7 +994,7 @@ Window { workAndClose("addAccount") } - property SequentialAnimation animateProgressBarUpgrade : SequentialAnimation { + property SequentialAnimation animateProgressBarUpdate : SequentialAnimation { // version PropertyAnimation{ target: go; properties: "progressDescription"; to: 1; duration: 1; } PropertyAnimation{ duration: 2000; } @@ -1066,7 +1105,6 @@ Window { onTriggered : { console.log("triggered "+timer.work) switch (timer.work) { - case "isNewVersionAvailable" : case "clearCache" : case "clearKeychain" : case "logout" : @@ -1093,8 +1131,8 @@ Window { go.animateProgressBar.start() break; - case "startUpgrade": - go.animateProgressBarUpgrade.start() + case "startManualUpdate": + go.animateProgressBarUpdate.start() go.updateFinished(true) default: @@ -1105,18 +1143,10 @@ Window { function workAndClose(workDescription) { go.progress=0.0 - timer.work = workDescription + timer.work = workDescription === undefined ? "" : workDescription timer.start() } - function startUpgrade() { - timer.work="startUpgrade" - timer.start() - } - - - - function checkPathStatus(path) { if ( path == "" ) return testgui.enums.pathEmptyPath if ( path == "wrong" ) return testgui.enums.pathWrongPath @@ -1218,20 +1248,6 @@ Window { workAndClose("switchAddressMode") } - function isNewVersionAvailable(showMessage){ - if (testroot.newVersion) { - setUpdateState("oldVersion") - } else { - setUpdateState("upToDate") - if(showMessage) { - notifyVersionIsTheLatest() - } - } - workAndClose("isNewVersionAvailable") - //notifyBubble(2,go.versionCheckFailed) - return 0 - } - function getLocalVersionInfo(){} function getBackendVersion() { @@ -1328,5 +1344,11 @@ Window { console.log("sending import report from ", address, " file ", fname) return !fname.includes("fail") } + + onToggleAutoUpdate: { + workAndClose() + isAutoUpdate = (isAutoUpdate!=false) ? false : true + console.log (" Test: onToggleAutoUpdate "+isAutoUpdate) + } } } diff --git a/internal/frontend/qt-ie/frontend.go b/internal/frontend/qt-ie/frontend.go index 58c5c7ef..8fd908b6 100644 --- a/internal/frontend/qt-ie/frontend.go +++ b/internal/frontend/qt-ie/frontend.go @@ -23,6 +23,7 @@ import ( "errors" "os" + "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/events" qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common" "github.com/ProtonMail/proton-bridge/internal/frontend/types" @@ -50,6 +51,7 @@ var log = logrus.WithField("pkg", "frontend-qt-ie") type FrontendQt struct { panicHandler types.PanicHandler locations *locations.Locations + settings *settings.Settings eventListener listener.Listener updater types.Updater ie types.ImportExporter @@ -71,14 +73,17 @@ type FrontendQt struct { progress *transfer.Progress restarter types.Restarter + + // saving most up-to-date update info to install it manually + updateInfo updater.VersionInfo } // New is constructor for Import-Export Qt-Go interface func New( version, buildVersion string, panicHandler types.PanicHandler, - locations *locations.Locations, + settings *settings.Settings, eventListener listener.Listener, updater types.Updater, ie types.ImportExporter, @@ -87,6 +92,7 @@ func New( f := &FrontendQt{ panicHandler: panicHandler, locations: locations, + settings: settings, programName: "ProtonMail Import-Export", programVersion: "v" + version, eventListener: eventListener, @@ -111,9 +117,21 @@ func (f *FrontendQt) Loop() (err error) { return err } -func (f *FrontendQt) NotifyManualUpdate(update updater.VersionInfo) error { - // NOTE: Save the update somewhere so that it can be installed when user chooses "install now". - return nil +func (f *FrontendQt) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) { + f.Qml.SetUpdateVersion(update.Version.String()) + f.Qml.SetUpdateLandingPage(update.Landing) + f.Qml.SetUpdateReleaseNotesLink("https://protonmail.com/download/ie/release_notes.html") + f.Qml.SetUpdateCanInstall(canInstall) + f.updateInfo = update + f.Qml.NotifyManualUpdate() +} + +func (f *FrontendQt) NotifySilentUpdateInstalled() { + f.Qml.NotifySilentUpdateRestartNeeded() +} + +func (f *FrontendQt) NotifySilentUpdateError(err error) { + f.Qml.NotifySilentUpdateError() } func (f *FrontendQt) watchEvents() { @@ -152,7 +170,7 @@ func (f *FrontendQt) watchEvents() { f.Qml.NotifyLogout(user.Username()) case <-updateApplicationCh: f.Qml.ProcessFinished() - f.Qml.NotifyUpdate() + f.Qml.NotifyForceUpdate() case <-newUserCh: f.Qml.LoadAccounts() } @@ -219,6 +237,12 @@ func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error { f.Qml.SetCredits(importexport.Credits) f.Qml.SetFullversion(f.buildVersion) + if f.settings.GetBool(settings.AutoUpdateKey) { + f.Qml.SetIsAutoUpdate(true) + } else { + f.Qml.SetIsAutoUpdate(false) + } + // Loop if ret := gui.QGuiApplication_Exec(); ret != 0 { //err := errors.New(errors.ErrQApplication, "Event loop ended with return value: %v", string(ret)) @@ -299,6 +323,18 @@ func (f *FrontendQt) sendBug(description, emailClient, address string) bool { return true } +func (f *FrontendQt) toggleAutoUpdate() { + defer f.Qml.ProcessFinished() + + if f.settings.GetBool(settings.AutoUpdateKey) { + f.settings.SetBool(settings.AutoUpdateKey, false) + f.Qml.SetIsAutoUpdate(false) + } else { + f.settings.SetBool(settings.AutoUpdateKey, true) + f.Qml.SetIsAutoUpdate(true) + } +} + // checkInternet is almost idetical to bridge func (f *FrontendQt) checkInternet() { f.Qml.SetConnectionStatus(f.ie.CheckConnection() == nil) @@ -369,21 +405,43 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) { }() } -func (f *FrontendQt) StartUpdate() { - // NOTE: Fix this. +func (f *FrontendQt) startManualUpdate() { + go func() { + err := f.updater.InstallUpdate(f.updateInfo) + + if err != nil { + logrus.WithError(err).Error("An error occurred while installing updates manually") + f.Qml.NotifyManualUpdateError() + } + + f.Qml.NotifyManualUpdateRestartNeeded() + }() } -// isNewVersionAvailable is identical to bridge -// return 0 when local version is fine -// return 1 when new version is available -func (f *FrontendQt) isNewVersionAvailable(showMessage bool) { +func (f *FrontendQt) checkForUpdates() { go func() { - defer f.Qml.ProcessFinished() - f.Qml.SetConnectionStatus(true) // if we are here connection is ok - f.Qml.SetUpdateState(StatusUpToDate) - if showMessage { - f.Qml.NotifyVersionIsTheLatest() + version, err := f.updater.Check() + + if err != nil { + logrus.WithError(err).Error("An error occurred while checking updates manually") + f.Qml.NotifyManualUpdateError() + return } + + if !f.updater.IsUpdateApplicable(version) { + logrus.Debug("No need to update") + return + } + + logrus.WithField("version", version.Version).Info("An update is available") + + if !f.updater.CanInstall(version) { + logrus.Debug("A manual update is required") + f.NotifyManualUpdate(version, false) + return + } + + f.NotifyManualUpdate(version, true) }() } diff --git a/internal/frontend/qt-ie/frontend_nogui.go b/internal/frontend/qt-ie/frontend_nogui.go index aef4ba69..9d23be0a 100644 --- a/internal/frontend/qt-ie/frontend_nogui.go +++ b/internal/frontend/qt-ie/frontend_nogui.go @@ -23,6 +23,7 @@ import ( "fmt" "net/http" + "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/updater" @@ -42,15 +43,21 @@ func (s *FrontendHeadless) Loop() error { return http.ListenAndServe(":8082", nil) } -func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo) error { +func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) { // NOTE: Save the update somewhere so that it can be installed when user chooses "install now". - return nil +} + +func (s *FrontendHeadless) NotifySilentUpdateInstalled() { +} + +func (s *FrontendHeadless) NotifySilentUpdateError(err error) { } func New( version, buildVersion string, panicHandler types.PanicHandler, locations *locations.Locations, + settings *settings.Settings, eventListener listener.Listener, updater types.Updater, ie types.ImportExporter, diff --git a/internal/frontend/qt-ie/ui.go b/internal/frontend/qt-ie/ui.go index 0c435983..df08e2ec 100644 --- a/internal/frontend/qt-ie/ui.go +++ b/internal/frontend/qt-ie/ui.go @@ -33,6 +33,7 @@ type GoQMLInterface struct { _ func() `constructor:"init"` + _ bool `property:"isAutoUpdate"` _ string `property:"currentAddress"` _ string `property:"goos"` _ string `property:"credits"` @@ -49,11 +50,21 @@ type GoQMLInterface struct { _ string `property:importLogFileName` _ string `property:"programTitle"` - _ string `property:"newversion"` _ string `property:"fullversion"` _ string `property:"downloadLink"` - _ string `property:"landingPage"` - _ string `property:"releaseNotesLink"` + + _ string `property:"updateVersion"` + _ bool `property:"updateCanInstall"` + _ string `property:"updateLandingPage"` + _ string `property:"updateReleaseNotesLink"` + _ func() `signal:"notifyManualUpdate"` + _ func() `signal:"notifyManualUpdateRestartNeeded"` + _ func() `signal:"notifyManualUpdateError"` + _ func() `signal:"notifyForceUpdate"` + _ func() `signal:"notifySilentUpdateRestartNeeded"` + _ func() `signal:"notifySilentUpdateError"` + _ func() `slot:"checkForUpdates"` + _ func() `slot:"startManualUpdate"` // translations _ string `property:"wrongCredentials"` @@ -79,6 +90,7 @@ type GoQMLInterface struct { _ func() `signal:"showWindow"` + _ func() `slot:"toggleAutoUpdate"` _ func() `slot:"quit"` _ func() `slot:"loadAccounts"` _ func() `slot:"openLogs"` @@ -89,8 +101,7 @@ type GoQMLInterface struct { _ func() `signal:"highlightSystray"` _ func() `signal:"normalSystray"` - _ func(showMessage bool) `slot:"isNewVersionAvailable"` - _ func() string `slot:"getBackendVersion"` + _ func() string `slot:"getBackendVersion"` _ func(description, client, address string) bool `slot:"sendBug"` _ func(address string) bool `slot:"sendImportReport"` @@ -128,12 +139,10 @@ type GoQMLInterface struct { _ func() `signal:"notifyVersionIsTheLatest"` _ func() `signal:"notifyKeychainRebuild"` _ func() `signal:"notifyHasNoKeychain"` - _ func() `signal:"notifyUpdate"` _ func(accname string) `signal:"notifyLogout"` _ func(accname string) `signal:"notifyAddressChanged"` _ func(accname string) `signal:"notifyAddressChangedLogout"` - _ func() `slot:"startUpdate"` _ func(hasError bool) `signal:"updateFinished"` // errors @@ -150,6 +159,7 @@ func (s *GoQMLInterface) init() {} func (s *GoQMLInterface) SetFrontend(f *FrontendQt) { s.ConnectQuit(f.App.Quit) + s.ConnectToggleAutoUpdate(f.toggleAutoUpdate) s.ConnectLoadAccounts(f.Accounts.LoadAccounts) s.ConnectOpenLogs(f.openLogs) s.ConnectOpenDownloadLink(f.openDownloadLink) @@ -170,10 +180,10 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) { s.SetProgramTitle(f.programName) s.ConnectOpenLicenseFile(f.openLicenseFile) - s.SetReleaseNotesLink("https://protonmail.com/download/ie/release_notes.html") + s.SetUpdateReleaseNotesLink("https://protonmail.com/download/ie/release_notes.html") s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo) - s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable) + s.ConnectCheckForUpdates(f.checkForUpdates) s.ConnectGetBackendVersion(func() string { return f.programVersion }) @@ -193,7 +203,5 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) { s.ConnectCheckPathStatus(CheckPathStatus) - s.ConnectStartUpdate(f.StartUpdate) - s.ConnectEmitEvent(f.emitEvent) } diff --git a/internal/frontend/qt/frontend.go b/internal/frontend/qt/frontend.go index 7889b8d9..948afe76 100644 --- a/internal/frontend/qt/frontend.go +++ b/internal/frontend/qt/frontend.go @@ -95,6 +95,9 @@ type FrontendQt struct { userIDAdded string restarter types.Restarter + + // saving most up-to-date update info to install it manually + updateInfo updater.VersionInfo } // New returns a new Qt frontend for the bridge. @@ -173,9 +176,21 @@ func (s *FrontendQt) Loop() (err error) { return err } -func (s *FrontendQt) NotifyManualUpdate(update updater.VersionInfo) error { - // NOTE: Save the update somewhere so that it can be installed when user chooses "install now". - return nil +func (s *FrontendQt) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) { + s.Qml.SetUpdateVersion(update.Version.String()) + s.Qml.SetUpdateLandingPage(update.Landing) + s.Qml.SetUpdateReleaseNotesLink("https://protonmail.com/download/bridge/release_notes.html") + s.Qml.SetUpdateCanInstall(canInstall) + s.updateInfo = update + s.Qml.NotifyManualUpdate() +} + +func (s *FrontendQt) NotifySilentUpdateInstalled() { + s.Qml.NotifySilentUpdateRestartNeeded() +} + +func (s *FrontendQt) NotifySilentUpdateError(err error) { + s.Qml.NotifySilentUpdateError() } func (s *FrontendQt) watchEvents() { @@ -233,7 +248,7 @@ func (s *FrontendQt) watchEvents() { s.Qml.NotifyLogout(user.Username()) case <-updateApplicationCh: s.Qml.ProcessFinished() - s.Qml.NotifyUpdate() + s.Qml.NotifyForceUpdate() case <-newUserCh: s.Qml.LoadAccounts() case <-certIssue: @@ -343,6 +358,12 @@ func (s *FrontendQt) qtExecute(Procedure func(*FrontendQt) error) error { s.Qml.SetIsAutoStart(false) } + if s.settings.GetBool(settings.AutoUpdateKey) { + s.Qml.SetIsAutoUpdate(true) + } else { + s.Qml.SetIsAutoUpdate(false) + } + if s.settings.GetBool(settings.AllowProxyKey) { s.Qml.SetIsProxyAllowed(true) } else { @@ -397,16 +418,30 @@ func (s *FrontendQt) openLogs() { go open.Run(logsPath) } -// Check version in separate goroutine to not block the GUI (avoid program not responding message). -func (s *FrontendQt) isNewVersionAvailable(showMessage bool) { +func (s *FrontendQt) checkForUpdates() { go func() { - defer s.panicHandler.HandlePanic() - defer s.Qml.ProcessFinished() - s.Qml.SetConnectionStatus(true) // If we are here connection is ok. - s.Qml.SetUpdateState("upToDate") - if showMessage { - s.Qml.NotifyVersionIsTheLatest() + version, err := s.updater.Check() + + if err != nil { + logrus.WithError(err).Error("An error occurred while checking updates manually") + s.Qml.NotifyManualUpdateError() + return } + + if !s.updater.IsUpdateApplicable(version) { + logrus.Debug("No need to update") + return + } + + logrus.WithField("version", version.Version).Info("An update is available") + + if !s.updater.CanInstall(version) { + logrus.Debug("A manual update is required") + s.NotifyManualUpdate(version, false) + return + } + + s.NotifyManualUpdate(version, true) }() } @@ -501,6 +536,18 @@ func (s *FrontendQt) toggleAutoStart() { } } +func (s *FrontendQt) toggleAutoUpdate() { + defer s.Qml.ProcessFinished() + + if s.settings.GetBool(settings.AutoUpdateKey) { + s.settings.SetBool(settings.AutoUpdateKey, false) + s.Qml.SetIsAutoUpdate(false) + } else { + s.settings.SetBool(settings.AutoUpdateKey, true) + s.Qml.SetIsAutoUpdate(true) + } +} + func (s *FrontendQt) toggleAllowProxy() { defer s.Qml.ProcessFinished() @@ -594,6 +641,15 @@ func (s *FrontendQt) saveOutgoingNoEncPopupCoord(x, y float32) { //prefs.SetFloat(prefs.OutgoingNoEncPopupCoordY, y) } -func (s *FrontendQt) StartUpdate() { - // NOTE: Fix this. +func (s *FrontendQt) startManualUpdate() { + go func() { + err := s.updater.InstallUpdate(s.updateInfo) + + if err != nil { + logrus.WithError(err).Error("An error occurred while installing updates manually") + s.Qml.NotifyManualUpdateError() + } + + s.Qml.NotifyManualUpdateRestartNeeded() + }() } diff --git a/internal/frontend/qt/frontend_nogui.go b/internal/frontend/qt/frontend_nogui.go index 127553a4..ff94b0a3 100644 --- a/internal/frontend/qt/frontend_nogui.go +++ b/internal/frontend/qt/frontend_nogui.go @@ -43,9 +43,14 @@ func (s *FrontendHeadless) Loop() error { return http.ListenAndServe(":8081", nil) } -func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo) error { +func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) { // NOTE: Save the update somewhere so that it can be installed when user chooses "install now". - return nil +} + +func (s *FrontendHeadless) NotifySilentUpdateInstalled() { +} + +func (s *FrontendHeadless) NotifySilentUpdateError(err error) { } func (s *FrontendHeadless) InstanceExistAlert() {} diff --git a/internal/frontend/qt/ui.go b/internal/frontend/qt/ui.go index 6d3e5bfc..c1115078 100644 --- a/internal/frontend/qt/ui.go +++ b/internal/frontend/qt/ui.go @@ -34,6 +34,7 @@ type GoQMLInterface struct { _ func() `constructor:"init"` _ bool `property:"isAutoStart"` + _ bool `property:"isAutoUpdate"` _ bool `property:"isProxyAllowed"` _ string `property:"currentAddress"` _ string `property:"goos"` @@ -45,11 +46,21 @@ type GoQMLInterface struct { _ bool `property:"isDefaultPort"` _ string `property:"programTitle"` - _ string `property:"newversion"` _ string `property:"fullversion"` _ string `property:"downloadLink"` - _ string `property:"landingPage"` - _ string `property:"releaseNotesLink"` + + _ string `property:"updateVersion"` + _ bool `property:"updateCanInstall"` + _ string `property:"updateLandingPage"` + _ string `property:"updateReleaseNotesLink"` + _ func() `signal:"notifyManualUpdate"` + _ func() `signal:"notifyManualUpdateRestartNeeded"` + _ func() `signal:"notifyManualUpdateError"` + _ func() `signal:"notifyForceUpdate"` + _ func() `signal:"notifySilentUpdateRestartNeeded"` + _ func() `signal:"notifySilentUpdateError"` + _ func() `slot:"checkForUpdates"` + _ func() `slot:"startManualUpdate"` // Translations. _ string `property:"wrongCredentials"` @@ -82,6 +93,7 @@ type GoQMLInterface struct { _ func() `signal:"showQuit"` _ func() `slot:"toggleAutoStart"` + _ func() `slot:"toggleAutoUpdate"` _ func() `slot:"toggleAllowProxy"` _ func() `slot:"loadAccounts"` _ func() `slot:"openLogs"` @@ -121,7 +133,6 @@ type GoQMLInterface struct { _ func() `signal:"notifyVersionIsTheLatest"` _ func() `signal:"notifyKeychainRebuild"` _ func() `signal:"notifyHasNoKeychain"` - _ func() `signal:"notifyUpdate"` _ func(accname string) `signal:"notifyLogout"` _ func(accname string) `signal:"notifyAddressChanged"` _ func(accname string) `signal:"notifyAddressChangedLogout"` @@ -137,7 +148,6 @@ type GoQMLInterface struct { _ func(recipient string) `signal:"showNoActiveKeyForRecipient"` _ func() `signal:"showCertIssue"` - _ func() `slot:"startUpdate"` _ func(hasError bool) `signal:"updateFinished"` } @@ -147,15 +157,16 @@ func (s *GoQMLInterface) init() {} // SetFrontend connects all slots and signals from Go to QML. func (s *GoQMLInterface) SetFrontend(f *FrontendQt) { s.ConnectToggleAutoStart(f.toggleAutoStart) + s.ConnectToggleAutoUpdate(f.toggleAutoUpdate) s.ConnectToggleAllowProxy(f.toggleAllowProxy) s.ConnectLoadAccounts(f.loadAccounts) s.ConnectOpenLogs(f.openLogs) s.ConnectClearCache(f.clearCache) s.ConnectClearKeychain(f.clearKeychain) - s.ConnectOpenLicenseFile(f.openLicenseFile) + s.ConnectStartManualUpdate(f.startManualUpdate) s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo) - s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable) + s.ConnectCheckForUpdates(f.checkForUpdates) s.ConnectGetIMAPPort(f.getIMAPPort) s.ConnectGetSMTPPort(f.getSMTPPort) s.ConnectGetLastMailClient(f.getLastMailClient) @@ -180,8 +191,6 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) { s.SetGoos(runtime.GOOS) s.SetProgramTitle(f.programName) - s.SetReleaseNotesLink("https://protonmail.com/download/bridge/release_notes.html") - s.ConnectGetBackendVersion(func() string { return f.programVer }) @@ -193,5 +202,4 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) { s.ConnectToggleIsReportingOutgoingNoEnc(f.toggleIsReportingOutgoingNoEnc) s.ConnectShouldSendAnswer(f.shouldSendAnswer) s.ConnectSaveOutgoingNoEncPopupCoord(f.saveOutgoingNoEncPopupCoord) - s.ConnectStartUpdate(f.StartUpdate) } diff --git a/internal/frontend/types/types.go b/internal/frontend/types/types.go index 5da0522f..24158812 100644 --- a/internal/frontend/types/types.go +++ b/internal/frontend/types/types.go @@ -41,7 +41,10 @@ type NoEncConfirmator interface { } type Updater interface { + Check() (updater.VersionInfo, error) InstallUpdate(updater.VersionInfo) error + IsUpdateApplicable(updater.VersionInfo) bool + CanInstall(updater.VersionInfo) bool } // UserManager is an interface of users needed by frontend. diff --git a/internal/updater/install_darwin.go b/internal/updater/install_darwin.go index daf0e9ec..779d4ca6 100644 --- a/internal/updater/install_darwin.go +++ b/internal/updater/install_darwin.go @@ -30,13 +30,13 @@ import ( "github.com/pkg/errors" ) -type Installer struct{} +type InstallerDarwin struct{} -func NewInstaller(*versioner.Versioner) *Installer { - return &Installer{} +func NewInstaller(*versioner.Versioner) *InstallerDarwin { + return &InstallerDarwin{} } -func (i *Installer) InstallUpdate(_ *semver.Version, r io.Reader) error { +func (i *InstallerDarwin) InstallUpdate(_ *semver.Version, r io.Reader) error { gr, err := gzip.NewReader(r) if err != nil { return err diff --git a/internal/updater/install_default.go b/internal/updater/install_default.go index 111a6294..b3f3ba54 100644 --- a/internal/updater/install_default.go +++ b/internal/updater/install_default.go @@ -26,16 +26,16 @@ import ( "github.com/ProtonMail/proton-bridge/internal/versioner" ) -type Installer struct { +type InstallerDefault struct { versioner *versioner.Versioner } -func NewInstaller(versioner *versioner.Versioner) *Installer { - return &Installer{ +func NewInstaller(versioner *versioner.Versioner) *InstallerDefault { + return &InstallerDefault{ versioner: versioner, } } -func (i *Installer) InstallUpdate(version *semver.Version, r io.Reader) error { +func (i *InstallerDefault) InstallUpdate(version *semver.Version, r io.Reader) error { return i.versioner.InstallNewVersion(version, r) } diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 045623ba..aa85de3c 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -20,7 +20,6 @@ package updater import ( "encoding/json" "io" - "time" "github.com/Masterminds/semver/v3" "github.com/ProtonMail/gopenpgp/v2/crypto" @@ -29,17 +28,19 @@ import ( "github.com/sirupsen/logrus" ) -type clientProvider interface { +var ErrManualUpdateRequired = errors.New("manual update is required") + +type ClientProvider interface { GetAnonymousClient() pmapi.Client } -type installer interface { +type Installer interface { InstallUpdate(*semver.Version, io.Reader) error } type Updater struct { - cm clientProvider - installer installer + cm ClientProvider + installer Installer kr *crypto.KeyRing curVer *semver.Version @@ -51,8 +52,8 @@ type Updater struct { } func New( - cm clientProvider, - installer installer, + cm ClientProvider, + installer Installer, kr *crypto.KeyRing, curVer *semver.Version, updateURLName, platform string, @@ -70,56 +71,44 @@ func New( } } -func (u *Updater) Watch( - period time.Duration, - handleUpdate func(VersionInfo) error, - handleError func(error), -) func() { - logrus.WithField("period", period).Info("Watching for updates") - - ticker := time.NewTicker(period) - - go func() { - for { - u.watch(handleUpdate, handleError) - <-ticker.C - } - }() - - return ticker.Stop -} - -func (u *Updater) watch( - handleUpdate func(VersionInfo) error, - handleError func(error), -) { +func (u *Updater) Check() (VersionInfo, error) { logrus.Info("Checking for updates") - latest, err := u.fetchVersionInfo() + client := u.cm.GetAnonymousClient() + defer client.Logout() + + r, err := client.DownloadAndVerify( + u.getVersionFileURL(), + u.getVersionFileURL()+".sig", + u.kr, + ) if err != nil { - handleError(errors.Wrap(err, "failed to fetch version info")) - return + return VersionInfo{}, err } - if !latest.Version.GreaterThan(u.curVer) || u.rollout > latest.Rollout { - logrus.WithError(err).Debug("No need to update") - return + var versionMap VersionMap + + if err := json.NewDecoder(r).Decode(&versionMap); err != nil { + return VersionInfo{}, err } - if u.curVer.LessThan(latest.MinAuto) { - logrus.Debug("A manual update is required") - // NOTE: Need to notify user that they must update manually. - return + return versionMap[Channel], nil +} + +func (u *Updater) IsUpdateApplicable(version VersionInfo) bool { + if !version.Version.GreaterThan(u.curVer) { + return false } - logrus. - WithField("latest", latest.Version). - WithField("current", u.curVer). - Info("An update is available") - - if err := handleUpdate(latest); err != nil { - handleError(errors.Wrap(err, "failed to handle update")) + if u.rollout > version.Rollout { + return false } + + return true +} + +func (u *Updater) CanInstall(version VersionInfo) bool { + return !u.curVer.LessThan(version.MinAuto) } func (u *Updater) InstallUpdate(update VersionInfo) error { @@ -143,25 +132,3 @@ func (u *Updater) InstallUpdate(update VersionInfo) error { return nil }) } - -func (u *Updater) fetchVersionInfo() (VersionInfo, error) { - client := u.cm.GetAnonymousClient() - defer client.Logout() - - r, err := client.DownloadAndVerify( - u.getVersionFileURL(), - u.getVersionFileURL()+".sig", - u.kr, - ) - if err != nil { - return VersionInfo{}, err - } - - var versionMap VersionMap - - if err := json.NewDecoder(r).Decode(&versionMap); err != nil { - return VersionInfo{}, err - } - - return versionMap[Channel], nil -} diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index ee2cec8d..e0dc329c 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -34,13 +34,13 @@ import ( "github.com/stretchr/testify/require" ) -func TestWatch(t *testing.T) { +func TestCheck(t *testing.T) { c := gomock.NewController(t) defer c.Finish() client := mocks.NewMockClient(c) - updater := newTestUpdater(client, "1.4.0") + updater := newTestUpdater(client, "1.1.0") versionMap := VersionMap{ "live": VersionInfo{ @@ -59,119 +59,19 @@ func TestWatch(t *testing.T) { client.EXPECT().Logout() - updateCh := make(chan VersionInfo) + version, err := updater.Check() - defer updater.Watch( - time.Minute, - func(update VersionInfo) error { - updateCh <- update - return nil - }, - func(err error) { - t.Fatal(err) - }, - )() - - assert.Equal(t, semver.MustParse("1.5.0"), (<-updateCh).Version) + assert.Equal(t, semver.MustParse("1.5.0"), version.Version) + assert.NoError(t, err) } -func TestWatchIgnoresCurrentVersion(t *testing.T) { +func TestCheckBadSignature(t *testing.T) { c := gomock.NewController(t) defer c.Finish() client := mocks.NewMockClient(c) - updater := newTestUpdater(client, "1.5.0") - - versionMap := VersionMap{ - "live": VersionInfo{ - Version: semver.MustParse("1.5.0"), - MinAuto: semver.MustParse("1.4.0"), - Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", - Rollout: 1.0, - }, - } - - client.EXPECT().DownloadAndVerify( - updater.getVersionFileURL(), - updater.getVersionFileURL()+".sig", - gomock.Any(), - ).Return(bytes.NewReader(mustMarshal(t, versionMap)), nil) - - client.EXPECT().Logout() - - updateCh := make(chan VersionInfo) - - defer updater.Watch( - time.Minute, - func(update VersionInfo) error { - updateCh <- update - return nil - }, - func(err error) { - t.Fatal(err) - }, - )() - - select { - case <-updateCh: - t.Fatal("We shouldn't update because we are already up to date") - case <-time.After(1500 * time.Millisecond): - } -} - -func TestWatchIgnoresVerionsThatRequireManualUpdate(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - client := mocks.NewMockClient(c) - - updater := newTestUpdater(client, "1.4.0") - - versionMap := VersionMap{ - "live": VersionInfo{ - Version: semver.MustParse("1.5.0"), - MinAuto: semver.MustParse("1.5.0"), - Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", - Rollout: 1.0, - }, - } - - client.EXPECT().DownloadAndVerify( - updater.getVersionFileURL(), - updater.getVersionFileURL()+".sig", - gomock.Any(), - ).Return(bytes.NewReader(mustMarshal(t, versionMap)), nil) - - client.EXPECT().Logout() - - updateCh := make(chan VersionInfo) - - defer updater.Watch( - time.Minute, - func(update VersionInfo) error { - updateCh <- update - return nil - }, - func(err error) { - t.Fatal(err) - }, - )() - - select { - case <-updateCh: - t.Fatal("We shouldn't update because this version requires a manual update") - case <-time.After(1500 * time.Millisecond): - } -} - -func TestWatchBadSignature(t *testing.T) { - c := gomock.NewController(t) - defer c.Finish() - - client := mocks.NewMockClient(c) - - updater := newTestUpdater(client, "1.4.0") + updater := newTestUpdater(client, "1.2.0") client.EXPECT().DownloadAndVerify( updater.getVersionFileURL(), @@ -181,21 +81,72 @@ func TestWatchBadSignature(t *testing.T) { client.EXPECT().Logout() - updateCh := make(chan VersionInfo) - errorsCh := make(chan error) + _, err := updater.Check() - defer updater.Watch( - time.Minute, - func(update VersionInfo) error { - updateCh <- update - return nil - }, - func(err error) { - errorsCh <- err - }, - )() + assert.Error(t, err) +} - assert.Error(t, <-errorsCh) +func TestIsUpdateApplicable(t *testing.T) { + c := gomock.NewController(t) + defer c.Finish() + + client := mocks.NewMockClient(c) + + updater := newTestUpdater(client, "1.4.0") + + versionOld := VersionInfo{ + Version: semver.MustParse("1.3.0"), + MinAuto: semver.MustParse("1.3.0"), + Package: "https://protonmail.com/download/bridge/update_1.3.0_linux.tgz", + Rollout: 1.0, + } + + assert.Equal(t, false, updater.IsUpdateApplicable(versionOld)) + + versionEqual := VersionInfo{ + Version: semver.MustParse("1.4.0"), + MinAuto: semver.MustParse("1.3.0"), + Package: "https://protonmail.com/download/bridge/update_1.4.0_linux.tgz", + Rollout: 1.0, + } + + assert.Equal(t, false, updater.IsUpdateApplicable(versionEqual)) + + versionNew := VersionInfo{ + Version: semver.MustParse("1.5.0"), + MinAuto: semver.MustParse("1.3.0"), + Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", + Rollout: 1.0, + } + + assert.Equal(t, true, updater.IsUpdateApplicable(versionNew)) +} + +func TestCanInstall(t *testing.T) { + c := gomock.NewController(t) + defer c.Finish() + + client := mocks.NewMockClient(c) + + updater := newTestUpdater(client, "1.4.0") + + versionManual := VersionInfo{ + Version: semver.MustParse("1.5.0"), + MinAuto: semver.MustParse("1.5.0"), + Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", + Rollout: 1.0, + } + + assert.Equal(t, false, updater.CanInstall(versionManual)) + + versionAuto := VersionInfo{ + Version: semver.MustParse("1.5.0"), + MinAuto: semver.MustParse("1.3.0"), + Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz", + Rollout: 1.0, + } + + assert.Equal(t, true, updater.CanInstall(versionAuto)) } func TestInstallUpdate(t *testing.T) { @@ -221,7 +172,9 @@ func TestInstallUpdate(t *testing.T) { client.EXPECT().Logout() - assert.NoError(t, updater.InstallUpdate(latestVersion)) + err := updater.InstallUpdate(latestVersion) + + assert.NoError(t, err) } func TestInstallUpdateBadSignature(t *testing.T) { @@ -247,7 +200,9 @@ func TestInstallUpdateBadSignature(t *testing.T) { client.EXPECT().Logout() - assert.Error(t, updater.InstallUpdate(latestVersion)) + err := updater.InstallUpdate(latestVersion) + + assert.Error(t, err) } func TestInstallUpdateAlreadyOngoing(t *testing.T) { diff --git a/unreleased.md b/unreleased.md index 0eff2053..aaa23c8c 100644 --- a/unreleased.md +++ b/unreleased.md @@ -5,6 +5,22 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) ## Unreleased ### Added +* GODT-906 Handle RFC2047-encoded content transfer encoding values. + +* GODT-875 Added GUI dialog on force update. +* GODT-820 Added GUI notification on impossibility of update installation (both silent and manual). +* GODT-870 Added GUI notification on error during silent update. +* GODT-805 Added GUI notification on update available. +* GODT-804 Added GUI notification on silent update installed (promt to restart). +* GODT-275 Added option to disable autoupdates in settings (default autoupdate is enabled). +* GODT-874 Added manual triggers to Updater module. + +### Changed +* GODT-893 Bump go-rfc5322 dependency to v0.2.1 to properly detect syntax errors during parsing. +* GODT-892 Swap type and value from sentry exception and cut panic handlers from the traceback. +* GODT-854 EXPUNGE and FETCH unilateral responses are returned before OK EXPUNGE or OK STORE, respectively. + +* GODT-806 Changed GUI dialog on manual update. Added autoupdates checkbox. Simplifyed installation process GUI. ### Removed