diff --git a/pom.xml b/pom.xml
index 79558bd..506900e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,9 +40,10 @@
- 1.6.0
+ 1.7.0-SNAPSHOT
2.0.1-alpha
1.4.0
+ 1.0.0
2.0.17
1.4.2
@@ -88,6 +89,11 @@
libappindicator-gtk3-java-minimal
${appindicator.version}
+
+ org.purejava
+ flatpak-update-portal
+ ${flatpakupdateportal.version}
+
org.junit.jupiter
junit-jupiter
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index c728d22..9d98f16 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -3,13 +3,15 @@
import org.cryptomator.integrations.quickaccess.QuickAccessService;
import org.cryptomator.integrations.revealpath.RevealPathService;
import org.cryptomator.integrations.tray.TrayMenuController;
+import org.cryptomator.integrations.update.UpdateMechanism;
import org.cryptomator.linux.autostart.FreedesktopAutoStartService;
-import org.cryptomator.linux.keychain.KDEWalletKeychainAccess;
import org.cryptomator.linux.keychain.GnomeKeyringKeychainAccess;
+import org.cryptomator.linux.keychain.KDEWalletKeychainAccess;
import org.cryptomator.linux.quickaccess.DolphinPlaces;
import org.cryptomator.linux.quickaccess.NautilusBookmarks;
import org.cryptomator.linux.revealpath.DBusSendRevealPathService;
import org.cryptomator.linux.tray.AppindicatorTrayMenuController;
+import org.cryptomator.linux.update.FlatpakUpdater;
module org.cryptomator.integrations.linux {
requires org.cryptomator.integrations.api;
@@ -17,6 +19,7 @@
requires org.freedesktop.dbus;
requires org.purejava.appindicator;
requires org.purejava.kwallet;
+ requires org.purejava.portal;
requires de.swiesend.secretservice;
requires java.xml;
@@ -25,8 +28,10 @@
provides RevealPathService with DBusSendRevealPathService;
provides TrayMenuController with AppindicatorTrayMenuController;
provides QuickAccessService with NautilusBookmarks, DolphinPlaces;
+ provides UpdateMechanism with FlatpakUpdater;
opens org.cryptomator.linux.tray to org.cryptomator.integrations.api;
opens org.cryptomator.linux.quickaccess to org.cryptomator.integrations.api;
opens org.cryptomator.linux.autostart to org.cryptomator.integrations.api;
+ opens org.cryptomator.linux.update to org.cryptomator.integrations.api;
}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/linux/update/FlatpakUpdater.java b/src/main/java/org/cryptomator/linux/update/FlatpakUpdater.java
new file mode 100644
index 0000000..f622e28
--- /dev/null
+++ b/src/main/java/org/cryptomator/linux/update/FlatpakUpdater.java
@@ -0,0 +1,202 @@
+package org.cryptomator.linux.update;
+
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.DisplayName;
+import org.cryptomator.integrations.common.OperatingSystem;
+import org.cryptomator.integrations.common.Priority;
+import org.cryptomator.integrations.update.UpdateFailedException;
+import org.cryptomator.integrations.update.UpdateMechanism;
+import org.cryptomator.integrations.update.UpdateProcess;
+import org.freedesktop.dbus.FileDescriptor;
+import org.freedesktop.dbus.exceptions.DBusException;
+import org.freedesktop.dbus.types.UInt32;
+import org.freedesktop.dbus.types.Variant;
+import org.purejava.portal.Flatpak;
+import org.purejava.portal.FlatpakSpawnFlag;
+import org.purejava.portal.UpdatePortal;
+import org.purejava.portal.Util;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@Priority(1000)
+@CheckAvailability
+@DisplayName("Update via Flatpak update")
+@OperatingSystem(OperatingSystem.Value.LINUX)
+public class FlatpakUpdater implements UpdateMechanism {
+
+ private static final Logger LOG = LoggerFactory.getLogger(FlatpakUpdater.class);
+ private static final String APP_NAME = "org.cryptomator.Cryptomator";
+
+ private final UpdatePortal portal;
+
+ public FlatpakUpdater() {
+ this.portal = new UpdatePortal();
+ portal.CreateUpdateMonitor(UpdatePortal.OPTIONS_DUMMY);
+ }
+
+ @CheckAvailability
+ public boolean isSupported() {
+ return portal.isAvailable();
+ }
+
+ @Override
+ public boolean isUpdateAvailable(String installedVersion) {
+ var cdl = new CountDownLatch(1);
+ portal.setUpdateCheckerTaskFor(APP_NAME);
+ var checkTask = portal.getUpdateCheckerTaskFor(APP_NAME);
+ var updateAvailable = new AtomicBoolean(false);
+ checkTask.setOnSucceeded(updateVersion -> {
+ updateAvailable.set(UpdateMechanism.isUpdateAvailable(updateVersion, installedVersion));
+ cdl.countDown();
+ });
+ checkTask.setOnFailed(error -> {
+ LOG.warn("Error while checking for updates.", error);
+ cdl.countDown();
+ });
+ try {
+ cdl.await();
+ return updateAvailable.get();
+ } catch (InterruptedException e) {
+ checkTask.cancel();
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ }
+
+ @Override
+ public UpdateProcess prepareUpdate() throws UpdateFailedException {
+ var monitorPath = portal.CreateUpdateMonitor(UpdatePortal.OPTIONS_DUMMY);
+ if (monitorPath == null) {
+ throw new UpdateFailedException("Failed to create UpdateMonitor on DBus");
+ }
+
+ return new FlatpakUpdateProcess(portal.getUpdateMonitor(monitorPath.toString()));
+ }
+
+ private class FlatpakUpdateProcess implements UpdateProcess {
+
+ private final CountDownLatch latch = new CountDownLatch(1);
+ private final Flatpak.UpdateMonitor monitor;
+ private volatile double progress = 0.0;
+ private volatile UpdateFailedException error;
+ private AutoCloseable signalHandler;
+
+ private FlatpakUpdateProcess(Flatpak.UpdateMonitor monitor) {
+ this.monitor = monitor;
+ startUpdate();
+ }
+
+ private void startUpdate() {
+ try {
+ this.signalHandler = portal.getDBusConnection().addSigHandler(Flatpak.UpdateMonitor.Progress.class, this::handleProgressSignal);
+ } catch (DBusException e) {
+ LOG.error("DBus error", e);
+ latch.countDown();
+ }
+ portal.updateApp("x11:0", monitor, UpdatePortal.OPTIONS_DUMMY);
+ }
+
+ private void handleProgressSignal(Flatpak.UpdateMonitor.Progress signal) {
+ int status = ((UInt32) signal.info.get("status").getValue()).intValue();
+ switch (status) {
+ case 0 -> { // In progress
+ Variant> progressVariant = signal.info.get("progress");
+ if (progressVariant != null) {
+ progress = ((UInt32) progressVariant.getValue()).doubleValue() / 100.0; // progress reported as int in range [0, 100]
+ }
+ }
+ case 1 -> { // No update available
+ error = new UpdateFailedException("No update available");
+ latch.countDown();
+ }
+ case 2 -> { // Update complete
+ progress = 1.0;
+ latch.countDown();
+ }
+ case 3 -> { // Update failed
+ error = new UpdateFailedException("Update preparation failed");
+ latch.countDown();
+ }
+ default -> {
+ error = new UpdateFailedException("Unknown update status " + status);
+ latch.countDown();
+ }
+ }
+ }
+
+ private void stopReceivingSignals() {
+ if (signalHandler != null) {
+ try {
+ signalHandler.close();
+ } catch (Exception e) {
+ LOG.error("Failed to close signal handler", e);
+ }
+ signalHandler = null;
+ }
+ }
+
+ @Override
+ public double preparationProgress() {
+ return progress;
+ }
+
+ @Override
+ public void cancel() {
+ portal.cancelUpdateMonitor(monitor);
+ stopReceivingSignals();
+ portal.close(); // TODO: is this right? belongs to parent class. update can not be retried afterwards. or should each process have its own portal instance?
+ error = new UpdateFailedException("Update cancelled by user");
+ }
+
+ @Override
+ public void await() throws InterruptedException {
+ latch.await();
+ }
+
+ @Override
+ public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
+ return latch.await(timeout, unit);
+ }
+
+ private boolean isDone() {
+ try {
+ return latch.await(0, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ }
+
+ @Override
+ public ProcessHandle applyUpdate() throws IllegalStateException, IOException {
+ if (!isDone()) {
+ throw new IllegalStateException("Update preparation is not complete");
+ }
+ stopReceivingSignals();
+ if (error != null) {
+ throw error;
+ }
+
+ // spawn new Cryptomator process:
+ var cwdPath = Util.stringToByteList(System.getProperty("user.dir"));
+ List> argv = List.of(
+ Util.stringToByteList(APP_NAME));
+ Map fds = Collections.emptyMap();
+ Map envs = Map.of();
+ UInt32 flags = new UInt32(FlatpakSpawnFlag.LATEST_VERSION.getValue());
+ Map> options = UpdatePortal.OPTIONS_DUMMY;
+ var pid = portal.Spawn(cwdPath, argv, fds, envs, flags, options).longValue();
+ return ProcessHandle.of(pid).orElseThrow();
+ }
+ }
+
+}
diff --git a/src/main/resources/META-INF/services/org.cryptomator.integrations.update.UpdateService b/src/main/resources/META-INF/services/org.cryptomator.integrations.update.UpdateService
new file mode 100644
index 0000000..f8d5490
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.cryptomator.integrations.update.UpdateService
@@ -0,0 +1 @@
+org.cryptomator.linux.update.FlatpakUpdater
\ No newline at end of file