From 298493169ca81da714f138f99a5c7052b40cb0a3 Mon Sep 17 00:00:00 2001
From: kkostiuk <kateryna.kostiuk@savoirfairelinux.com>
Date: Tue, 25 Jan 2022 11:56:34 -0500
Subject: [PATCH] updater: add sparkle

- add sparkle submodule
- add an option to enable sparkle
- modify entitlements
- cleanup Info.plist

Gitlab: #578
Change-Id: I7f562112a72a33e008ab316479fbaa68dc0e07f1
---
 .gitmodules                                   |   4 +
 CMakeLists.txt                                |  71 +++--
 cmake/macos_qt_deploy.cmake                   |   8 +-
 resources/Info.plist                          |  68 ++---
 resources/entitlements/Jami.entitlements      |   6 +-
 sparkle/Sparkle                               |   1 +
 sparkle/dsa_pub.pem                           |  36 +++
 sparkle/sign_update.sh                        |  11 +
 sparkle/sparkle-xml-updater.sh                |  46 +++
 src/MainApplicationWindow.qml                 |  12 +-
 src/{ => os/macos}/connectivitymonitor.mm     |   0
 src/os/macos/updatemanager.mm                 | 132 +++++++++
 .../components/GeneralSettingsPage.qml        |   3 +-
 .../components/UpdateSettings.qml             |   6 +-
 src/updatemanager.cpp                         | 268 +++++++++++-------
 src/updatemanager.h                           |  20 +-
 16 files changed, 502 insertions(+), 190 deletions(-)
 create mode 160000 sparkle/Sparkle
 create mode 100644 sparkle/dsa_pub.pem
 create mode 100755 sparkle/sign_update.sh
 create mode 100755 sparkle/sparkle-xml-updater.sh
 rename src/{ => os/macos}/connectivitymonitor.mm (100%)
 create mode 100644 src/os/macos/updatemanager.mm

diff --git a/.gitmodules b/.gitmodules
index 971ddabe0..2cb2b72ee 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -2,3 +2,7 @@
 	path = 3rdparty/qrencode-win32
 	url = https://github.com/BlueDragon747/qrencode-win32.git
 	ignore = dirty
+[submodule "sparkle/Sparkle"]
+        path = sparkle/Sparkle
+        url = https://github.com/sparkle-project/Sparkle.git
+        ignore = dirty
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b9d94f24b..ca3b06c7b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -98,7 +98,6 @@ set(COMMON_SOURCES
     ${SRC_DIR}/accountlistmodel.cpp
     ${SRC_DIR}/networkmanager.cpp
     ${SRC_DIR}/instancemanager.cpp
-    ${SRC_DIR}/updatemanager.cpp
     ${SRC_DIR}/main.cpp
     ${SRC_DIR}/smartlistmodel.cpp
     ${SRC_DIR}/utils.cpp
@@ -214,7 +213,9 @@ if(MSVC)
     )
 
     list(APPEND COMMON_SOURCES
-                ${SRC_DIR}/connectivitymonitor.cpp)
+                ${SRC_DIR}/connectivitymonitor.cpp
+                ${SRC_DIR}/updatemanager.cpp
+    )
     # preprocessor defines
     add_definitions(-DUNICODE -DQT_NO_DEBUG -DNDEBUG)
 
@@ -269,7 +270,8 @@ elseif (NOT APPLE)
     list(APPEND COMMON_SOURCES
                 ${SRC_DIR}/xrectsel.c
                 ${SRC_DIR}/dbuserrorhandler.cpp
-                ${SRC_DIR}/connectivitymonitor.cpp)
+                ${SRC_DIR}/connectivitymonitor.cpp
+                ${SRC_DIR}/updatemanager.cpp)
     list(APPEND COMMON_HEADERS
                 ${SRC_DIR}/dbuserrorhandler.h
                 ${SRC_DIR}/xrectsel.h)
@@ -348,10 +350,10 @@ elseif (NOT APPLE)
     find_library(ringclient ringclient ${LRCLIBDIR} NO_DEFAULT_PATH)
     find_library(qrencode qrencode)
     find_library(X11 X11)
-else()
+else() # APPLE
     list(APPEND COMMON_SOURCES
-                ${SRC_DIR}/connectivitymonitor.mm)
-    find_package(PkgConfig REQUIRED)
+                ${SRC_DIR}/os/macos/updatemanager.mm
+                ${SRC_DIR}/os/macos/connectivitymonitor.mm)
     if(NOT DEFINED LRC)
         if(EXISTS ${PROJECT_SOURCE_DIR}/../install/lrc)
             set(LRC ${PROJECT_SOURCE_DIR}/../install/lrc)
@@ -388,9 +390,24 @@ else()
 
     find_library(ringclient ringclient ${LRCLIBDIR} NO_DEFAULT_PATH)
     find_library(SYSTEM_CONFIGURATUION SystemConfiguration)
-    SET(myApp_ICON ${CMAKE_CURRENT_SOURCE_DIR}/resources/images/jami.icns)
-    SET_SOURCE_FILES_PROPERTIES(${myApp_ICON} PROPERTIES
+    set(myApp_ICON ${CMAKE_CURRENT_SOURCE_DIR}/resources/images/jami.icns)
+    set_source_files_properties(${myApp_ICON} PROPERTIES
            MACOSX_PACKAGE_LOCATION Resources)
+    if(ENABLE_SPARKLE)
+        message("Sparkle auto-update enabled")
+        find_library(SPARKLE_FRAMEWORK
+                     NAMES Sparkle
+                     HINTS ${CMAKE_CURRENT_SOURCE_DIR}/sparkle)
+        add_definitions(-DENABLE_SPARKLE)
+        message("Sparkle is here:" ${SPARKLE_FRAMEWORK})
+        set(PUBLIC_KEY_PATH "${CMAKE_CURRENT_SOURCE_DIR}/sparkle/dsa_pub.pem")
+        set_source_files_properties(${PUBLIC_KEY_PATH} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
+        set(PUBLIC_KEY ${PUBLIC_KEY_PATH})
+     endif()
+     if(BETA)
+         message(STATUS "Beta config enabled")
+         add_definitions(-DBETA)
+     endif()
 endif()
 
 # Qt find package
@@ -607,12 +624,14 @@ elseif (NOT APPLE)
     add_custom_target(uninstall
         COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
 else()
-    target_sources(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/resources/images/jami.icns)
-    target_link_libraries(${PROJECT_NAME} PRIVATE
-                          ${QT_LIBS}
-                          ${LRC_LIB_NAME}
-                          ${SYSTEM_CONFIGURATUION}
-                          qrencode)
+    set(resources ${CMAKE_CURRENT_SOURCE_DIR}/resources/images/jami.icns)
+    set(libs ${QT_LIBS} ${LRC_LIB_NAME} ${SYSTEM_CONFIGURATUION} qrencode)
+    if(ENABLE_SPARKLE)
+        set(resources ${resources} ${PUBLIC_KEY} ${SPARKLE_FRAMEWORK})
+        set(libs ${libs} ${SPARKLE_FRAMEWORK})
+    endif(ENABLE_SPARKLE)
+    target_sources(${PROJECT_NAME} PRIVATE ${resources})
+    target_link_libraries(${PROJECT_NAME} PRIVATE ${libs})
 
      # translations
      if(Qt${QT_VERSION_MAJOR}LinguistTools_FOUND)
@@ -636,21 +655,23 @@ else()
                             DESTINATION ${CMAKE_INSTALL_PREFIX}/share/libringclient/translations)
       endif()
       set_target_properties(${PROJECT_NAME} PROPERTIES
-                            MACOSX_BUNDLE TRUE
-                            MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/resources/Info.plist"
-                            MACOSX_BUNDLE_EXECUTABLE_NAME "${PROJ_NAME}"
-                            MACOSX_BUNDLE_ICON_FILE "jami.icns"
-                            MACOSX_BUNDLE_GUI_IDENTIFIER "${BUNDLE_ID}"
-                            MACOSX_BUNDLE_BUNDLE_NAME "${PROJ_NAME}"
-                            MACOSX_BUNDLE_SHORT_VERSION_STRING "${JAMI_VERSION}"
-                            MACOSX_BUNDLE_BUNDLE_VERSION "${JAMI_BUILD}"
-                            MACOSX_BUNDLE_COPYRIGHT "${PROJ_COPYRIGHT}"
-                            XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/resources/entitlements/Jami.entitlements"
-                            XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME TRUE)
+          MACOSX_BUNDLE TRUE
+          MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/resources/Info.plist"
+          MACOSX_BUNDLE_EXECUTABLE_NAME "${PROJ_NAME}"
+          MACOSX_BUNDLE_ICON_FILE "jami.icns"
+          MACOSX_BUNDLE_GUI_IDENTIFIER "${BUNDLE_ID}"
+          MACOSX_BUNDLE_SHORT_VERSION_STRING "${JAMI_VERSION}"
+          MACOSX_BUNDLE_BUNDLE_VERSION "${JAMI_BUILD}"
+          MACOSX_BUNDLE_COPYRIGHT "${PROJ_COPYRIGHT}"
+          SPARKLE_URL "${SPARKLE_URL}"
+          XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/resources/entitlements/Jami.entitlements"
+          XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME TRUE)
       add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
                          COMMAND ${CMAKE_COMMAND} -DQML_SRC_DIR=${SRC_DIR}
                          -DMAC_DEPLOY_QT_PATH=${CMAKE_PREFIX_PATH}/bin
                          -DEXE_NAME="${CMAKE_BINARY_DIR}/${PROJECT_NAME}.app"
+                         -DSPARKLE_PATH=${SPARKLE_FRAMEWORK}
+                         -DENABLE_SPARKLE=${ENABLE_SPARKLE}
                          -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos_qt_deploy.cmake)
 endif()
 
diff --git a/cmake/macos_qt_deploy.cmake b/cmake/macos_qt_deploy.cmake
index 5af951040..cf12292a0 100644
--- a/cmake/macos_qt_deploy.cmake
+++ b/cmake/macos_qt_deploy.cmake
@@ -1,5 +1,7 @@
-
-    message("Qt deploying in dir " ${QML_SRC_DIR})
-    execute_process(COMMAND "${MAC_DEPLOY_QT_PATH}/macdeployqt"
+message("Qt deploying in dir " ${QML_SRC_DIR})
+execute_process(COMMAND "${MAC_DEPLOY_QT_PATH}/macdeployqt"
                             ${EXE_NAME}
                             -qmldir=${QML_SRC_DIR})
+if(${ENABLE_SPARKLE} MATCHES true)
+    file(COPY ${SPARKLE_PATH} DESTINATION ${EXE_NAME}/Contents/Frameworks/)
+endif()
\ No newline at end of file
diff --git a/resources/Info.plist b/resources/Info.plist
index 9ae6f7a74..6bc475055 100644
--- a/resources/Info.plist
+++ b/resources/Info.plist
@@ -2,39 +2,39 @@
 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-    <key>CFBundleDevelopmentRegion</key>
-    <string>English</string>
-    <key>CFBundleExecutable</key>
-    <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
-    <key>CFBundleIconFile</key>
-    <string>${MACOSX_BUNDLE_ICON_FILE}</string>
-    <key>CFBundleIdentifier</key>
-    <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
-    <key>CFBundleName</key>
-    <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
-    <key>CFBundlePackageType</key>
-    <string>APPL</string>
-    <key>CFBundleShortVersionString</key>
-    <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
-    <key>LSMinimumSystemVersion</key>
-    <string>10.13</string>
-    <key>CFBundleVersion</key>
-    <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
-    <key>CSResourcesFileMapped</key>
-    <true/>
-    <key>LSApplicationCategoryType</key>
-    <string>public.app-category.social-networking</string>
-    <key>LSRequiresCarbon</key>
-    <true/>
-    <key>NSHumanReadableCopyright</key>
-    <string>${MACOSX_BUNDLE_COPYRIGHT}</string>
-    <key>NSHighResolutionCapable</key>
-    <string>True</string>
-    <key>NSCameraUsageDescription</key>
-    <string>Jami requires to access your camera to make calls and record video</string>
-    <key>NSMicrophoneUsageDescription</key>
-    <string>Jami requires to access your microphone to make calls and record audio</string>
-    <key>NSPhotoLibraryUsageDescription</key>
-    <string>Jami requires to access your photo library to show image on profile and send via chat</string>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>English</string>
+	<key>CFBundleExecutable</key>
+	<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
+	<key>CFBundleIconFile</key>
+	<string>${MACOSX_BUNDLE_ICON_FILE}</string>
+	<key>CFBundleIdentifier</key>
+	<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
+	<key>CFBundleName</key>
+	<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
+	<key>LSMinimumSystemVersion</key>
+	<string>10.13</string>
+	<key>CFBundleVersion</key>
+	<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
+	<key>LSApplicationCategoryType</key>
+	<string>public.app-category.social-networking</string>
+	<key>NSHumanReadableCopyright</key>
+	<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
+	<key>SUPublicDSAKeyFile</key>
+	<string>dsa_pub.pem</string>
+	<key>SUFeedURL</key>
+	<string>${SPARKLE_URL}</string>
+	<key>NSPrincipalClass</key>
+	<string>NSApplication</string>
+	<key>NSCameraUsageDescription</key>
+	<string>Jami requires to access your camera to make calls and record video</string>
+	<key>NSMicrophoneUsageDescription</key>
+	<string>Jami requires to access your microphone to make calls and record audio</string>
+	<key>NSPhotoLibraryUsageDescription</key>
+	<string>Jami requires to access your photo library to show image on profile and send via chat</string>
 </dict>
 </plist>
diff --git a/resources/entitlements/Jami.entitlements b/resources/entitlements/Jami.entitlements
index 725c1ee5f..80ca05b1f 100644
--- a/resources/entitlements/Jami.entitlements
+++ b/resources/entitlements/Jami.entitlements
@@ -2,13 +2,13 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-	<key>com.apple.security.app-sandbox</key>
-	<true/>
 	<key>com.apple.security.device.camera</key>
 	<true/>
-	<key>com.apple.security.device.microphone</key>
+	<key>com.apple.security.device.audio-input</key>
 	<true/>
 	<key>com.apple.security.files.user-selected.read-write</key>
 	<true/>
+	<key>com.apple.security.cs.allow-jit</key>
+	<true/>
 </dict>
 </plist>
diff --git a/sparkle/Sparkle b/sparkle/Sparkle
new file mode 160000
index 000000000..2195ee088
--- /dev/null
+++ b/sparkle/Sparkle
@@ -0,0 +1 @@
+Subproject commit 2195ee0883efc92828a0cf33b830f43f9bd7e01b
diff --git a/sparkle/dsa_pub.pem b/sparkle/dsa_pub.pem
new file mode 100644
index 000000000..97455e2f2
--- /dev/null
+++ b/sparkle/dsa_pub.pem
@@ -0,0 +1,36 @@
+-----BEGIN PUBLIC KEY-----
+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
+ZnO5P9x/rPIJLCXY4xQYBryQCMu6JC5ibWzP
+-----END PUBLIC KEY-----
diff --git a/sparkle/sign_update.sh b/sparkle/sign_update.sh
new file mode 100755
index 000000000..bb2fbab5e
--- /dev/null
+++ b/sparkle/sign_update.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -e
+set -o pipefail
+if [ "$#" -ne 2 ]; then
+  echo "Usage: $0 update_archive private_key"
+  exit 1
+fi
+
+openssl=/usr/bin/openssl
+$openssl dgst -sha1 -binary < "$1" | $openssl dgst -dss1 -sign "$2" | base64 $BASE64_OPTS
+
diff --git a/sparkle/sparkle-xml-updater.sh b/sparkle/sparkle-xml-updater.sh
new file mode 100755
index 000000000..62ca00323
--- /dev/null
+++ b/sparkle/sparkle-xml-updater.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+# Take the package to add as argument ./sparkle-xml-updater.sh jami.dmg
+
+REPO_FOLDER=$1
+SPARKLE_FILE=$2
+REPO_URL=$3
+PACKAGE=$4
+DSA_KEY=$5
+CHANNEL_NAME=$6
+VERSION=$7
+BUILD=$8
+
+if [ ! -f ${PACKAGE} -o ! -f ${DSA_KEY} ]; then
+    echo "Can't find package or dsa key, aborting..."
+    exit 1
+fi
+
+if [ -f ${REPO_FOLDER}/${SPARKLE_FILE} ]; then
+    ITEMS=$(sed -n "/<item>/,/<\/item>/p" ${REPO_FOLDER}/${SPARKLE_FILE})
+fi
+
+PACKAGE_SIZE=`stat -f%z ${PACKAGE}`
+DATE_RFC2822=`date "+%a, %d %b %Y %T %z"`
+
+cat << EOFILE > ${REPO_FOLDER}/${SPARKLE_FILE}
+<?xml version="1.0" encoding="utf-8"?>
+<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
+    <channel>
+        <title>${CHANNEL_NAME}</title>
+        <link>${REPO_URL}/${SPARKLE_FILE}</link>
+        <description>Most recent changes with links to updates.</description>
+        <language>en</language>
+        <item>
+            <title>"${CHANNEL_NAME}-${BUILD}"</title>
+            <pubDate>$DATE_RFC2822</pubDate>
+            <sparkle:version>${BUILD}</sparkle:version>
+            <sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString>
+            <sparkle:minimumSystemVersion>10.13</sparkle:minimumSystemVersion>
+            <enclosure url="${REPO_URL}/$(basename ${PACKAGE})" length="$PACKAGE_SIZE" type="application/octet-stream" sparkle:dsaSignature="$(./sign_update.sh ${PACKAGE} ${DSA_KEY})" />
+        </item>
+$(echo -e "${ITEMS}")
+    </channel>
+</rss>
+EOFILE
+
diff --git a/src/MainApplicationWindow.qml b/src/MainApplicationWindow.qml
index fa337d5ee..d03d75d08 100644
--- a/src/MainApplicationWindow.qml
+++ b/src/MainApplicationWindow.qml
@@ -155,9 +155,11 @@ ApplicationWindow {
             windowSettingsLoaded = true
 
             // Quiet check for updates on start if set to.
-            if (UtilsAdapter.getAppValue(Settings.AutoUpdate)) {
-                UpdateManager.checkForUpdates(true)
-                UpdateManager.setAutoUpdateCheck(true)
+            if (Qt.platform.os.toString() !== "osx") {
+                if (UtilsAdapter.getAppValue(Settings.AutoUpdate)) {
+                    UpdateManager.checkForUpdates(true)
+                    UpdateManager.setAutoUpdateCheck(true)
+                }
             }
 
             // Handle a start URI if set as start option.
@@ -190,7 +192,7 @@ ApplicationWindow {
 
     Connections {
         target: {
-            if (Qt.platform.os !== "windows" && Qt.platform.os !== "macos")
+            if (Qt.platform.os.toString()  !== "windows" && Qt.platform.os.toString()  !== "osx")
                 return DBusErrorHandler
             return null
         }
@@ -220,7 +222,7 @@ ApplicationWindow {
 
         JamiQmlUtils.mainApplicationScreen = root.screen
 
-        if (Qt.platform.os !== "windows" && Qt.platform.os !== "macos")
+        if (Qt.platform.os.toString()  !== "windows" && Qt.platform.os.toString()  !== "osx")
             DBusErrorHandler.setActive(true)
     }
 }
diff --git a/src/connectivitymonitor.mm b/src/os/macos/connectivitymonitor.mm
similarity index 100%
rename from src/connectivitymonitor.mm
rename to src/os/macos/connectivitymonitor.mm
diff --git a/src/os/macos/updatemanager.mm b/src/os/macos/updatemanager.mm
new file mode 100644
index 000000000..3ff7dfae6
--- /dev/null
+++ b/src/os/macos/updatemanager.mm
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2021-2022 Savoir-faire Linux Inc.
+ * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "updatemanager.h"
+
+#ifdef ENABLE_SPARKLE
+#include <Sparkle/Sparkle.h>
+#endif
+
+#ifdef BETA
+static constexpr bool isBeta = true;
+#else
+static constexpr bool isBeta = false;
+#endif
+
+#ifdef ENABLE_SPARKLE
+ struct UpdateManager::Impl
+{
+
+    Impl()
+    {
+        updaterController_ = [[SPUStandardUpdaterController alloc] initWithStartingUpdater: true
+                                                                           updaterDelegate: nil
+                                                                        userDriverDelegate: nil];
+    };
+
+    void checkForUpdates()
+    {
+        [updaterController_ checkForUpdates: nil];
+    };
+
+    void setAutoUpdateCheck(bool state)
+    {
+        updaterController_.updater.automaticallyChecksForUpdates = state;
+    };
+
+    bool isAutoUpdaterEnabled()
+    {
+        return updaterController_.updater.automaticallyChecksForUpdates;
+    };
+
+    bool isUpdaterEnabled() {
+        return true;
+    };
+
+    SPUStandardUpdaterController* updaterController_;
+
+};
+#else
+struct UpdateManager::Impl
+{
+    void checkForUpdates() {};
+
+    void setAutoUpdateCheck(bool state) {};
+
+    bool isAutoUpdaterEnabled()
+    {
+        return false;
+    };
+    bool isUpdaterEnabled()
+    {
+        return false;
+    };
+};
+#endif
+
+UpdateManager::UpdateManager(const QString& url,
+                             ConnectivityMonitor* cm,
+                             LRCInstance* instance,
+                             QObject* parent)
+    : NetWorkManager(cm, parent)
+    , pimpl_(std::make_unique<Impl>())
+{}
+
+UpdateManager::~UpdateManager()
+{}
+
+void
+UpdateManager::checkForUpdates(bool quiet)
+{
+    Q_UNUSED(quiet)
+    pimpl_->checkForUpdates();
+}
+
+void
+UpdateManager::applyUpdates(bool beta)
+{
+    Q_UNUSED(beta)
+}
+
+void
+UpdateManager::cancelUpdate()
+{}
+
+void
+UpdateManager::setAutoUpdateCheck(bool state)
+{
+    pimpl_->setAutoUpdateCheck(state);
+}
+
+bool
+UpdateManager::isCurrentVersionBeta()
+{
+    return isBeta;
+}
+
+bool
+UpdateManager::isUpdaterEnabled()
+{
+    return pimpl_->isUpdaterEnabled();
+}
+
+bool
+UpdateManager::isAutoUpdaterEnabled()
+{
+    return pimpl_->isAutoUpdaterEnabled();
+}
diff --git a/src/settingsview/components/GeneralSettingsPage.qml b/src/settingsview/components/GeneralSettingsPage.qml
index 478fda3ca..82f7a93a4 100644
--- a/src/settingsview/components/GeneralSettingsPage.qml
+++ b/src/settingsview/components/GeneralSettingsPage.qml
@@ -23,6 +23,7 @@ import net.jami.Models 1.1
 import net.jami.Adapters 1.1
 import net.jami.Enums 1.1
 import net.jami.Constants 1.1
+import net.jami.Helpers 1.1
 
 import "../../commoncomponents"
 
@@ -99,7 +100,7 @@ Rectangle {
             Layout.leftMargin: JamiTheme.preferredMarginSize
             Layout.rightMargin: JamiTheme.preferredMarginSize
             Layout.bottomMargin: JamiTheme.preferredMarginSize
-            visible: Qt.platform.os == "windows" ? true : false
+            visible: UpdateManager.isUpdaterEnabled()
         }
     }
 }
diff --git a/src/settingsview/components/UpdateSettings.qml b/src/settingsview/components/UpdateSettings.qml
index af3bb96a1..f56530bc1 100644
--- a/src/settingsview/components/UpdateSettings.qml
+++ b/src/settingsview/components/UpdateSettings.qml
@@ -50,7 +50,9 @@ ColumnLayout {
         Layout.fillWidth: true
         Layout.leftMargin: JamiTheme.preferredMarginSize
 
-        checked: UtilsAdapter.getAppValue(Settings.Key.AutoUpdate)
+        checked: Qt.platform.os.toString() === "windows" ?
+                     UtilsAdapter.getAppValue(Settings.Key.AutoUpdate) :
+                     UpdateManager.isAutoUpdaterEnabled()
 
         labelText: JamiStrings.update
         tooltipText: JamiStrings.enableAutoUpdates
@@ -84,7 +86,7 @@ ColumnLayout {
     MaterialButton {
         id: installBetaButton
 
-        visible: !UpdateManager.isCurrentVersionBeta()
+        visible: !UpdateManager.isCurrentVersionBeta() && Qt.platform.os.toString()  === "windows"
 
         Layout.alignment: Qt.AlignHCenter
 
diff --git a/src/updatemanager.cpp b/src/updatemanager.cpp
index 59c7f1092..b9af40444 100644
--- a/src/updatemanager.cpp
+++ b/src/updatemanager.cpp
@@ -1,4 +1,4 @@
-/*!
+/*
  * Copyright (C) 2020-2022 Savoir-faire Linux Inc.
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
  *
@@ -38,135 +38,191 @@ static constexpr char betaVersionSubUrl[] = "/beta/version";
 static constexpr char msiSubUrl[] = "/jami.release.x64.msi";
 static constexpr char betaMsiSubUrl[] = "/beta/jami.beta.x64.msi";
 
+struct UpdateManager::Impl : public QObject
+{
+    Impl(const QString& url, ConnectivityMonitor* cm, LRCInstance* instance, UpdateManager& parent)
+        : QObject(nullptr)
+        , parent_(parent)
+        , lrcInstance_(instance)
+        , baseUrlString_(url.isEmpty() ? downloadUrl : url)
+        , tempPath_(Utils::WinGetEnv("TEMP"))
+        , updateTimer_(new QTimer(this))
+    {
+        connect(updateTimer_, &QTimer::timeout, [this] {
+            // Quiet period update check.
+            parent_.checkForUpdates(true);
+        });
+    };
+    ~Impl() = default;
+
+    void checkForUpdates(bool quiet)
+    {
+        parent_.disconnect();
+        // Fail without UI if this is a programmatic check.
+        if (!quiet)
+            connect(&parent_,
+                    &NetWorkManager::errorOccured,
+                    &parent_,
+                    &UpdateManager::updateCheckErrorOccurred);
+
+        cleanUpdateFiles();
+        QUrl versionUrl {isBeta ? QUrl::fromUserInput(baseUrlString_ + betaVersionSubUrl)
+                                : QUrl::fromUserInput(baseUrlString_ + versionSubUrl)};
+        parent_.get(versionUrl, [this, quiet](const QString& latestVersionString) {
+            if (latestVersionString.isEmpty()) {
+                qWarning() << "Error checking version";
+                if (!quiet)
+                    Q_EMIT parent_.updateCheckReplyReceived(false);
+                return;
+            }
+            auto currentVersion = QString(VERSION_STRING).toULongLong();
+            auto latestVersion = latestVersionString.toULongLong();
+            qDebug() << "latest: " << latestVersion << " current: " << currentVersion;
+            if (latestVersion > currentVersion) {
+                qDebug() << "New version found";
+                Q_EMIT parent_.updateCheckReplyReceived(true, true);
+            } else {
+                qDebug() << "No new version found";
+                if (!quiet)
+                    Q_EMIT parent_.updateCheckReplyReceived(true, false);
+            }
+        });
+    };
+
+    void applyUpdates(bool beta = false)
+    {
+        parent_.disconnect();
+        connect(&parent_,
+                &NetWorkManager::errorOccured,
+                &parent_,
+                &UpdateManager::updateDownloadErrorOccurred);
+        connect(&parent_, &NetWorkManager::statusChanged, [this](GetStatus status) {
+            switch (status) {
+            case GetStatus::STARTED:
+                connect(&parent_,
+                        &NetWorkManager::downloadProgressChanged,
+                        &parent_,
+                        &UpdateManager::updateDownloadProgressChanged);
+                Q_EMIT parent_.updateDownloadStarted();
+                break;
+            case GetStatus::FINISHED:
+                Q_EMIT parent_.updateDownloadFinished();
+                break;
+            default:
+                break;
+            }
+        });
+
+        QUrl downloadUrl {(beta || isBeta) ? QUrl::fromUserInput(baseUrlString_ + betaMsiSubUrl)
+                                           : QUrl::fromUserInput(baseUrlString_ + msiSubUrl)};
+
+        parent_.get(
+            downloadUrl,
+            [this, downloadUrl](const QString&) {
+                lrcInstance_->finish();
+                Q_EMIT lrcInstance_->quitEngineRequested();
+                auto args = QString(" /passive /norestart WIXNONUILAUNCH=1");
+                QProcess process;
+                process.start("powershell ",
+                              QStringList() << tempPath_ + "\\" + downloadUrl.fileName() << "/L*V"
+                                            << tempPath_ + "\\jami_x64_install.log" + args);
+                process.waitForFinished();
+            },
+            tempPath_);
+    };
+
+    void cancelUpdate()
+    {
+        parent_.cancelRequest();
+    };
+
+    void setAutoUpdateCheck(bool state)
+    {
+        // Quiet check for updates periodically, if set to.
+        if (!state) {
+            updateTimer_->stop();
+            return;
+        }
+        updateTimer_->start(updatePeriod);
+    };
+
+    void cleanUpdateFiles()
+    {
+        // Delete all logs and msi in the %TEMP% directory before launching.
+        QString dir = QString(Utils::WinGetEnv("TEMP"));
+        QDir log_dir(dir, {"jami*.log"});
+        for (const QString& filename : log_dir.entryList()) {
+            log_dir.remove(filename);
+        }
+        QDir msi_dir(dir, {"jami*.msi"});
+        for (const QString& filename : msi_dir.entryList()) {
+            msi_dir.remove(filename);
+        }
+        QDir version_dir(dir, {"version"});
+        for (const QString& filename : version_dir.entryList()) {
+            version_dir.remove(filename);
+        }
+    };
+
+    UpdateManager& parent_;
+
+    LRCInstance* lrcInstance_ {nullptr};
+    QString baseUrlString_;
+    QString tempPath_;
+    QTimer* updateTimer_;
+};
+
 UpdateManager::UpdateManager(const QString& url,
                              ConnectivityMonitor* cm,
                              LRCInstance* instance,
                              QObject* parent)
     : NetWorkManager(cm, parent)
-    , lrcInstance_(instance)
-    , baseUrlString_(url.isEmpty() ? downloadUrl : url)
-    , tempPath_(Utils::WinGetEnv("TEMP"))
-    , updateTimer_(new QTimer(this))
+    , pimpl_(std::make_unique<Impl>(url, cm, instance, *this))
+{}
+
+UpdateManager::~UpdateManager() {}
+
+void
+UpdateManager::checkForUpdates(bool quiet)
 {
-    connect(updateTimer_, &QTimer::timeout, [this] {
-        // Quiet period update check.
-        checkForUpdates(true);
-    });
+    pimpl_->checkForUpdates(quiet);
 }
 
 void
-UpdateManager::setAutoUpdateCheck(bool state)
+UpdateManager::applyUpdates(bool beta)
 {
-    // Quiet check for updates periodically, if set to.
-    if (!state) {
-        updateTimer_->stop();
-        return;
-    }
-    updateTimer_->start(updatePeriod);
+    pimpl_->applyUpdates(beta);
 }
 
-bool
-UpdateManager::isCurrentVersionBeta()
+void
+UpdateManager::cancelUpdate()
 {
-    return isBeta;
+    pimpl_->cancelUpdate();
 }
 
 void
-UpdateManager::checkForUpdates(bool quiet)
+UpdateManager::setAutoUpdateCheck(bool state)
 {
-    disconnect();
-
-    // Fail without UI if this is a programmatic check.
-    if (!quiet)
-        connect(this, &NetWorkManager::errorOccured, this, &UpdateManager::updateCheckErrorOccurred);
-
-    cleanUpdateFiles();
-    QUrl versionUrl {isBeta ? QUrl::fromUserInput(baseUrlString_ + betaVersionSubUrl)
-                            : QUrl::fromUserInput(baseUrlString_ + versionSubUrl)};
-    get(versionUrl, [this, quiet](const QString& latestVersionString) {
-        if (latestVersionString.isEmpty()) {
-            qWarning() << "Error checking version";
-            if (!quiet)
-                Q_EMIT updateCheckReplyReceived(false);
-            return;
-        }
-        auto currentVersion = QString(VERSION_STRING).toULongLong();
-        auto latestVersion = latestVersionString.toULongLong();
-        qDebug() << "latest: " << latestVersion << " current: " << currentVersion;
-        if (latestVersion > currentVersion) {
-            qDebug() << "New version found";
-            Q_EMIT updateCheckReplyReceived(true, true);
-        } else {
-            qDebug() << "No new version found";
-            if (!quiet)
-                Q_EMIT updateCheckReplyReceived(true, false);
-        }
-    });
+    pimpl_->setAutoUpdateCheck(state);
 }
 
-void
-UpdateManager::applyUpdates(bool beta)
+bool
+UpdateManager::isCurrentVersionBeta()
 {
-    disconnect();
-    connect(this, &NetWorkManager::errorOccured, this, &UpdateManager::updateDownloadErrorOccurred);
-    connect(this, &NetWorkManager::statusChanged, [this](GetStatus status) {
-        switch (status) {
-        case GetStatus::STARTED:
-            connect(this,
-                    &NetWorkManager::downloadProgressChanged,
-                    this,
-                    &UpdateManager::updateDownloadProgressChanged);
-            Q_EMIT updateDownloadStarted();
-            break;
-        case GetStatus::FINISHED:
-            Q_EMIT updateDownloadFinished();
-            break;
-        default:
-            break;
-        }
-    });
-
-    QUrl downloadUrl {(beta || isBeta) ? QUrl::fromUserInput(baseUrlString_ + betaMsiSubUrl)
-                                       : QUrl::fromUserInput(baseUrlString_ + msiSubUrl)};
-
-    get(
-        downloadUrl,
-        [this, downloadUrl](const QString&) {
-            lrcInstance_->finish();
-            Q_EMIT lrcInstance_->quitEngineRequested();
-            auto args = QString(" /passive /norestart WIXNONUILAUNCH=1");
-            QProcess process;
-            process.start("powershell ",
-                          QStringList() << tempPath_ + "\\" + downloadUrl.fileName() << "/L*V"
-                                        << tempPath_ + "\\jami_x64_install.log" + args);
-            process.waitForFinished();
-        },
-        tempPath_);
+    return isBeta;
 }
 
-void
-UpdateManager::cancelUpdate()
+bool
+UpdateManager::isUpdaterEnabled()
 {
-    cancelRequest();
+#ifdef Q_OS_WIN
+    return true;
+#endif
+    return false;
 }
 
-void
-UpdateManager::cleanUpdateFiles()
+bool
+UpdateManager::isAutoUpdaterEnabled()
 {
-    /*
-     * Delete all logs and msi in the %TEMP% directory before launching.
-     */
-    QString dir = QString(Utils::WinGetEnv("TEMP"));
-    QDir log_dir(dir, {"jami*.log"});
-    for (const QString& filename : log_dir.entryList()) {
-        log_dir.remove(filename);
-    }
-    QDir msi_dir(dir, {"jami*.msi"});
-    for (const QString& filename : msi_dir.entryList()) {
-        msi_dir.remove(filename);
-    }
-    QDir version_dir(dir, {"version"});
-    for (const QString& filename : version_dir.entryList()) {
-        version_dir.remove(filename);
-    }
+    return false;
 }
diff --git a/src/updatemanager.h b/src/updatemanager.h
index d506a542c..cf8279c12 100644
--- a/src/updatemanager.h
+++ b/src/updatemanager.h
@@ -1,4 +1,4 @@
-/*!
+/*
  * Copyright (C) 2020-2022 Savoir-faire Linux Inc.
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
  *
@@ -20,25 +20,29 @@
 
 #include "networkmanager.h"
 
+#include <memory>
+
 class LRCInstance;
 class ConnectivityMonitor;
-class QTimer;
 
 class UpdateManager final : public NetWorkManager
 {
     Q_OBJECT
+    Q_DISABLE_COPY(UpdateManager)
 public:
     explicit UpdateManager(const QString& url,
                            ConnectivityMonitor* cm,
                            LRCInstance* instance = nullptr,
                            QObject* parent = nullptr);
-    ~UpdateManager() = default;
+    ~UpdateManager();
 
     Q_INVOKABLE void checkForUpdates(bool quiet = false);
     Q_INVOKABLE void applyUpdates(bool beta = false);
     Q_INVOKABLE void cancelUpdate();
     Q_INVOKABLE void setAutoUpdateCheck(bool state);
     Q_INVOKABLE bool isCurrentVersionBeta();
+    Q_INVOKABLE bool isUpdaterEnabled();
+    Q_INVOKABLE bool isAutoUpdaterEnabled();
 
 Q_SIGNALS:
     void updateCheckReplyReceived(bool ok, bool found = false);
@@ -50,13 +54,7 @@ Q_SIGNALS:
     void appCloseRequested();
 
 private:
-    // LRCInstance pointer
-    LRCInstance* lrcInstance_ {nullptr};
-
-    QString baseUrlString_;
-    QString tempPath_;
-    QTimer* updateTimer_;
-
-    void cleanUpdateFiles();
+    struct Impl;
+    std::unique_ptr<Impl> pimpl_;
 };
 Q_DECLARE_METATYPE(UpdateManager*)
-- 
GitLab