From 4b474ebb4d70bd61241c7c7b14bf4d1e497b27f6 Mon Sep 17 00:00:00 2001
From: Goldmaster <7105970+Goldmaster@users.noreply.github.com>
Date: Sun, 19 Jul 2026 19:14:41 +0100
Subject: [PATCH 1/9] Start work on SMB implementation.
---
.idea/appInsightsSettings.xml | 26 ++++
.idea/deploymentTargetSelector.xml | 18 +++
.idea/misc.xml | 2 +-
.idea/planningMode.xml | 10 ++
.../CloudContentRepositoryFactories.java | 3 +
.../CloudContentRepositoryFactories.java | 3 +
.../CloudContentRepositoryFactories.java | 7 +-
.../cloud/smb/SmbCloudContentRepository.kt | 91 +++++++++++
.../smb/SmbCloudContentRepositoryFactory.java | 41 +++++
.../org/cryptomator/data/cloud/smb/SmbFile.kt | 17 ++
.../cryptomator/data/cloud/smb/SmbFolder.kt | 19 +++
.../org/cryptomator/data/cloud/smb/SmbNode.kt | 22 +++
.../data/db/mappers/CloudEntityMapper.java | 14 ++
.../java/org/cryptomator/domain/CloudType.kt | 2 +-
.../java/org/cryptomator/domain/SmbCloud.java | 145 ++++++++++++++++++
.../presentation/model/CloudTypeModel.kt | 7 +
.../presentation/model/SmbCloudModel.kt | 40 +++++
.../model/mappers/CloudModelMapper.kt | 2 +
.../presenter/AuthenticateCloudPresenter.kt | 15 ++
.../presenter/CloudConnectionListPresenter.kt | 1 +
.../presenter/CloudSettingsPresenter.kt | 11 +-
.../activity/CryptomatorVariantsActivity.kt | 6 +-
presentation/src/main/res/values/strings.xml | 1 +
23 files changed, 495 insertions(+), 8 deletions(-)
create mode 100644 .idea/appInsightsSettings.xml
create mode 100644 .idea/deploymentTargetSelector.xml
create mode 100644 .idea/planningMode.xml
create mode 100644 data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
create mode 100644 data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepositoryFactory.java
create mode 100644 data/src/main/java/org/cryptomator/data/cloud/smb/SmbFile.kt
create mode 100644 data/src/main/java/org/cryptomator/data/cloud/smb/SmbFolder.kt
create mode 100644 data/src/main/java/org/cryptomator/data/cloud/smb/SmbNode.kt
create mode 100644 domain/src/main/java/org/cryptomator/domain/SmbCloud.java
create mode 100644 presentation/src/main/java/org/cryptomator/presentation/model/SmbCloudModel.kt
diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
new file mode 100644
index 0000000000..371f2e299f
--- /dev/null
+++ b/.idea/appInsightsSettings.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000000..8c1575c2be
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 1d3e3ba73f..edb77fab24 100755
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -54,7 +54,7 @@
-
+
diff --git a/.idea/planningMode.xml b/.idea/planningMode.xml
new file mode 100644
index 0000000000..affd64cc89
--- /dev/null
+++ b/.idea/planningMode.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data/src/apkStorePlaystore/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java b/data/src/apkStorePlaystore/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
index 918d4dfb20..51fb48a863 100644
--- a/data/src/apkStorePlaystore/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
+++ b/data/src/apkStorePlaystore/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
@@ -7,6 +7,7 @@
import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
+import org.cryptomator.data.cloud.smb.SmbCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.jetbrains.annotations.NotNull;
@@ -29,6 +30,7 @@ public CloudContentRepositoryFactories(DropboxCloudContentRepositoryFactory drop
OnedriveCloudContentRepositoryFactory oneDriveFactory, //
PCloudContentRepositoryFactory pCloudFactory, //
S3CloudContentRepositoryFactory s3Factory, //
+ SmbCloudContentRepositoryFactory smbFactory, //
CryptoCloudContentRepositoryFactory cryptoFactory, //
LocalStorageContentRepositoryFactory localStorageFactory, //
WebDavCloudContentRepositoryFactory webDavFactory) {
@@ -38,6 +40,7 @@ public CloudContentRepositoryFactories(DropboxCloudContentRepositoryFactory drop
oneDriveFactory, //
pCloudFactory, //
s3Factory, //
+ smbFactory, //
cryptoFactory, //
localStorageFactory, //
webDavFactory);
diff --git a/data/src/fdroidAccrescent/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java b/data/src/fdroidAccrescent/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
index 4511d33771..1d3e577f3d 100644
--- a/data/src/fdroidAccrescent/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
+++ b/data/src/fdroidAccrescent/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
@@ -6,6 +6,7 @@
import org.cryptomator.data.cloud.onedrive.OnedriveCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.pcloud.PCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
+import org.cryptomator.data.cloud.smb.SmbCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.jetbrains.annotations.NotNull;
@@ -27,6 +28,7 @@ public CloudContentRepositoryFactories(DropboxCloudContentRepositoryFactory drop
OnedriveCloudContentRepositoryFactory oneDriveFactory, //
PCloudContentRepositoryFactory pCloudFactory, //
S3CloudContentRepositoryFactory s3Factory, //
+ SmbCloudContentRepositoryFactory smbFactory, //
CryptoCloudContentRepositoryFactory cryptoFactory, //
LocalStorageContentRepositoryFactory localStorageFactory, //
WebDavCloudContentRepositoryFactory webDavFactory) {
@@ -35,6 +37,7 @@ public CloudContentRepositoryFactories(DropboxCloudContentRepositoryFactory drop
oneDriveFactory, //
pCloudFactory, //
s3Factory, //
+ smbFactory, //
cryptoFactory, //
localStorageFactory, //
webDavFactory);
diff --git a/data/src/lite/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java b/data/src/lite/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
index b058c305b4..91805dae92 100644
--- a/data/src/lite/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
+++ b/data/src/lite/java/org/cryptomator/data/cloud/CloudContentRepositoryFactories.java
@@ -5,6 +5,7 @@
import org.cryptomator.data.cloud.crypto.CryptoCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.local.LocalStorageContentRepositoryFactory;
import org.cryptomator.data.cloud.s3.S3CloudContentRepositoryFactory;
+import org.cryptomator.data.cloud.smb.SmbCloudContentRepositoryFactory;
import org.cryptomator.data.cloud.webdav.WebDavCloudContentRepositoryFactory;
import org.cryptomator.data.repository.CloudContentRepositoryFactory;
import org.jetbrains.annotations.NotNull;
@@ -23,12 +24,14 @@ public class CloudContentRepositoryFactories implements Iterable(Intercepted(cloud)) {
+
+ @Throws(BackendException::class)
+ override fun throwWrappedIfRequired(e: Exception) {
+ // Not yet implemented
+ }
+
+ private class Intercepted(private val cloud: SmbCloud) : CloudContentRepository {
+
+ override fun root(cloud: SmbCloud): SmbFolder {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun resolve(cloud: SmbCloud, path: String): SmbFolder {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun file(parent: SmbFolder, name: String): SmbFile {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun file(parent: SmbFolder, name: String, size: Long?): SmbFile {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun folder(parent: SmbFolder, name: String): SmbFolder {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun exists(node: SmbNode): Boolean {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun list(folder: SmbFolder): List {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun create(folder: SmbFolder): SmbFolder {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun move(source: SmbFolder, target: SmbFolder): SmbFolder {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun move(source: SmbFile, target: SmbFile): SmbFile {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun write(file: SmbFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): SmbFile {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun read(file: SmbFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun delete(node: SmbNode) {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: SmbCloud): String {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+
+ override fun logout(cloud: SmbCloud) {
+ throw UnsupportedOperationException("SMB not yet implemented")
+ }
+ }
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepositoryFactory.java b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepositoryFactory.java
new file mode 100644
index 0000000000..6beffa8916
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepositoryFactory.java
@@ -0,0 +1,41 @@
+package org.cryptomator.data.cloud.smb;
+
+import android.content.Context;
+
+import org.cryptomator.data.repository.CloudContentRepositoryFactory;
+import org.cryptomator.domain.Cloud;
+import org.cryptomator.domain.SmbCloud;
+import org.cryptomator.domain.exception.authentication.NoAuthenticationProvidedException;
+import org.cryptomator.domain.repository.CloudContentRepository;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import static org.cryptomator.domain.CloudType.SMB;
+
+/**
+ * SMB Cloud content repository factory.
+ * Skeleton for the first step of SMB support.
+ */
+@Singleton
+public class SmbCloudContentRepositoryFactory implements CloudContentRepositoryFactory {
+
+ private final Context context;
+
+ @Inject
+ public SmbCloudContentRepositoryFactory(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public boolean supports(Cloud cloud) {
+ return cloud.type() == SMB;
+ }
+
+ @Override
+ public CloudContentRepository cloudContentRepositoryFor(Cloud cloud) {
+ SmbCloud smbCloud = (SmbCloud) cloud;
+ // Authentication check will be added when SMB implementation is ready
+ return new SmbCloudContentRepository(smbCloud, context);
+ }
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbFile.kt b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbFile.kt
new file mode 100644
index 0000000000..1f5aec6467
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbFile.kt
@@ -0,0 +1,17 @@
+package org.cryptomator.data.cloud.smb
+
+import org.cryptomator.domain.Cloud
+import org.cryptomator.domain.CloudFile
+import java.util.Date
+
+/**
+ * SMB Cloud file implementation.
+ * Skeleton for the first step of SMB support.
+ */
+class SmbFile(
+ override val parent: SmbFolder,
+ override val name: String,
+ override val cloud: Cloud,
+ override val size: Long? = null,
+ override val modified: Date? = null
+) : SmbNode, CloudFile
diff --git a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbFolder.kt b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbFolder.kt
new file mode 100644
index 0000000000..09a4fd78a4
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbFolder.kt
@@ -0,0 +1,19 @@
+package org.cryptomator.data.cloud.smb
+
+import org.cryptomator.domain.Cloud
+import org.cryptomator.domain.CloudFolder
+
+/**
+ * SMB Cloud folder implementation.
+ * Skeleton for the first step of SMB support.
+ */
+open class SmbFolder(
+ override val parent: SmbFolder?,
+ override val name: String,
+ override val cloud: Cloud
+) : SmbNode, CloudFolder {
+
+ override fun withCloud(cloud: Cloud?): CloudFolder? {
+ return cloud?.let { SmbFolder(parent, name, it) }
+ }
+}
diff --git a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbNode.kt b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbNode.kt
new file mode 100644
index 0000000000..9fc63926ec
--- /dev/null
+++ b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbNode.kt
@@ -0,0 +1,22 @@
+package org.cryptomator.data.cloud.smb
+
+import org.cryptomator.domain.Cloud
+import org.cryptomator.domain.CloudNode
+
+/**
+ * SMB Cloud node interface.
+ * Skeleton for the first step of SMB support.
+ */
+interface SmbNode : CloudNode {
+ override val parent: SmbFolder?
+
+ override val path: String
+ get() {
+ val parentPath = parent?.path ?: ""
+ return if (parentPath.endsWith("/") || parentPath.isEmpty()) {
+ parentPath + name
+ } else {
+ "$parentPath/$name"
+ }
+ }
+}
diff --git a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java
index 5dd41c4d61..f89ba6aa90 100644
--- a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java
+++ b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java
@@ -9,6 +9,7 @@
import org.cryptomator.domain.OnedriveCloud;
import org.cryptomator.domain.PCloud;
import org.cryptomator.domain.S3Cloud;
+import org.cryptomator.domain.SmbCloud;
import org.cryptomator.domain.WebDavCloud;
import javax.inject.Inject;
@@ -20,6 +21,7 @@
import static org.cryptomator.domain.OnedriveCloud.aOnedriveCloud;
import static org.cryptomator.domain.PCloud.aPCloud;
import static org.cryptomator.domain.S3Cloud.aS3Cloud;
+import static org.cryptomator.domain.SmbCloud.aSmbCloud;
import static org.cryptomator.domain.WebDavCloud.aWebDavCloudCloud;
@Singleton
@@ -79,6 +81,13 @@ public Cloud fromEntity(CloudEntity entity) {
.withPassword(entity.getAccessToken()) //
.withCertificate(entity.getWebdavCertificate()) //
.build();
+ case SMB:
+ return aSmbCloud() //
+ .withId(entity.getId()) //
+ .withUrl(entity.getUrl()) //
+ .withUsername(entity.getUsername()) //
+ .withPassword(entity.getAccessToken()) //
+ .build();
default:
throw new IllegalStateException("Unhandled enum constant " + type);
}
@@ -123,6 +132,11 @@ public CloudEntity toEntity(Cloud domainObject) {
result.setUsername(((WebDavCloud) domainObject).username());
result.setWebdavCertificate(((WebDavCloud) domainObject).certificate());
break;
+ case SMB:
+ result.setAccessToken(((SmbCloud) domainObject).password());
+ result.setUrl(((SmbCloud) domainObject).url());
+ result.setUsername(((SmbCloud) domainObject).username());
+ break;
default:
throw new IllegalStateException("Unhandled enum constant " + domainObject.type());
}
diff --git a/domain/src/main/java/org/cryptomator/domain/CloudType.kt b/domain/src/main/java/org/cryptomator/domain/CloudType.kt
index a003c02565..558f159ff1 100644
--- a/domain/src/main/java/org/cryptomator/domain/CloudType.kt
+++ b/domain/src/main/java/org/cryptomator/domain/CloudType.kt
@@ -1,5 +1,5 @@
package org.cryptomator.domain
enum class CloudType {
- DROPBOX, GOOGLE_DRIVE, ONEDRIVE, PCLOUD, WEBDAV, LOCAL, S3, CRYPTO
+ DROPBOX, GOOGLE_DRIVE, ONEDRIVE, PCLOUD, WEBDAV, LOCAL, S3, CRYPTO, SMB
}
diff --git a/domain/src/main/java/org/cryptomator/domain/SmbCloud.java b/domain/src/main/java/org/cryptomator/domain/SmbCloud.java
new file mode 100644
index 0000000000..43651ed42d
--- /dev/null
+++ b/domain/src/main/java/org/cryptomator/domain/SmbCloud.java
@@ -0,0 +1,145 @@
+package org.cryptomator.domain;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+/**
+ * SMB Cloud implementation.
+ * Currently just a skeleton for the first step of SMB support.
+ */
+public class SmbCloud implements Cloud {
+
+ private final Long id;
+ private final String url;
+ private final String username;
+ private final String password;
+
+ private SmbCloud(Builder builder) {
+ this.id = builder.id;
+ this.url = builder.url;
+ this.username = builder.username;
+ this.password = builder.password;
+ }
+
+ public static Builder aSmbCloud() {
+ return new Builder();
+ }
+
+ public static Builder aCopyOf(SmbCloud smbCloud) {
+ return new Builder() //
+ .withId(smbCloud.id()) //
+ .withUrl(smbCloud.url()) //
+ .withUsername(smbCloud.username()) //
+ .withPassword(smbCloud.password());
+ }
+
+ @Override
+ public Long id() {
+ return id;
+ }
+
+ @Override
+ public boolean configurationMatches(Cloud cloud) {
+ return cloud instanceof SmbCloud && configurationMatches((SmbCloud) cloud);
+ }
+
+ private boolean configurationMatches(SmbCloud cloud) {
+ return Objects.equals(url, cloud.url) && Objects.equals(username, cloud.username);
+ }
+
+ @Override
+ public CloudType type() {
+ return CloudType.SMB;
+ }
+
+ public String password() {
+ return password;
+ }
+
+ public String url() {
+ return url;
+ }
+
+ public String username() {
+ return username;
+ }
+
+ @Override
+ public boolean persistent() {
+ return true;
+ }
+
+ @Override
+ public boolean requiresNetwork() {
+ return true;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ @NotNull
+ @Override
+ public String toString() {
+ return "SMB";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ return internalEquals((SmbCloud) obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return id == null ? 0 : id.hashCode();
+ }
+
+ private boolean internalEquals(SmbCloud obj) {
+ return Objects.equals(id, obj.id);
+ }
+
+ public static class Builder {
+
+ private Long id;
+ private String password;
+ private String url;
+ private String username;
+
+ private Builder() {
+ }
+
+ public Builder withId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder withUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+ public Builder withPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+ public Builder withUrl(String url) {
+ this.url = url;
+ return this;
+ }
+
+ public SmbCloud build() {
+ return new SmbCloud(this);
+ }
+
+ }
+
+}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt
index 60d1d97989..1c1d582372 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt
@@ -46,6 +46,13 @@ enum class CloudTypeModel(builder: Builder) {
.withVaultSelectedImageResource(R.drawable.s3_vault_selected) //
.withMultiInstances()
), //
+ SMB(
+ Builder("SMB", R.string.cloud_names_smb) //
+ .withCloudImageResource(R.drawable.webdav) //
+ .withVaultImageResource(R.drawable.webdav_vault) //
+ .withVaultSelectedImageResource(R.drawable.webdav_vault_selected) //
+ .withMultiInstances()
+ ), //
LOCAL(
Builder("LOCAL", R.string.cloud_names_local_storage) //
.withCloudImageResource(R.drawable.local_fs) //
diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/SmbCloudModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/SmbCloudModel.kt
new file mode 100644
index 0000000000..b524362a71
--- /dev/null
+++ b/presentation/src/main/java/org/cryptomator/presentation/model/SmbCloudModel.kt
@@ -0,0 +1,40 @@
+package org.cryptomator.presentation.model
+
+import org.cryptomator.domain.Cloud
+import org.cryptomator.domain.SmbCloud
+import org.cryptomator.presentation.R
+
+/**
+ * SMB Cloud model.
+ * Currently just a skeleton for the first step of SMB support.
+ */
+class SmbCloudModel(cloud: Cloud) : CloudModel(cloud) {
+
+ override fun name(): Int {
+ return R.string.cloud_names_smb
+ }
+
+ override fun username(): String? {
+ return smbCloud().username()
+ }
+
+ override fun cloudType(): CloudTypeModel {
+ return CloudTypeModel.SMB
+ }
+
+ fun url(): String? {
+ return smbCloud().url()
+ }
+
+ fun password(): String? {
+ return smbCloud().password()
+ }
+
+ fun id(): Long? {
+ return smbCloud().id()
+ }
+
+ private fun smbCloud(): SmbCloud {
+ return toCloud() as SmbCloud
+ }
+}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/mappers/CloudModelMapper.kt b/presentation/src/main/java/org/cryptomator/presentation/model/mappers/CloudModelMapper.kt
index 0006b4571e..4aa977f097 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/model/mappers/CloudModelMapper.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/model/mappers/CloudModelMapper.kt
@@ -11,6 +11,7 @@ import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel
+import org.cryptomator.presentation.model.SmbCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import javax.inject.Inject
@@ -31,6 +32,7 @@ class CloudModelMapper @Inject constructor() : ModelMapper()
CloudTypeModel.S3 -> S3CloudModel(domainObject)
CloudTypeModel.CRYPTO -> CryptoCloudModel(domainObject)
CloudTypeModel.WEBDAV -> WebDavCloudModel(domainObject)
+ CloudTypeModel.SMB -> SmbCloudModel(domainObject)
null -> throw IllegalStateException("The type of the object shouldn't be null")
}
}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt
index 73dbce2a69..92e9c1d95a 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/AuthenticateCloudPresenter.kt
@@ -13,6 +13,7 @@ import org.cryptomator.domain.DropboxCloud
import org.cryptomator.domain.GoogleDriveCloud
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.PCloud
+import org.cryptomator.domain.SmbCloud
import org.cryptomator.domain.WebDavCloud
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.FatalBackendException
@@ -69,6 +70,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
OnedriveAuthStrategy(), //
PCloudAuthStrategy(), //
WebDAVAuthStrategy(), //
+ SmbAuthStrategy(), //
S3AuthStrategy(), //
LocalStorageAuthStrategy() //
)
@@ -117,6 +119,7 @@ class AuthenticateCloudPresenter @Inject constructor( //
return when (cloud.type()) {
CloudType.DROPBOX -> DropboxCloud.aCopyOf(cloud as DropboxCloud).withUsername(username).build()
CloudType.ONEDRIVE -> OnedriveCloud.aCopyOf(cloud as OnedriveCloud).withUsername(username).build()
+ CloudType.SMB -> SmbCloud.aCopyOf(cloud as SmbCloud).withUsername(username).build()
else -> throw IllegalStateException("Cloud " + cloud.type() + " is not supported")
}
}
@@ -395,6 +398,18 @@ class AuthenticateCloudPresenter @Inject constructor( //
}
}
+ private inner class SmbAuthStrategy : AuthStrategy {
+
+ override fun supports(cloud: CloudModel): Boolean {
+ return cloud.cloudType() == CloudTypeModel.SMB
+ }
+
+ override fun resumed(intent: AuthenticateCloudIntent) {
+ // SMB authentication is not yet implemented
+ failAuthentication(intent.cloud().name())
+ }
+ }
+
fun onAcceptWebDavCertificateClicked(cloud: WebDavCloud, certificate: X509Certificate) {
try {
val webDavCloudWithAcceptedCert = WebDavCloud.aCopyOf(cloud) //
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
index 6c3f205b30..ee7285ec5b 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
@@ -128,6 +128,7 @@ class CloudConnectionListPresenter @Inject constructor( //
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.webDavAddOrChangeIntent())
CloudTypeModel.PCLOUD -> requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), Intents.authenticatePCloudIntent())
CloudTypeModel.S3 -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.s3AddOrChangeIntent())
+ CloudTypeModel.SMB -> Toast.makeText(activity().applicationContext, "SMB not yet implemented", Toast.LENGTH_SHORT).show()
CloudTypeModel.LOCAL -> openDocumentTree()
else -> throw IllegalStateException("Cloud type is not supported")
}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt
index f72c032ead..39b6fc21bb 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt
@@ -5,6 +5,7 @@ import org.cryptomator.domain.LocalStorageCloud
import org.cryptomator.domain.OnedriveCloud
import org.cryptomator.domain.PCloud
import org.cryptomator.domain.S3Cloud
+import org.cryptomator.domain.SmbCloud
import org.cryptomator.domain.WebDavCloud
import org.cryptomator.domain.di.PerView
import org.cryptomator.domain.exception.FatalBackendException
@@ -22,6 +23,7 @@ import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel
+import org.cryptomator.presentation.model.SmbCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.CloudSettingsView
@@ -44,7 +46,8 @@ class CloudSettingsPresenter @Inject constructor( //
CloudTypeModel.ONEDRIVE, //
CloudTypeModel.PCLOUD, //
CloudTypeModel.S3, //
- CloudTypeModel.WEBDAV
+ CloudTypeModel.WEBDAV, //
+ CloudTypeModel.SMB
)
fun loadClouds() {
@@ -101,6 +104,7 @@ class CloudSettingsPresenter @Inject constructor( //
CloudTypeModel.ONEDRIVE -> return context().getString(R.string.screen_cloud_settings_onedrive_connections)
CloudTypeModel.PCLOUD -> return context().getString(R.string.screen_cloud_settings_pcloud_connections)
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
+ CloudTypeModel.SMB -> return context().getString(R.string.cloud_names_smb)
CloudTypeModel.S3 -> return context().getString(R.string.screen_cloud_settings_s3_connections)
CloudTypeModel.LOCAL -> return context().getString(R.string.screen_cloud_settings_local_storage_locations)
else -> {}
@@ -138,6 +142,7 @@ class CloudSettingsPresenter @Inject constructor( //
it.add(aOnedriveCloud())
it.add(aPCloud())
it.add(aWebdavCloud())
+ it.add(aSmbCloud())
it.add(aS3Cloud())
it.add(aLocalCloud())
}
@@ -175,6 +180,10 @@ class CloudSettingsPresenter @Inject constructor( //
return WebDavCloudModel(WebDavCloud.aWebDavCloudCloud().build())
}
+ private fun aSmbCloud(): SmbCloudModel {
+ return SmbCloudModel(SmbCloud.aSmbCloud().build())
+ }
+
private fun aS3Cloud(): S3CloudModel {
return S3CloudModel(S3Cloud.aS3Cloud().build())
}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/CryptomatorVariantsActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/CryptomatorVariantsActivity.kt
index a1033bcd84..9448385b71 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/CryptomatorVariantsActivity.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/CryptomatorVariantsActivity.kt
@@ -18,13 +18,13 @@ class CryptomatorVariantsActivity : BaseActivitypCloud
WebDAV
S3
+ SMB
Local storage
From 50fde8d9641910d4e29519e33c3ce916d6e655f1 Mon Sep 17 00:00:00 2001
From: Goldmaster <7105970+Goldmaster@users.noreply.github.com>
Date: Sun, 19 Jul 2026 19:41:10 +0100
Subject: [PATCH 2/9] Add SMB settings interface.
---
.../cloud/smb/SmbCloudContentRepository.kt | 6 +-
.../domain/usecases/cloud/ConnectToSmb.java | 23 ++++
presentation/src/main/AndroidManifest.xml | 3 +
.../di/component/ActivityComponent.java | 6 +
.../intent/SmbAddOrChangeIntent.java | 14 +++
.../presenter/CloudConnectionListPresenter.kt | 10 +-
.../presenter/SmbAddOrChangePresenter.kt | 105 ++++++++++++++++++
.../ui/activity/SmbAddOrChangeActivity.kt | 38 +++++++
.../ui/activity/view/SmbAddOrChangeView.kt | 9 ++
.../ui/fragment/SmbAddOrChangeFragment.kt | 85 ++++++++++++++
.../main/res/layout/fragment_setup_smb.xml | 64 +++++++++++
11 files changed, 359 insertions(+), 4 deletions(-)
create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/cloud/ConnectToSmb.java
create mode 100644 presentation/src/main/java/org/cryptomator/presentation/intent/SmbAddOrChangeIntent.java
create mode 100644 presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/activity/SmbAddOrChangeActivity.kt
create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SmbAddOrChangeView.kt
create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt
create mode 100644 presentation/src/main/res/layout/fragment_setup_smb.xml
diff --git a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
index 0b67f19b9d..a4bee46bbd 100644
--- a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
+++ b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
@@ -80,12 +80,12 @@ internal class SmbCloudContentRepository(
throw UnsupportedOperationException("SMB not yet implemented")
}
- override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: SmbCloud): String {
+ override fun logout(cloud: SmbCloud) {
throw UnsupportedOperationException("SMB not yet implemented")
}
- override fun logout(cloud: SmbCloud) {
- throw UnsupportedOperationException("SMB not yet implemented")
+ override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: SmbCloud): String {
+ throw UnsupportedOperationException("SMB protocol is not yet implemented. This is just a UI placeholder.")
}
}
}
diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ConnectToSmb.java b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ConnectToSmb.java
new file mode 100644
index 0000000000..28bd38d49c
--- /dev/null
+++ b/domain/src/main/java/org/cryptomator/domain/usecases/cloud/ConnectToSmb.java
@@ -0,0 +1,23 @@
+package org.cryptomator.domain.usecases.cloud;
+
+import org.cryptomator.domain.SmbCloud;
+import org.cryptomator.domain.exception.BackendException;
+import org.cryptomator.domain.repository.CloudContentRepository;
+import org.cryptomator.generator.Parameter;
+import org.cryptomator.generator.UseCase;
+
+@UseCase
+public class ConnectToSmb {
+
+ private final CloudContentRepository cloudContentRepository;
+ private final SmbCloud cloud;
+
+ public ConnectToSmb(CloudContentRepository cloudContentRepository, @Parameter SmbCloud cloud) {
+ this.cloudContentRepository = cloudContentRepository;
+ this.cloud = cloud;
+ }
+
+ public void execute() throws BackendException {
+ cloudContentRepository.checkAuthenticationAndRetrieveCurrentAccount(cloud);
+ }
+}
diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml
index ca2c57f1c2..d8a5692355 100644
--- a/presentation/src/main/AndroidManifest.xml
+++ b/presentation/src/main/AndroidManifest.xml
@@ -126,6 +126,9 @@
+
diff --git a/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java b/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java
index a47963aed3..81e9d7bdf6 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java
+++ b/presentation/src/main/java/org/cryptomator/presentation/di/component/ActivityComponent.java
@@ -22,6 +22,7 @@
import org.cryptomator.presentation.ui.activity.SetPasswordActivity;
import org.cryptomator.presentation.ui.activity.SettingsActivity;
import org.cryptomator.presentation.ui.activity.SharedFilesActivity;
+import org.cryptomator.presentation.ui.activity.SmbAddOrChangeActivity;
import org.cryptomator.presentation.ui.activity.TextEditorActivity;
import org.cryptomator.presentation.ui.activity.UnlockVaultActivity;
import org.cryptomator.presentation.ui.activity.VaultListActivity;
@@ -36,6 +37,7 @@
import org.cryptomator.presentation.ui.fragment.ImagePreviewFragment;
import org.cryptomator.presentation.ui.fragment.S3AddOrChangeFragment;
import org.cryptomator.presentation.ui.fragment.SetPasswordFragment;
+import org.cryptomator.presentation.ui.fragment.SmbAddOrChangeFragment;
import org.cryptomator.presentation.ui.fragment.SharedFilesFragment;
import org.cryptomator.presentation.ui.fragment.TextEditorFragment;
import org.cryptomator.presentation.ui.fragment.UnlockVaultFragment;
@@ -130,6 +132,10 @@ public interface ActivityComponent {
void inject(S3AddOrChangeFragment s3AddOrChangeFragment);
+ void inject(SmbAddOrChangeActivity smbAddOrChangeActivity);
+
+ void inject(SmbAddOrChangeFragment smbAddOrChangeFragment);
+
void inject(CryptomatorVariantsActivity cryptomatorVariantsActivity);
void inject(WelcomeActivity welcomeActivity);
diff --git a/presentation/src/main/java/org/cryptomator/presentation/intent/SmbAddOrChangeIntent.java b/presentation/src/main/java/org/cryptomator/presentation/intent/SmbAddOrChangeIntent.java
new file mode 100644
index 0000000000..4d807e4893
--- /dev/null
+++ b/presentation/src/main/java/org/cryptomator/presentation/intent/SmbAddOrChangeIntent.java
@@ -0,0 +1,14 @@
+package org.cryptomator.presentation.intent;
+
+import org.cryptomator.generator.Intent;
+import org.cryptomator.generator.Optional;
+import org.cryptomator.presentation.model.SmbCloudModel;
+import org.cryptomator.presentation.ui.activity.SmbAddOrChangeActivity;
+
+@Intent(SmbAddOrChangeActivity.class)
+public interface SmbAddOrChangeIntent {
+
+ @Optional
+ SmbCloudModel smbCloud();
+
+}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
index ee7285ec5b..48ff06e08e 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
@@ -25,6 +25,7 @@ import org.cryptomator.presentation.model.CloudModel
import org.cryptomator.presentation.model.CloudTypeModel
import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.S3CloudModel
+import org.cryptomator.presentation.model.SmbCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
import org.cryptomator.presentation.model.mappers.CloudModelMapper
import org.cryptomator.presentation.ui.activity.view.CloudConnectionListView
@@ -128,7 +129,7 @@ class CloudConnectionListPresenter @Inject constructor( //
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.webDavAddOrChangeIntent())
CloudTypeModel.PCLOUD -> requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), Intents.authenticatePCloudIntent())
CloudTypeModel.S3 -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.s3AddOrChangeIntent())
- CloudTypeModel.SMB -> Toast.makeText(activity().applicationContext, "SMB not yet implemented", Toast.LENGTH_SHORT).show()
+ CloudTypeModel.SMB -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.smbAddOrChangeIntent())
CloudTypeModel.LOCAL -> openDocumentTree()
else -> throw IllegalStateException("Cloud type is not supported")
}
@@ -185,6 +186,13 @@ class CloudConnectionListPresenter @Inject constructor( //
.withWebDavCloud(cloudModel as WebDavCloudModel)
)
}
+ cloudModel.cloudType() == CloudTypeModel.SMB -> {
+ requestActivityResult(
+ ActivityResultCallbacks.addChangeMultiCloud(), //
+ Intents.smbAddOrChangeIntent() //
+ .withSmbCloud(cloudModel as SmbCloudModel)
+ )
+ }
cloudModel.cloudType() == CloudTypeModel.S3 -> {
requestActivityResult(
ActivityResultCallbacks.addChangeMultiCloud(), //
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
new file mode 100644
index 0000000000..2b6d0af6f3
--- /dev/null
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
@@ -0,0 +1,105 @@
+package org.cryptomator.presentation.presenter
+
+import android.widget.Toast
+import org.cryptomator.domain.Cloud
+import org.cryptomator.domain.SmbCloud
+import org.cryptomator.domain.di.PerView
+import org.cryptomator.domain.usecases.cloud.AddOrChangeCloudConnectionUseCase
+import org.cryptomator.domain.usecases.cloud.ConnectToSmbUseCase
+import org.cryptomator.presentation.R
+import org.cryptomator.presentation.exception.ExceptionHandlers
+import org.cryptomator.presentation.model.ProgressModel
+import org.cryptomator.presentation.model.ProgressStateModel
+import org.cryptomator.presentation.ui.activity.view.SmbAddOrChangeView
+import org.cryptomator.util.crypto.CredentialCryptor
+import javax.inject.Inject
+
+@PerView
+class SmbAddOrChangePresenter @Inject internal constructor( //
+ private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
+ private val connectToSmbUseCase: ConnectToSmbUseCase, //
+ exceptionMappings: ExceptionHandlers
+) : Presenter(exceptionMappings) {
+
+ fun checkUserInput(urlPort: String, username: String, password: String, cloudId: Long?) {
+ var statusMessage: String? = null
+
+ if (password.isEmpty()) {
+ statusMessage = getString(R.string.screen_webdav_settings_msg_password_must_not_be_empty)
+ }
+ if (username.isEmpty()) {
+ statusMessage = getString(R.string.screen_webdav_settings_msg_username_must_not_be_empty)
+ }
+ if (urlPort.isEmpty()) {
+ statusMessage = getString(R.string.screen_webdav_settings_msg_url_must_not_be_empty)
+ } else if (!isValid(urlPort)) {
+ statusMessage = getString(R.string.screen_webdav_settings_msg_url_is_invalid)
+ }
+ if (statusMessage != null) {
+ Toast.makeText(context(), statusMessage, Toast.LENGTH_SHORT).show()
+ } else {
+ val urlPortWithoutTrailingSlash = if (urlPort.endsWith("/")) urlPort.substring(0, urlPort.length - 1) else urlPort
+ val encryptedPassword = encryptPassword(password)
+ view?.onCheckUserInputSucceeded(urlPortWithoutTrailingSlash, username, encryptedPassword, cloudId)
+ }
+ }
+
+ private fun encryptPassword(password: String): String {
+ return CredentialCryptor //
+ .getInstance(context()) //
+ .encrypt(password)
+ }
+
+ private fun isValid(urlPort: String): Boolean {
+ return urlPort.startsWith("smb://", ignoreCase = true)
+ }
+
+ private fun mapToCloud(username: String, password: String, hostPort: String, id: Long?): SmbCloud {
+ var builder = SmbCloud //
+ .aSmbCloud() //
+ .withUrl(hostPort) //
+ .withUsername(username) //
+ .withPassword(password)
+
+ if (id != null) {
+ builder = builder.withId(id)
+ }
+
+ return builder.build()
+ }
+
+ fun authenticate(username: String, password: String, urlPort: String, cloudId: Long?) {
+ authenticate(mapToCloud(username, password, urlPort, cloudId))
+ }
+
+ private fun authenticate(cloud: SmbCloud) {
+ view?.showProgress(ProgressModel(ProgressStateModel.AUTHENTICATION))
+ connectToSmbUseCase //
+ .withCloud(cloud) //
+ .run(object : DefaultResultHandler() {
+ override fun onSuccess(void: Void?) {
+ onCloudAuthenticated(cloud)
+ }
+
+ override fun onError(e: Throwable) {
+ view?.showProgress(ProgressModel.COMPLETED)
+ super.onError(e)
+ }
+ })
+ }
+
+ private fun onCloudAuthenticated(cloud: Cloud) {
+ save(cloud)
+ finishWithResult(CloudConnectionListPresenter.SELECTED_CLOUD, cloud)
+ }
+
+ private fun save(cloud: Cloud) {
+ addOrChangeCloudConnectionUseCase //
+ .withCloud(cloud) //
+ .run(DefaultResultHandler())
+ }
+
+ init {
+ unsubscribeOnDestroy(addOrChangeCloudConnectionUseCase, connectToSmbUseCase)
+ }
+}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SmbAddOrChangeActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SmbAddOrChangeActivity.kt
new file mode 100644
index 0000000000..0e0e6bc80c
--- /dev/null
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SmbAddOrChangeActivity.kt
@@ -0,0 +1,38 @@
+package org.cryptomator.presentation.ui.activity
+
+import androidx.fragment.app.Fragment
+import org.cryptomator.generator.Activity
+import org.cryptomator.generator.InjectIntent
+import org.cryptomator.presentation.R
+import org.cryptomator.presentation.databinding.ActivityLayoutBinding
+import org.cryptomator.presentation.intent.SmbAddOrChangeIntent
+import org.cryptomator.presentation.presenter.SmbAddOrChangePresenter
+import org.cryptomator.presentation.ui.activity.view.SmbAddOrChangeView
+import org.cryptomator.presentation.ui.fragment.SmbAddOrChangeFragment
+import javax.inject.Inject
+
+@Activity
+class SmbAddOrChangeActivity : BaseActivity(ActivityLayoutBinding::inflate),
+ SmbAddOrChangeView {
+
+ @Inject
+ lateinit var smbAddOrChangePresenter: SmbAddOrChangePresenter
+
+ @InjectIntent
+ lateinit var smbAddOrChangeIntent: SmbAddOrChangeIntent
+
+ override fun setupView() {
+ binding.mtToolbar.toolbar.setTitle(R.string.cloud_names_smb)
+ setSupportActionBar(binding.mtToolbar.toolbar)
+ }
+
+ override fun createFragment(): Fragment = SmbAddOrChangeFragment.newInstance(smbAddOrChangeIntent.smbCloud())
+
+ override fun onCheckUserInputSucceeded(urlPort: String, username: String, password: String, cloudId: Long?) {
+ smbAddOrChangeFragment().hideKeyboard()
+ smbAddOrChangePresenter.authenticate(username, password, urlPort, cloudId)
+ }
+
+ private fun smbAddOrChangeFragment(): SmbAddOrChangeFragment = getCurrentFragment(R.id.fragment_container) as SmbAddOrChangeFragment
+
+}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SmbAddOrChangeView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SmbAddOrChangeView.kt
new file mode 100644
index 0000000000..c2173e9782
--- /dev/null
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SmbAddOrChangeView.kt
@@ -0,0 +1,9 @@
+package org.cryptomator.presentation.ui.activity.view
+
+import org.cryptomator.presentation.model.ProgressModel
+
+interface SmbAddOrChangeView : View {
+
+ fun onCheckUserInputSucceeded(urlPort: String, username: String, password: String, cloudId: Long?)
+
+}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt
new file mode 100644
index 0000000000..6c76a8c83b
--- /dev/null
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt
@@ -0,0 +1,85 @@
+package org.cryptomator.presentation.ui.fragment
+
+import android.os.Bundle
+import android.view.inputmethod.EditorInfo
+import org.cryptomator.generator.Fragment
+import org.cryptomator.presentation.databinding.FragmentSetupSmbBinding
+import org.cryptomator.presentation.model.SmbCloudModel
+import org.cryptomator.presentation.presenter.SmbAddOrChangePresenter
+import org.cryptomator.util.crypto.CredentialCryptor
+import org.cryptomator.util.crypto.FatalCryptoException
+import javax.inject.Inject
+import timber.log.Timber
+
+@Fragment
+class SmbAddOrChangeFragment : BaseFragment(FragmentSetupSmbBinding::inflate) {
+
+ @Inject
+ lateinit var smbAddOrChangePresenter: SmbAddOrChangePresenter
+
+ private var cloudId: Long? = null
+
+ private val smbCloudModel: SmbCloudModel?
+ get() = arguments?.getSerializable(ARG_SMB_CLOUD) as? SmbCloudModel
+
+ override fun setupView() {
+ binding.createCloudButton.setOnClickListener { createCloud() }
+ binding.createCloudButton.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ createCloud()
+ }
+ false
+ }
+
+ binding.urlPortEditText.text?.length?.let { binding.urlPortEditText.setSelection(it) }
+ showEditableCloudContent(smbCloudModel)
+ }
+
+ private fun showEditableCloudContent(smbCloudModel: SmbCloudModel?) {
+ if (smbCloudModel != null) {
+ binding.urlPortEditText.setText(smbCloudModel.url())
+ binding.userNameEditText.setText(smbCloudModel.username())
+ binding.passwordEditText.setText(getPassword(smbCloudModel.password()))
+ cloudId = smbCloudModel.id()
+ }
+ }
+
+ private fun getPassword(password: String?): String {
+ return if (password != null) {
+ try {
+ CredentialCryptor //
+ .getInstance(activity?.applicationContext) //
+ .decrypt(password)
+ } catch (e: FatalCryptoException) {
+ Timber.tag("SmbAddOrChangeFragment").e(e, "Unable to decrypt password, clearing it")
+ ""
+ }
+ } else ""
+ }
+
+ private fun createCloud() {
+ val urlPort = binding.urlPortEditText.text.toString().trim()
+ val username = binding.userNameEditText.text.toString().trim()
+ val password = binding.passwordEditText.text.toString().trim()
+
+ smbAddOrChangePresenter.checkUserInput(urlPort, username, password, cloudId)
+ }
+
+ fun hideKeyboard() {
+ hideKeyboard(binding.passwordEditText)
+ }
+
+ companion object {
+
+ private const val ARG_SMB_CLOUD = "SMB_CLOUD"
+
+ fun newInstance(cloudModel: SmbCloudModel?): SmbAddOrChangeFragment {
+ val result = SmbAddOrChangeFragment()
+ val args = Bundle()
+ args.putSerializable(ARG_SMB_CLOUD, cloudModel)
+ result.arguments = args
+ return result
+ }
+ }
+
+}
diff --git a/presentation/src/main/res/layout/fragment_setup_smb.xml b/presentation/src/main/res/layout/fragment_setup_smb.xml
new file mode 100644
index 0000000000..2537396fed
--- /dev/null
+++ b/presentation/src/main/res/layout/fragment_setup_smb.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 24f41f36329e747c264faae7ef67e69e54a2786f Mon Sep 17 00:00:00 2001
From: Goldmaster <7105970+Goldmaster@users.noreply.github.com>
Date: Sun, 19 Jul 2026 20:22:07 +0100
Subject: [PATCH 3/9] Ensured SMB options show up in the correct order.
---
.idea/misc.xml | 2 +-
buildsystem/dependencies.gradle | 2 +
data/build.gradle | 1 +
.../cloud/smb/SmbCloudContentRepository.kt | 77 +++++++++++++++----
.../data/db/mappers/CloudEntityMapper.java | 2 +
.../java/org/cryptomator/domain/SmbCloud.java | 15 +++-
gradle.properties | 2 +-
.../presentation/model/SmbCloudModel.kt | 4 +
.../presenter/CloudSettingsPresenter.kt | 4 +-
.../presenter/SmbAddOrChangePresenter.kt | 13 ++--
.../ui/activity/SmbAddOrChangeActivity.kt | 4 +-
.../ui/activity/view/SmbAddOrChangeView.kt | 4 +-
.../ui/fragment/SmbAddOrChangeFragment.kt | 4 +-
.../main/res/layout/fragment_setup_smb.xml | 15 ++++
presentation/src/main/res/values/strings.xml | 1 +
15 files changed, 117 insertions(+), 33 deletions(-)
diff --git a/.idea/misc.xml b/.idea/misc.xml
index edb77fab24..7261e4cdda 100755
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -54,7 +54,7 @@
-
+
diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle
index f41ed12c8f..e922c93c5d 100644
--- a/buildsystem/dependencies.gradle
+++ b/buildsystem/dependencies.gradle
@@ -100,6 +100,7 @@ ext {
minIoVersion = '9.0.3'
pcloudVersion = '1.10.0-dev.0001'
+ smbjVersion = '0.13.0'
staxVersion = '1.2.0' // needed for minIO
commonsCodecVersion = '1.22.0'
@@ -193,6 +194,7 @@ ext {
rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}",
rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}",
rxBinding : "com.jakewharton.rxbinding2:rxbinding:${rxBindingVersion}",
+ smbj : "com.hierynomus:smbj:${smbjVersion}",
stax : "stax:stax:${staxVersion}",
testingSupportLib : "com.android.support.test:testing-support-lib:${testingSupportLibVersion}",
timber : "com.jakewharton.timber:timber:${timberVersion}",
diff --git a/data/build.gradle b/data/build.gradle
index 69ca59c8f4..ff7bf522ac 100644
--- a/data/build.gradle
+++ b/data/build.gradle
@@ -157,6 +157,7 @@ dependencies {
addToFlavors(cloudFlavors, dependencies.pcloud)
+ implementation dependencies.smbj
implementation dependencies.stax
api dependencies.minIo
diff --git a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
index a4bee46bbd..74910f3143 100644
--- a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
+++ b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
@@ -5,47 +5,96 @@ import org.cryptomator.data.cloud.InterceptingCloudContentRepository
import org.cryptomator.domain.SmbCloud
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.exception.BackendException
+import org.cryptomator.domain.exception.NetworkConnectionException
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.UploadState
import java.io.File
+import java.io.InputStream
import java.io.OutputStream
+import java.net.URI
+import com.hierynomus.smbj.SMBClient
+import com.hierynomus.smbj.auth.AuthenticationContext
+import com.hierynomus.smbj.share.DiskShare
+import org.cryptomator.util.crypto.CredentialCryptor
+import timber.log.Timber
/**
* SMB Cloud content repository implementation.
- * Skeleton for the first step of SMB support.
*/
internal class SmbCloudContentRepository(
private val cloud: SmbCloud,
- context: Context
-) : InterceptingCloudContentRepository(Intercepted(cloud)) {
+ private val context: Context
+) : InterceptingCloudContentRepository(Intercepted(cloud, context)) {
@Throws(BackendException::class)
override fun throwWrappedIfRequired(e: Exception) {
- // Not yet implemented
+ if (e is com.hierynomus.smbj.common.SMBException) {
+ throw NetworkConnectionException(e)
+ }
}
- private class Intercepted(private val cloud: SmbCloud) : CloudContentRepository {
+ private class Intercepted(
+ private val cloud: SmbCloud,
+ private val context: Context
+ ) : CloudContentRepository {
+
+ private fun getDecryptedPassword(): String {
+ return CredentialCryptor.getInstance(context).decrypt(cloud.password())
+ }
+
+ private fun parseSmbUrl(url: String): Pair {
+ // Expected format: smb://host/share/
+ val uri = URI(url)
+ val host = uri.host ?: throw IllegalArgumentException("Invalid host in SMB URL")
+ val path = uri.path ?: ""
+ val share = path.split("/").filter { it.isNotEmpty() }.firstOrNull()
+ ?: throw IllegalArgumentException("Missing share name in SMB URL")
+ return Pair(host, share)
+ }
+
+ override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: SmbCloud): String {
+ val (host, share) = parseSmbUrl(cloud.url())
+ val client = SMBClient()
+ try {
+ client.connect(host).use { connection ->
+ val authContext = AuthenticationContext(cloud.username(), getDecryptedPassword().toCharArray(), cloud.domain() ?: "")
+ val session = connection.authenticate(authContext)
+ session.use { s ->
+ s.connectShare(share).use { ds ->
+ if (ds is DiskShare) {
+ return cloud.username()
+ } else {
+ throw NetworkConnectionException(RuntimeException("Specified share is not a disk share"))
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Timber.tag("SmbContentRepo").e(e, "SMB Authentication failed")
+ throw NetworkConnectionException(e)
+ }
+ }
override fun root(cloud: SmbCloud): SmbFolder {
- throw UnsupportedOperationException("SMB not yet implemented")
+ return SmbFolder(null, "", cloud)
}
override fun resolve(cloud: SmbCloud, path: String): SmbFolder {
- throw UnsupportedOperationException("SMB not yet implemented")
+ return SmbFolder(null, path, cloud)
}
override fun file(parent: SmbFolder, name: String): SmbFile {
- throw UnsupportedOperationException("SMB not yet implemented")
+ return SmbFile(parent, name, cloud)
}
override fun file(parent: SmbFolder, name: String, size: Long?): SmbFile {
- throw UnsupportedOperationException("SMB not yet implemented")
+ return SmbFile(parent, name, cloud, size)
}
override fun folder(parent: SmbFolder, name: String): SmbFolder {
- throw UnsupportedOperationException("SMB not yet implemented")
+ return SmbFolder(parent, name, cloud)
}
override fun exists(node: SmbNode): Boolean {
@@ -53,7 +102,7 @@ internal class SmbCloudContentRepository(
}
override fun list(folder: SmbFolder): List {
- throw UnsupportedOperationException("SMB not yet implemented")
+ return emptyList()
}
override fun create(folder: SmbFolder): SmbFolder {
@@ -81,11 +130,7 @@ internal class SmbCloudContentRepository(
}
override fun logout(cloud: SmbCloud) {
- throw UnsupportedOperationException("SMB not yet implemented")
- }
-
- override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: SmbCloud): String {
- throw UnsupportedOperationException("SMB protocol is not yet implemented. This is just a UI placeholder.")
+ // No-op for now
}
}
}
diff --git a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java
index f89ba6aa90..71e759f4ed 100644
--- a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java
+++ b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java
@@ -87,6 +87,7 @@ public Cloud fromEntity(CloudEntity entity) {
.withUrl(entity.getUrl()) //
.withUsername(entity.getUsername()) //
.withPassword(entity.getAccessToken()) //
+ .withDomain(entity.getWebdavCertificate()) //
.build();
default:
throw new IllegalStateException("Unhandled enum constant " + type);
@@ -136,6 +137,7 @@ public CloudEntity toEntity(Cloud domainObject) {
result.setAccessToken(((SmbCloud) domainObject).password());
result.setUrl(((SmbCloud) domainObject).url());
result.setUsername(((SmbCloud) domainObject).username());
+ result.setWebdavCertificate(((SmbCloud) domainObject).domain());
break;
default:
throw new IllegalStateException("Unhandled enum constant " + domainObject.type());
diff --git a/domain/src/main/java/org/cryptomator/domain/SmbCloud.java b/domain/src/main/java/org/cryptomator/domain/SmbCloud.java
index 43651ed42d..40302ac771 100644
--- a/domain/src/main/java/org/cryptomator/domain/SmbCloud.java
+++ b/domain/src/main/java/org/cryptomator/domain/SmbCloud.java
@@ -14,12 +14,14 @@ public class SmbCloud implements Cloud {
private final String url;
private final String username;
private final String password;
+ private final String domain;
private SmbCloud(Builder builder) {
this.id = builder.id;
this.url = builder.url;
this.username = builder.username;
this.password = builder.password;
+ this.domain = builder.domain;
}
public static Builder aSmbCloud() {
@@ -31,7 +33,8 @@ public static Builder aCopyOf(SmbCloud smbCloud) {
.withId(smbCloud.id()) //
.withUrl(smbCloud.url()) //
.withUsername(smbCloud.username()) //
- .withPassword(smbCloud.password());
+ .withPassword(smbCloud.password()) //
+ .withDomain(smbCloud.domain());
}
@Override
@@ -65,6 +68,10 @@ public String username() {
return username;
}
+ public String domain() {
+ return domain;
+ }
+
@Override
public boolean persistent() {
return true;
@@ -112,6 +119,7 @@ public static class Builder {
private String password;
private String url;
private String username;
+ private String domain;
private Builder() {
}
@@ -136,6 +144,11 @@ public Builder withUrl(String url) {
return this;
}
+ public Builder withDomain(String domain) {
+ this.domain = domain;
+ return this;
+ }
+
public SmbCloud build() {
return new SmbCloud(this);
}
diff --git a/gradle.properties b/gradle.properties
index 5db8c731d8..d72ea74fc1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-org.gradle.jvmargs=-Xmx2048m
+org.gradle.jvmargs=-Xmx4096m
android.useAndroidX=true
android.enableJetifier=true
android.jetifier.ignorelist=jackson-core,fastdoubleparser
diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/SmbCloudModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/SmbCloudModel.kt
index b524362a71..ac16a3f1ca 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/model/SmbCloudModel.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/model/SmbCloudModel.kt
@@ -18,6 +18,10 @@ class SmbCloudModel(cloud: Cloud) : CloudModel(cloud) {
return smbCloud().username()
}
+ fun domain(): String? {
+ return smbCloud().domain()
+ }
+
override fun cloudType(): CloudTypeModel {
return CloudTypeModel.SMB
}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt
index 39b6fc21bb..c5280a8c79 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudSettingsPresenter.kt
@@ -104,7 +104,7 @@ class CloudSettingsPresenter @Inject constructor( //
CloudTypeModel.ONEDRIVE -> return context().getString(R.string.screen_cloud_settings_onedrive_connections)
CloudTypeModel.PCLOUD -> return context().getString(R.string.screen_cloud_settings_pcloud_connections)
CloudTypeModel.WEBDAV -> return context().getString(R.string.screen_cloud_settings_webdav_connections)
- CloudTypeModel.SMB -> return context().getString(R.string.cloud_names_smb)
+ CloudTypeModel.SMB -> return context().getString(R.string.screen_cloud_settings_smb_connections)
CloudTypeModel.S3 -> return context().getString(R.string.screen_cloud_settings_s3_connections)
CloudTypeModel.LOCAL -> return context().getString(R.string.screen_cloud_settings_local_storage_locations)
else -> {}
@@ -142,8 +142,8 @@ class CloudSettingsPresenter @Inject constructor( //
it.add(aOnedriveCloud())
it.add(aPCloud())
it.add(aWebdavCloud())
- it.add(aSmbCloud())
it.add(aS3Cloud())
+ it.add(aSmbCloud())
it.add(aLocalCloud())
}
.filter { cloud -> !(FlavorConfig.isLiteFlavor && excludeApiCloudsInLite(cloud.cloudType())) } //
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
index 2b6d0af6f3..9b74b74d6e 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
@@ -21,7 +21,7 @@ class SmbAddOrChangePresenter @Inject internal constructor( //
exceptionMappings: ExceptionHandlers
) : Presenter(exceptionMappings) {
- fun checkUserInput(urlPort: String, username: String, password: String, cloudId: Long?) {
+ fun checkUserInput(urlPort: String, username: String, password: String, domain: String, cloudId: Long?) {
var statusMessage: String? = null
if (password.isEmpty()) {
@@ -40,7 +40,7 @@ class SmbAddOrChangePresenter @Inject internal constructor( //
} else {
val urlPortWithoutTrailingSlash = if (urlPort.endsWith("/")) urlPort.substring(0, urlPort.length - 1) else urlPort
val encryptedPassword = encryptPassword(password)
- view?.onCheckUserInputSucceeded(urlPortWithoutTrailingSlash, username, encryptedPassword, cloudId)
+ view?.onCheckUserInputSucceeded(urlPortWithoutTrailingSlash, username, encryptedPassword, domain, cloudId)
}
}
@@ -54,12 +54,13 @@ class SmbAddOrChangePresenter @Inject internal constructor( //
return urlPort.startsWith("smb://", ignoreCase = true)
}
- private fun mapToCloud(username: String, password: String, hostPort: String, id: Long?): SmbCloud {
+ private fun mapToCloud(username: String, password: String, hostPort: String, domain: String, id: Long?): SmbCloud {
var builder = SmbCloud //
.aSmbCloud() //
.withUrl(hostPort) //
.withUsername(username) //
- .withPassword(password)
+ .withPassword(password) //
+ .withDomain(domain)
if (id != null) {
builder = builder.withId(id)
@@ -68,8 +69,8 @@ class SmbAddOrChangePresenter @Inject internal constructor( //
return builder.build()
}
- fun authenticate(username: String, password: String, urlPort: String, cloudId: Long?) {
- authenticate(mapToCloud(username, password, urlPort, cloudId))
+ fun authenticate(username: String, password: String, urlPort: String, domain: String, cloudId: Long?) {
+ authenticate(mapToCloud(username, password, urlPort, domain, cloudId))
}
private fun authenticate(cloud: SmbCloud) {
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SmbAddOrChangeActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SmbAddOrChangeActivity.kt
index 0e0e6bc80c..a3c2927c98 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SmbAddOrChangeActivity.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/SmbAddOrChangeActivity.kt
@@ -28,9 +28,9 @@ class SmbAddOrChangeActivity : BaseActivity(ActivityLayou
override fun createFragment(): Fragment = SmbAddOrChangeFragment.newInstance(smbAddOrChangeIntent.smbCloud())
- override fun onCheckUserInputSucceeded(urlPort: String, username: String, password: String, cloudId: Long?) {
+ override fun onCheckUserInputSucceeded(urlPort: String, username: String, password: String, domain: String, cloudId: Long?) {
smbAddOrChangeFragment().hideKeyboard()
- smbAddOrChangePresenter.authenticate(username, password, urlPort, cloudId)
+ smbAddOrChangePresenter.authenticate(username, password, urlPort, domain, cloudId)
}
private fun smbAddOrChangeFragment(): SmbAddOrChangeFragment = getCurrentFragment(R.id.fragment_container) as SmbAddOrChangeFragment
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SmbAddOrChangeView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SmbAddOrChangeView.kt
index c2173e9782..7c1ca4f8ea 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SmbAddOrChangeView.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/SmbAddOrChangeView.kt
@@ -1,9 +1,7 @@
package org.cryptomator.presentation.ui.activity.view
-import org.cryptomator.presentation.model.ProgressModel
-
interface SmbAddOrChangeView : View {
- fun onCheckUserInputSucceeded(urlPort: String, username: String, password: String, cloudId: Long?)
+ fun onCheckUserInputSucceeded(urlPort: String, username: String, password: String, domain: String, cloudId: Long?)
}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt
index 6c76a8c83b..2a94038dc8 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt
@@ -40,6 +40,7 @@ class SmbAddOrChangeFragment : BaseFragment(FragmentSet
binding.urlPortEditText.setText(smbCloudModel.url())
binding.userNameEditText.setText(smbCloudModel.username())
binding.passwordEditText.setText(getPassword(smbCloudModel.password()))
+ binding.domainEditText.setText(smbCloudModel.domain())
cloudId = smbCloudModel.id()
}
}
@@ -61,8 +62,9 @@ class SmbAddOrChangeFragment : BaseFragment(FragmentSet
val urlPort = binding.urlPortEditText.text.toString().trim()
val username = binding.userNameEditText.text.toString().trim()
val password = binding.passwordEditText.text.toString().trim()
+ val domain = binding.domainEditText.text.toString().trim()
- smbAddOrChangePresenter.checkUserInput(urlPort, username, password, cloudId)
+ smbAddOrChangePresenter.checkUserInput(urlPort, username, password, domain, cloudId)
}
fun hideKeyboard() {
diff --git a/presentation/src/main/res/layout/fragment_setup_smb.xml b/presentation/src/main/res/layout/fragment_setup_smb.xml
index 2537396fed..edb7a79609 100644
--- a/presentation/src/main/res/layout/fragment_setup_smb.xml
+++ b/presentation/src/main/res/layout/fragment_setup_smb.xml
@@ -53,6 +53,21 @@
+
+
+
+
+
+
OneDrive connections
WebDAV connections
+ SMB connections
pCloud connections
S3 connections
Local storage locations
From e766cec80b84c96c9d141849dec23a942c3c6c0b Mon Sep 17 00:00:00 2001
From: Goldmaster <7105970+Goldmaster@users.noreply.github.com>
Date: Sun, 19 Jul 2026 20:25:52 +0100
Subject: [PATCH 4/9] Fixed SMB connections naming.
---
.../cryptomator/presentation/ui/adapter/CloudSettingsAdapter.kt | 1 +
1 file changed, 1 insertion(+)
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudSettingsAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudSettingsAdapter.kt
index aaa47b3a33..53266d6109 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudSettingsAdapter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/CloudSettingsAdapter.kt
@@ -45,6 +45,7 @@ constructor(private val context: Context) : RecyclerViewBaseAdapter binding.cloudName.text = context.getString(R.string.screen_cloud_settings_onedrive_connections)
CloudTypeModel.PCLOUD -> binding.cloudName.text = context.getString(R.string.screen_cloud_settings_pcloud_connections)
CloudTypeModel.S3 -> binding.cloudName.text = context.getString(R.string.screen_cloud_settings_s3_connections)
+ CloudTypeModel.SMB -> binding.cloudName.text = context.getString(R.string.screen_cloud_settings_smb_connections)
CloudTypeModel.WEBDAV -> binding.cloudName.text = context.getString(R.string.screen_cloud_settings_webdav_connections)
CloudTypeModel.LOCAL -> binding.cloudName.text = context.getString(R.string.screen_cloud_settings_local_storage_locations)
else -> {
From 441c9fb983ddf182e29cec8dd2455a5f0450ca79 Mon Sep 17 00:00:00 2001
From: Goldmaster <7105970+Goldmaster@users.noreply.github.com>
Date: Wed, 22 Jul 2026 16:55:44 +0100
Subject: [PATCH 5/9] Fixed SMB connection error.
---
.idea/appInsightsSettings.xml | 14 ++++++++++++++
.idea/misc.xml | 1 +
.../data/cloud/smb/SmbCloudContentRepository.kt | 8 ++++++--
.../presenter/SmbAddOrChangePresenter.kt | 14 +++++++++++++-
.../src/main/res/layout/fragment_setup_smb.xml | 2 +-
presentation/src/main/res/values/strings.xml | 1 +
6 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
index 371f2e299f..d66fe1703c 100644
--- a/.idea/appInsightsSettings.xml
+++ b/.idea/appInsightsSettings.xml
@@ -3,6 +3,20 @@
+
diff --git a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
index 74910f3143..d860dd04bc 100644
--- a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
+++ b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
@@ -46,11 +46,15 @@ internal class SmbCloudContentRepository(
private fun parseSmbUrl(url: String): Pair {
// Expected format: smb://host/share/
- val uri = URI(url)
+ val uri = try {
+ URI(url)
+ } catch (e: Exception) {
+ throw IllegalArgumentException("Invalid SMB URL format", e)
+ }
val host = uri.host ?: throw IllegalArgumentException("Invalid host in SMB URL")
val path = uri.path ?: ""
val share = path.split("/").filter { it.isNotEmpty() }.firstOrNull()
- ?: throw IllegalArgumentException("Missing share name in SMB URL")
+ ?: throw IllegalArgumentException("Missing share name in SMB URL. Format: smb://hostname/sharename/")
return Pair(host, share)
}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
index 9b74b74d6e..b0643c618d 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
@@ -30,10 +30,12 @@ class SmbAddOrChangePresenter @Inject internal constructor( //
if (username.isEmpty()) {
statusMessage = getString(R.string.screen_webdav_settings_msg_username_must_not_be_empty)
}
- if (urlPort.isEmpty()) {
+ if (urlPort.isEmpty() || urlPort == "smb://") {
statusMessage = getString(R.string.screen_webdav_settings_msg_url_must_not_be_empty)
} else if (!isValid(urlPort)) {
statusMessage = getString(R.string.screen_webdav_settings_msg_url_is_invalid)
+ } else if (!hasShare(urlPort)) {
+ statusMessage = getString(R.string.screen_smb_settings_msg_share_must_not_be_empty)
}
if (statusMessage != null) {
Toast.makeText(context(), statusMessage, Toast.LENGTH_SHORT).show()
@@ -54,6 +56,16 @@ class SmbAddOrChangePresenter @Inject internal constructor( //
return urlPort.startsWith("smb://", ignoreCase = true)
}
+ private fun hasShare(urlPort: String): Boolean {
+ try {
+ val uri = java.net.URI(urlPort)
+ val path = uri.path ?: ""
+ return path.split("/").any { it.isNotEmpty() }
+ } catch (e: Exception) {
+ return false
+ }
+ }
+
private fun mapToCloud(username: String, password: String, hostPort: String, domain: String, id: Long?): SmbCloud {
var builder = SmbCloud //
.aSmbCloud() //
diff --git a/presentation/src/main/res/layout/fragment_setup_smb.xml b/presentation/src/main/res/layout/fragment_setup_smb.xml
index edb7a79609..e7422ab220 100644
--- a/presentation/src/main/res/layout/fragment_setup_smb.xml
+++ b/presentation/src/main/res/layout/fragment_setup_smb.xml
@@ -13,7 +13,7 @@
android:id="@+id/url_port_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:hint="@string/screen_webdav_settings_url_port_label"
+ android:hint="URL (e.g. smb://hostname/share/)"
android:imeOptions="flagNoPersonalizedLearning"
android:inputType="textUri"
android:maxLines="1"
diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml
index 0aba3fdd05..ba8f0ab6a5 100644
--- a/presentation/src/main/res/values/strings.xml
+++ b/presentation/src/main/res/values/strings.xml
@@ -251,6 +251,7 @@
URL is invalid.
Username can\'t be empty.
Password can\'t be empty.
+ Share name can\'t be empty.
@string/cloud_names_s3
From 7a4637cc45758260abc10479813133d68cefe741 Mon Sep 17 00:00:00 2001
From: Goldmaster <7105970+Goldmaster@users.noreply.github.com>
Date: Wed, 22 Jul 2026 17:45:23 +0100
Subject: [PATCH 6/9] Fixed password input prompt not showing up.
---
.../cloud/smb/SmbCloudContentRepository.kt | 120 +++++++++++++++---
.../CloudConnectionSettingsBottomSheet.kt | 8 ++
2 files changed, 110 insertions(+), 18 deletions(-)
diff --git a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
index d860dd04bc..534b5552d3 100644
--- a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
+++ b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
@@ -1,17 +1,21 @@
package org.cryptomator.data.cloud.smb
import android.content.Context
+import com.hierynomus.msdtyp.AccessMask
+import com.hierynomus.mssmb2.SMB2ShareAccess
+import com.hierynomus.mssmb2.SMB2CreateDisposition
+import com.hierynomus.mssmb2.SMBApiException
import org.cryptomator.data.cloud.InterceptingCloudContentRepository
import org.cryptomator.domain.SmbCloud
import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.NetworkConnectionException
+import org.cryptomator.domain.exception.NoSuchCloudFileException
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.UploadState
import java.io.File
-import java.io.InputStream
import java.io.OutputStream
import java.net.URI
import com.hierynomus.smbj.SMBClient
@@ -19,6 +23,8 @@ import com.hierynomus.smbj.auth.AuthenticationContext
import com.hierynomus.smbj.share.DiskShare
import org.cryptomator.util.crypto.CredentialCryptor
import timber.log.Timber
+import java.util.Date
+import java.util.EnumSet
/**
* SMB Cloud content repository implementation.
@@ -30,6 +36,12 @@ internal class SmbCloudContentRepository(
@Throws(BackendException::class)
override fun throwWrappedIfRequired(e: Exception) {
+ if (e is SMBApiException) {
+ val status = e.status.value
+ if (status == 0xc000003aL || status == 0xc0000034L) { // STATUS_OBJECT_PATH_NOT_FOUND or STATUS_OBJECT_NAME_NOT_FOUND
+ throw NoSuchCloudFileException(e.message)
+ }
+ }
if (e is com.hierynomus.smbj.common.SMBException) {
throw NetworkConnectionException(e)
}
@@ -44,22 +56,23 @@ internal class SmbCloudContentRepository(
return CredentialCryptor.getInstance(context).decrypt(cloud.password())
}
- private fun parseSmbUrl(url: String): Pair {
- // Expected format: smb://host/share/
+ private fun parseSmbUrl(url: String): Triple {
+ // Expected format: smb://host/share/optional/path
val uri = try {
URI(url)
} catch (e: Exception) {
throw IllegalArgumentException("Invalid SMB URL format", e)
}
val host = uri.host ?: throw IllegalArgumentException("Invalid host in SMB URL")
- val path = uri.path ?: ""
- val share = path.split("/").filter { it.isNotEmpty() }.firstOrNull()
- ?: throw IllegalArgumentException("Missing share name in SMB URL. Format: smb://hostname/sharename/")
- return Pair(host, share)
+ val path = uri.path?.removePrefix("/") ?: ""
+ val parts = path.split("/").filter { it.isNotEmpty() }
+ val share = parts.firstOrNull() ?: throw IllegalArgumentException("Missing share name in SMB URL. Format: smb://hostname/sharename/")
+ val basePath = parts.drop(1).joinToString("/")
+ return Triple(host, share, basePath)
}
override fun checkAuthenticationAndRetrieveCurrentAccount(cloud: SmbCloud): String {
- val (host, share) = parseSmbUrl(cloud.url())
+ val (host, share, _) = parseSmbUrl(cloud.url())
val client = SMBClient()
try {
client.connect(host).use { connection ->
@@ -82,10 +95,13 @@ internal class SmbCloudContentRepository(
}
override fun root(cloud: SmbCloud): SmbFolder {
- return SmbFolder(null, "", cloud)
+ val (_, _, basePath) = parseSmbUrl(cloud.url())
+ // Root folder has basePath as its path (absolute from share root)
+ return SmbFolder(null, basePath, cloud)
}
override fun resolve(cloud: SmbCloud, path: String): SmbFolder {
+ // Path is assumed to be absolute from share root
return SmbFolder(null, path, cloud)
}
@@ -102,39 +118,107 @@ internal class SmbCloudContentRepository(
}
override fun exists(node: SmbNode): Boolean {
- throw UnsupportedOperationException("SMB not yet implemented")
+ val (host, share, _) = parseSmbUrl(cloud.url())
+ val client = SMBClient()
+ try {
+ client.connect(host).use { connection ->
+ val authContext = AuthenticationContext(cloud.username(), getDecryptedPassword().toCharArray(), cloud.domain() ?: "")
+ val session = connection.authenticate(authContext)
+ session.use { s ->
+ s.connectShare(share).use { ds ->
+ if (ds is DiskShare) {
+ return ds.fileExists(node.path) || ds.folderExists(node.path)
+ }
+ return false
+ }
+ }
+ }
+ } catch (e: Exception) {
+ return false
+ }
}
override fun list(folder: SmbFolder): List {
- return emptyList()
+ val (host, share, _) = parseSmbUrl(cloud.url())
+ val client = SMBClient()
+ try {
+ client.connect(host).use { connection ->
+ val authContext = AuthenticationContext(cloud.username(), getDecryptedPassword().toCharArray(), cloud.domain() ?: "")
+ val session = connection.authenticate(authContext)
+ session.use { s ->
+ s.connectShare(share).use { ds ->
+ if (ds is DiskShare) {
+ return ds.list(folder.path).map { fileInfo ->
+ if (fileInfo.fileName == "." || fileInfo.fileName == "..") {
+ null
+ } else {
+ val isDirectory = (fileInfo.fileAttributes and 0x10L) != 0L
+ if (isDirectory) {
+ SmbFolder(folder, fileInfo.fileName, cloud)
+ } else {
+ SmbFile(folder, fileInfo.fileName, cloud, fileInfo.endOfFile, Date(fileInfo.lastWriteTime.toEpochMillis()))
+ }
+ }
+ }.filterNotNull()
+ } else {
+ throw NetworkConnectionException(RuntimeException("Specified share is not a disk share"))
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Timber.tag("SmbContentRepo").e(e, "SMB Listing failed for path: ${folder.path}")
+ throw NetworkConnectionException(e)
+ }
}
override fun create(folder: SmbFolder): SmbFolder {
- throw UnsupportedOperationException("SMB not yet implemented")
+ throw UnsupportedOperationException("SMB create not yet implemented")
}
override fun move(source: SmbFolder, target: SmbFolder): SmbFolder {
- throw UnsupportedOperationException("SMB not yet implemented")
+ throw UnsupportedOperationException("SMB move not yet implemented")
}
override fun move(source: SmbFile, target: SmbFile): SmbFile {
- throw UnsupportedOperationException("SMB not yet implemented")
+ throw UnsupportedOperationException("SMB move not yet implemented")
}
override fun write(file: SmbFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): SmbFile {
- throw UnsupportedOperationException("SMB not yet implemented")
+ throw UnsupportedOperationException("SMB write not yet implemented")
}
override fun read(file: SmbFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) {
- throw UnsupportedOperationException("SMB not yet implemented")
+ val (host, share, _) = parseSmbUrl(cloud.url())
+ val client = SMBClient()
+ try {
+ client.connect(host).use { connection ->
+ val authContext = AuthenticationContext(cloud.username(), getDecryptedPassword().toCharArray(), cloud.domain() ?: "")
+ val session = connection.authenticate(authContext)
+ session.use { s ->
+ s.connectShare(share).use { ds ->
+ if (ds is DiskShare) {
+ ds.openFile(file.path, EnumSet.of(AccessMask.FILE_READ_DATA), null, SMB2ShareAccess.ALL, SMB2CreateDisposition.FILE_OPEN, null).use { f ->
+ f.inputStream.use { it.copyTo(data) }
+ }
+ } else {
+ throw NetworkConnectionException(RuntimeException("Specified share is not a disk share"))
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Timber.tag("SmbContentRepo").e(e, "SMB Read failed for path: ${file.path}")
+ throw NetworkConnectionException(e)
+ }
}
override fun delete(node: SmbNode) {
- throw UnsupportedOperationException("SMB not yet implemented")
+ throw UnsupportedOperationException("SMB delete not yet implemented")
}
override fun logout(cloud: SmbCloud) {
- // No-op for now
+ // No-op
}
}
}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/CloudConnectionSettingsBottomSheet.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/CloudConnectionSettingsBottomSheet.kt
index b13df542a8..6194478531 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/CloudConnectionSettingsBottomSheet.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/CloudConnectionSettingsBottomSheet.kt
@@ -11,6 +11,7 @@ import org.cryptomator.presentation.model.LocalStorageModel
import org.cryptomator.presentation.model.OnedriveCloudModel
import org.cryptomator.presentation.model.PCloudModel
import org.cryptomator.presentation.model.S3CloudModel
+import org.cryptomator.presentation.model.SmbCloudModel
import org.cryptomator.presentation.model.WebDavCloudModel
@BottomSheet(R.layout.dialog_bottom_sheet_cloud_settings)
@@ -30,6 +31,7 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet bindViewForWebDAV(cloudModel as WebDavCloudModel)
CloudTypeModel.PCLOUD -> bindViewForPCloud(cloudModel as PCloudModel)
CloudTypeModel.S3 -> bindViewForS3(cloudModel as S3CloudModel)
+ CloudTypeModel.SMB -> bindViewForSMB(cloudModel as SmbCloudModel)
CloudTypeModel.LOCAL -> bindViewForLocal(cloudModel as LocalStorageModel)
else -> throw IllegalStateException("Cloud model is not binded in the view")
}
@@ -76,6 +78,12 @@ class CloudConnectionSettingsBottomSheet : BaseBottomSheet
Date: Wed, 22 Jul 2026 18:06:47 +0100
Subject: [PATCH 7/9] Ability to create file within a vault.
---
.../cloud/smb/SmbCloudContentRepository.kt | 114 ++++++++++++++++--
gradle/wrapper/gradle-wrapper.properties | 2 +-
settings.gradle | 3 +
3 files changed, 106 insertions(+), 13 deletions(-)
diff --git a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
index 534b5552d3..b38563b226 100644
--- a/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
+++ b/data/src/main/java/org/cryptomator/data/cloud/smb/SmbCloudContentRepository.kt
@@ -2,27 +2,29 @@ package org.cryptomator.data.cloud.smb
import android.content.Context
import com.hierynomus.msdtyp.AccessMask
-import com.hierynomus.mssmb2.SMB2ShareAccess
import com.hierynomus.mssmb2.SMB2CreateDisposition
+import com.hierynomus.mssmb2.SMB2ShareAccess
import com.hierynomus.mssmb2.SMBApiException
+import com.hierynomus.smbj.SMBClient
+import com.hierynomus.smbj.auth.AuthenticationContext
+import com.hierynomus.smbj.share.DiskShare
import org.cryptomator.data.cloud.InterceptingCloudContentRepository
import org.cryptomator.domain.SmbCloud
-import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.exception.BackendException
+import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.NetworkConnectionException
import org.cryptomator.domain.exception.NoSuchCloudFileException
+import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
+import org.cryptomator.domain.usecases.cloud.Progress
import org.cryptomator.domain.usecases.cloud.UploadState
+import org.cryptomator.util.crypto.CredentialCryptor
+import timber.log.Timber
import java.io.File
import java.io.OutputStream
import java.net.URI
-import com.hierynomus.smbj.SMBClient
-import com.hierynomus.smbj.auth.AuthenticationContext
-import com.hierynomus.smbj.share.DiskShare
-import org.cryptomator.util.crypto.CredentialCryptor
-import timber.log.Timber
import java.util.Date
import java.util.EnumSet
@@ -96,12 +98,10 @@ internal class SmbCloudContentRepository(
override fun root(cloud: SmbCloud): SmbFolder {
val (_, _, basePath) = parseSmbUrl(cloud.url())
- // Root folder has basePath as its path (absolute from share root)
return SmbFolder(null, basePath, cloud)
}
override fun resolve(cloud: SmbCloud, path: String): SmbFolder {
- // Path is assumed to be absolute from share root
return SmbFolder(null, path, cloud)
}
@@ -173,7 +173,29 @@ internal class SmbCloudContentRepository(
}
override fun create(folder: SmbFolder): SmbFolder {
- throw UnsupportedOperationException("SMB create not yet implemented")
+ val (host, share, _) = parseSmbUrl(cloud.url())
+ val client = SMBClient()
+ try {
+ client.connect(host).use { connection ->
+ val authContext = AuthenticationContext(cloud.username(), getDecryptedPassword().toCharArray(), cloud.domain() ?: "")
+ val session = connection.authenticate(authContext)
+ session.use { s ->
+ s.connectShare(share).use { ds ->
+ if (ds is DiskShare) {
+ if (!ds.folderExists(folder.path)) {
+ ds.mkdir(folder.path)
+ }
+ return folder
+ } else {
+ throw NetworkConnectionException(RuntimeException("Specified share is not a disk share"))
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Timber.tag("SmbContentRepo").e(e, "SMB Create folder failed for path: ${folder.path}")
+ throw NetworkConnectionException(e)
+ }
}
override fun move(source: SmbFolder, target: SmbFolder): SmbFolder {
@@ -185,7 +207,52 @@ internal class SmbCloudContentRepository(
}
override fun write(file: SmbFile, data: DataSource, progressAware: ProgressAware, replace: Boolean, size: Long): SmbFile {
- throw UnsupportedOperationException("SMB write not yet implemented")
+ val (host, share, _) = parseSmbUrl(cloud.url())
+ val client = SMBClient()
+ try {
+ client.connect(host).use { connection ->
+ val authContext = AuthenticationContext(cloud.username(), getDecryptedPassword().toCharArray(), cloud.domain() ?: "")
+ val session = connection.authenticate(authContext)
+ session.use { s ->
+ s.connectShare(share).use { ds ->
+ if (ds is DiskShare) {
+ if (!replace && ds.fileExists(file.path)) {
+ throw CloudNodeAlreadyExistsException(file.name)
+ }
+ progressAware.onProgress(Progress.started(UploadState.upload(file)))
+ val disposition = if (replace) SMB2CreateDisposition.FILE_OVERWRITE_IF else SMB2CreateDisposition.FILE_CREATE
+ ds.openFile(file.path, EnumSet.of(AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, disposition, null).use { f ->
+ f.outputStream.use { outputStream ->
+ data.open(context)?.use { inputStream ->
+ val buffer = ByteArray(8192)
+ var bytesRead: Int
+ var totalTransferred = 0L
+ while (inputStream.read(buffer).also { bytesRead = it } != -1) {
+ outputStream.write(buffer, 0, bytesRead)
+ totalTransferred += bytesRead
+ progressAware.onProgress(
+ Progress.progress(UploadState.upload(file))
+ .between(0)
+ .and(size)
+ .withValue(totalTransferred)
+ )
+ }
+ }
+ }
+ }
+ // Retrieve file info after upload
+ val fileInfo = ds.getFileInformation(file.path)
+ return SmbFile(file.parent, file.name, cloud, fileInfo.standardInformation.endOfFile, Date(fileInfo.basicInformation.lastWriteTime.toEpochMillis()))
+ } else {
+ throw NetworkConnectionException(RuntimeException("Specified share is not a disk share"))
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Timber.tag("SmbContentRepo").e(e, "SMB Write failed for path: ${file.path}")
+ throw NetworkConnectionException(e)
+ }
}
override fun read(file: SmbFile, encryptedTmpFile: File?, data: OutputStream, progressAware: ProgressAware) {
@@ -214,7 +281,30 @@ internal class SmbCloudContentRepository(
}
override fun delete(node: SmbNode) {
- throw UnsupportedOperationException("SMB delete not yet implemented")
+ val (host, share, _) = parseSmbUrl(cloud.url())
+ val client = SMBClient()
+ try {
+ client.connect(host).use { connection ->
+ val authContext = AuthenticationContext(cloud.username(), getDecryptedPassword().toCharArray(), cloud.domain() ?: "")
+ val session = connection.authenticate(authContext)
+ session.use { s ->
+ s.connectShare(share).use { ds ->
+ if (ds is DiskShare) {
+ if (ds.folderExists(node.path)) {
+ ds.rmdir(node.path, true)
+ } else if (ds.fileExists(node.path)) {
+ ds.rm(node.path)
+ }
+ } else {
+ throw NetworkConnectionException(RuntimeException("Specified share is not a disk share"))
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Timber.tag("SmbContentRepo").e(e, "SMB Delete failed for path: ${node.path}")
+ throw NetworkConnectionException(e)
+ }
}
override fun logout(cloud: SmbCloud) {
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index aaaabb3cb9..4f5eb9dcc0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle b/settings.gradle
index 326464e57f..57438dda41 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1,4 @@
+plugins {
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0'
+}
include ':generator', ':presentation', ':generator-api', ':domain', ':data', ':util'
From ae2d5f3829b54b06ebcef34be406997f1f984b68 Mon Sep 17 00:00:00 2001
From: Goldmaster <7105970+Goldmaster@users.noreply.github.com>
Date: Wed, 22 Jul 2026 20:54:30 +0100
Subject: [PATCH 8/9] Recreated icons as best as from current files.
---
buildsystem/dependencies.gradle | 3 +++
.../presentation/model/CloudTypeModel.kt | 6 +++---
.../src/main/res/drawable-xxhdpi/smb.png | Bin 0 -> 4465 bytes
.../src/main/res/drawable/smb_vault.xml | 17 +++++++++++++++++
.../main/res/drawable/smb_vault_selected.xml | 17 +++++++++++++++++
5 files changed, 40 insertions(+), 3 deletions(-)
create mode 100644 presentation/src/main/res/drawable-xxhdpi/smb.png
create mode 100644 presentation/src/main/res/drawable/smb_vault.xml
create mode 100644 presentation/src/main/res/drawable/smb_vault_selected.xml
diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle
index e922c93c5d..89affab264 100644
--- a/buildsystem/dependencies.gradle
+++ b/buildsystem/dependencies.gradle
@@ -79,6 +79,8 @@ ext {
scaleImageViewVersion = '3.10.1-dev.0002'
+ slf4jAndroidVersion = '1.7.36'
+
lruFileCacheVersion = '1.2'
// KEEP IN SYNC WITH GENERATOR VERSION IN root build.gradle
@@ -194,6 +196,7 @@ ext {
rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}",
rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}",
rxBinding : "com.jakewharton.rxbinding2:rxbinding:${rxBindingVersion}",
+ slf4jAndroid : "org.slf4j:slf4j-android:${slf4jAndroidVersion}",
smbj : "com.hierynomus:smbj:${smbjVersion}",
stax : "stax:stax:${staxVersion}",
testingSupportLib : "com.android.support.test:testing-support-lib:${testingSupportLibVersion}",
diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt
index 1c1d582372..fe9da03f13 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt
@@ -48,9 +48,9 @@ enum class CloudTypeModel(builder: Builder) {
), //
SMB(
Builder("SMB", R.string.cloud_names_smb) //
- .withCloudImageResource(R.drawable.webdav) //
- .withVaultImageResource(R.drawable.webdav_vault) //
- .withVaultSelectedImageResource(R.drawable.webdav_vault_selected) //
+ .withCloudImageResource(R.drawable.smb) //
+ .withVaultImageResource(R.drawable.smb_vault) //
+ .withVaultSelectedImageResource(R.drawable.smb_vault_selected) //
.withMultiInstances()
), //
LOCAL(
diff --git a/presentation/src/main/res/drawable-xxhdpi/smb.png b/presentation/src/main/res/drawable-xxhdpi/smb.png
new file mode 100644
index 0000000000000000000000000000000000000000..0643432fae1d77541334dad43571f23e4f26fc65
GIT binary patch
literal 4465
zcmY*dXH-+&whf37iWETzHB><~Ap}8s6QxU+E`}t8p3sEQ{3wES=}kl=H0dBn6A+{~
zk=_i50TrYO5|Q@8eRte<-;aG(*>jaW$J%F%b7G9|XxhO1c;F~#06xnu%Dj`O3?}7Ba^lEm>)KxRbP_@W0FZx;Xt`eNUf%(?B6a>U>8^8VWma`CL~t@^(Q}x;QVXDy{sl
z{r~4vmO5|bzZz5hUk~N16zxCEoYV9lcF~>`O8qI^-AKz~1OQlkbhR|h10MXe^N7DP
zgAX0eZr-pzxX>N$kFXH#(}cSU^(+e_8mwsEf)?nTyz1_mmK;u)m)RZ~eLc`N*wA|Y
z{61XrAn9(xrd1KGQf-0fYVflrozY@BFHQs73LA^Gbd&GV{IfypL8JJi)!}RfpGRA8
zA0#W+&xp~{Y;wq0&}aN;-*C1J*F$=JtFFr#HIZUB5cIO0?iyytUng@Ny!gqtPGC6s
z)y5VnL0!k477gL2`Nw3XZRIx1*o*$gvN(s>kEtcZVEk6zUMtJHKOvDO@nQFev`v}IqYVh+5zKV|7~h7PY5vly&Mo$l{}
z1OygC7N#qS5XsV{o~{QR)g4}c3(pk%w`L3JUUYS;GeTw{f1|ESV6xDWJTDoz-Rp)=
zoxTn@$6p>iNqt%d(|g333jorjL^AI29}j>-Y2SLb9J0t^|2A|;GfdqbOBL{`71p~9
z>#Z9SoZ9{M;5FQ-JxvmIh&G#!8F+K;_yWzKZ?o=cp9FR`Jw(zWjh>rIi)NJjlQ|&1
za=PH=ov}#8VQXF*p%l@JLs6+Zjs)F`#Mt9C(L(QQ$Fqnv)1a`+Hl@eka*UNM}
zh&44n)Bu~Rh(~?e9DS+PZNCPeS`(mw!oE&UP7;ZQF^~G9H*o4ps~+{`oWh2JroR`f
zge}kbq6>c>>ZG8!VNHWjOF9*{AM~|~dY{HA=vDa;t)c|)uZgRjdlzUlXhvPoy4LIz
z1iTT>gbWA;^m%bI<%NdbP+6z0+UVT79iRb}`&Lo&bK|ABCimi%7hsC-jU8qMH7O-{
zqTRFQohTT;Oh;uWZ&?$q7+eI7-05aboJfr;rzZ7bsF6&%DZ)Tu*l19o?~j;EGk(*5
z5atyrf9oG}>XU8+^vY;OUMZa{
zw{g8yb}S@jTlPq{lueS3*<#(U)3AWr^AZemsKU%r2i70jfnFUc^5&rvI1zSA0{7Np
z`TEgge8=7c>zljX@;l$S^hwZ*wCZ{k&ghW7Bp~{-n<770^Qpn7&EoyveP>MKPM?da1+<1oi?s}cIFNx*G?RhBgALT;_Za#7+hEput_-3DVpL~aqUoM-o?dUL~z6RTm
zurTtQ7uzw$U$U~c1Din@aP;c#|=_g00}0AP8A
zPA!^}Z8%Q}Tdw!4vf>#DUoSq!Jf*j*B=Be&%Hkub{qdK+M#*S?B`O>r7iFF}#LF8!
zj{2J{zNR=Ee9KtCI&k;wUg95%whg4M)W5bfC7$2gQZ{gLuOvYRL+m}URZGh!c3^L1
zBqe;Q6@MW)=i;1S-^YRs+{UgB&%NEFy(vVv`T0*~>MG7x8LTVZBqMrtvo2E;sc9WQ
zAj;M@vAeQU5fB0mbC>u0ESu1+-4U0-JB>W}vohGc)j7kASeist<$iAE#f-LlQ1DQ@)Jsc
zwEtExiDQRC1!8Lvzu3GdcRmgkxj7%gs`=|iTE0-|c(`QV^R7q0ym#D>-}e~M4t{m9
zWcR)fQj>1E;^2h!1^6Y&yE7nTeLe^vjV_<7FSTmlKF&3x`R*b9t@5-pvx~`M#~05_P<}zODzqq~fqO+kR0-+-Tv)
z9k2D#pXp{=7JF{oNcEiJx!=1(My1`)(=|YxLAn;>knp}20SuHG(`Q*eAG+H~$3-4F
zw^Kj;bRr-=r=3qP7RxOaig=Nh{q?t
z*QJ$}%@?x(HL8ak(K_?5__QL=U%ZSgi*XBYv7v~gMvo5U!i}h2CRkKMD2S9_Z5Q;A
z)$}K%_PSRlrjuav==J@paPb4xu+xu1wnC(tXO(G@tgb37HwAu;(4*a7+1vE_XOpTO
zON}f4Nid@=T{057n#897N9EWvBmh;szxF?4{f^SUL}wc^(n;Z3k?M;mzW|8W?TwAA
z*0<&5->D{&Q40@h7p(7{P%EwdQdsBXCu;MREPpkxU}`C3D{!uj5xqCQOO1;rgOhl~
z1crG11j9FQe>DCPWO~2kwC60M$=`eR7#7JzgX0&^+^CFf*W|=n%m$JtSVZK9UEZh>
zpBiud+7$$@90}Q3R;I-ERx$kip;bF;E}fZg3S{=5+$sTgl1A?9CjkBA!Q=>I$WW6?
z^Z_peHDk=Hbe||#ETw!Cq-0!a{BC3m6ucck)g?i@J}tg@@}X>?Xh~d$ji--ZSq*^6
z+%5jVkEvUwd-{cDF_>%K2<2+t13bt>pkK07P)~wy-F#&IJ`kgmG8l6q{+KV
z>2P>!MH%dO^RPTmWttQx=O0N^7mfGvn1PGvOU+f1QkYq8zp|w%We7l~Ko-Nj)roArKTj4_EXC3@>2Iio#?GskWtr<8>aox;_C(>=^p7!as5OvtXF-GOVm^c7d2mAq*ws}B(~u;
z-|wc+fP+tqRs;SvW09x;YnP){oA9*DTuK6}_%ZFBxub(+E-q;3cXsmDNvwzElle&1
zqmjZSCZ?vxqoD2qGmMv4d&K>rhLI|If=g^cfrk6SaXHSO4#6tWcGezrHrrUP$*!3%
zn&JQgIU|n$k{&)sFVOX5cA^v~#pm{WYx-p}pjy4#j*R17#Myb;MuS2Q_E?89e9ODF-Xz5lz`e^3LnCj#GJGf1VgF0LCT5LfROU-8H)DO{#wLASavR(Z^
zu|kbEcI!SJb1XT!L!3FBc(~)omI^9E4tP3(=~-m9QAqH%2YSeLGM^^f&a6cwrM>=j
z+bN$h*7yDFzUe_u(JS|Rjg145c!gHg1#Dw;#!!Wo9$tpFatK+LQr2W{|0}9cJ1H^a
z&$n=!NLBKp&rn%3f2SLrJG}JJr^hCeVJ1(obSiutV=i+4Wq`HYEGql8&lLBtW!v3P
z{KFD%(;oBlaybBaLomOu#iniNh8g-Nmw>YuXnV4
zPGM9bWz|vJJO{_9KODN0T2*@I+qZACZLBV&=Y;SnxM7a88ro88ZipiYIBG2tm>{R&t2TN_N&_T;H`{X1x7^N261zT%&ZkL
zG~CxCD(x1ws_!S|9Vkb#7H#L0cEx0*`&vZBy)3}fr}38$S|0v%GSXUpjzsw~Jzphn
z&F=CNYYaF$4@_dV`P6n1xPzQkc~X*v{+UoZkJX`@%HXt@`n~D``v8}3bH>
z6zUQXQpCv^{nZ^5mJ5eW5wf_jhd1Lgg3)q#>j|q&41R($kiADN({+S*ywOs)_d_&!
z(W_6_7F{^aCm9h&*7j;VDFz|T#;w$!7#_KCdJvQfn-CuLE3M*?6hc~-(3#uy=CT1It~
z>dGSpX;UWxFDe_Y$7Qo(JA?WDgcX>nTfu%EH&MWv`vBazGYbsADd})qAZ{h@+gVmi
zkp1O`C}jX2eg!OmVPeVdHq-byzR~eSzI8Y4Au}#WohwyKI?IsQn8$Pa^elTOu?!H#9O6yvQ$-Gy8+SzOaqC{wND*5RLkn-wLSIc^S|3JfZkbM?oW(hQprA`7NvIqbsK5M6TL-j+_YCl
z`~jx9-XzoqQ8B{H&b2~bS9ZxrAzB8X>q$07B?~-A
z8_=3|HeGOir6WsyjXEZ>ZX{Umnh;HyGQCa#P<&7kK+g&gqFGzm+c{B(K#FW0tVw@O
z$oWo))cR|fo8(!l@|9|N=WV-Rxy~*g7W@o2Gxv8Wd`o1vE}Tjl6bj{j{MUbbfjrh2
g`rlu+9UQ0NBE>XxG;7YQ^Zy>YckXFbX+8}9ANzD-tN;K2
literal 0
HcmV?d00001
diff --git a/presentation/src/main/res/drawable/smb_vault.xml b/presentation/src/main/res/drawable/smb_vault.xml
new file mode 100644
index 0000000000..8f2fe67ccb
--- /dev/null
+++ b/presentation/src/main/res/drawable/smb_vault.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/presentation/src/main/res/drawable/smb_vault_selected.xml b/presentation/src/main/res/drawable/smb_vault_selected.xml
new file mode 100644
index 0000000000..8f2fe67ccb
--- /dev/null
+++ b/presentation/src/main/res/drawable/smb_vault_selected.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
From 13f4bdffb4499bccbfb1a3715618028872f946b7 Mon Sep 17 00:00:00 2001
From: Goldmaster <7105970+Goldmaster@users.noreply.github.com>
Date: Wed, 22 Jul 2026 21:10:19 +0100
Subject: [PATCH 9/9] Comments added and code clean up.
---
.../intent/SmbAddOrChangeIntent.java | 7 +++
.../presentation/model/CloudTypeModel.kt | 43 ++++++++++---------
.../presenter/CloudConnectionListPresenter.kt | 4 +-
.../presenter/SmbAddOrChangePresenter.kt | 39 +++++++++++------
.../ui/fragment/SmbAddOrChangeFragment.kt | 17 +++++++-
5 files changed, 75 insertions(+), 35 deletions(-)
diff --git a/presentation/src/main/java/org/cryptomator/presentation/intent/SmbAddOrChangeIntent.java b/presentation/src/main/java/org/cryptomator/presentation/intent/SmbAddOrChangeIntent.java
index 4d807e4893..e24d567be6 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/intent/SmbAddOrChangeIntent.java
+++ b/presentation/src/main/java/org/cryptomator/presentation/intent/SmbAddOrChangeIntent.java
@@ -5,9 +5,16 @@
import org.cryptomator.presentation.model.SmbCloudModel;
import org.cryptomator.presentation.ui.activity.SmbAddOrChangeActivity;
+/**
+ * Intent interface for navigating to the SMB connection setup or edit screen.
+ * Uses the generator to create an 'IntentBuilder'.
+ */
@Intent(SmbAddOrChangeActivity.class)
public interface SmbAddOrChangeIntent {
+ /**
+ * Optional parameter to pass an existing SMB connection model for editing.
+ */
@Optional
SmbCloudModel smbCloud();
diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt
index fe9da03f13..d051b49f25 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/model/CloudTypeModel.kt
@@ -5,70 +5,73 @@ import org.cryptomator.presentation.R
enum class CloudTypeModel(builder: Builder) {
- CRYPTO(Builder("CRYPTO", R.string.cloud_names_crypto)), //
+ CRYPTO(Builder(R.string.cloud_names_crypto)), //
DROPBOX(
- Builder("DROPBOX", R.string.cloud_names_dropbox) //
+ Builder(R.string.cloud_names_dropbox) //
.withCloudImageResource(R.drawable.dropbox) //
.withVaultImageResource(R.drawable.dropbox_vault) //
- .withVaultSelectedImageResource(R.drawable.dropbox_vault_selected)
+ .withVaultSelectedImageResource(R.drawable.dropbox_vault_selected),
), //
GOOGLE_DRIVE(
- Builder("GOOGLE_DRIVE", R.string.cloud_names_google_drive) //
+ Builder(R.string.cloud_names_google_drive) //
.withCloudImageResource(R.drawable.google_drive) //
.withVaultImageResource(R.drawable.google_drive_vault) //
- .withVaultSelectedImageResource(R.drawable.google_drive_vault_selected)
+ .withVaultSelectedImageResource(R.drawable.google_drive_vault_selected),
), //
ONEDRIVE(
- Builder("ONEDRIVE", R.string.cloud_names_onedrive) //
+ Builder(R.string.cloud_names_onedrive) //
.withCloudImageResource(R.drawable.onedrive) //
.withVaultImageResource(R.drawable.onedrive_vault) //
- .withVaultSelectedImageResource(R.drawable.onedrive_vault_selected)
- .withMultiInstances()
+ .withVaultSelectedImageResource(R.drawable.onedrive_vault_selected) //
+ .withMultiInstances(),
), //
PCLOUD(
- Builder("PCLOUD", R.string.cloud_names_pcloud) //
+ Builder(R.string.cloud_names_pcloud) //
.withCloudImageResource(R.drawable.pcloud) //
.withVaultImageResource(R.drawable.pcloud_vault) //
.withVaultSelectedImageResource(R.drawable.pcloud_vault_selected) //
- .withMultiInstances()
+ .withMultiInstances(),
), //
WEBDAV(
- Builder("WEBDAV", R.string.cloud_names_webdav) //
+ Builder(R.string.cloud_names_webdav) //
.withCloudImageResource(R.drawable.webdav) //
.withVaultImageResource(R.drawable.webdav_vault) //
.withVaultSelectedImageResource(R.drawable.webdav_vault_selected) //
- .withMultiInstances()
+ .withMultiInstances(),
), //
S3(
- Builder("S3", R.string.cloud_names_s3) //
+ Builder(R.string.cloud_names_s3) //
.withCloudImageResource(R.drawable.s3) //
.withVaultImageResource(R.drawable.s3_vault) //
.withVaultSelectedImageResource(R.drawable.s3_vault_selected) //
- .withMultiInstances()
+ .withMultiInstances(),
), //
+ /**
+ * SMB (Server Message Block) cloud provider.
+ * Configured with its own icon set and supports multiple connection instances.
+ */
SMB(
- Builder("SMB", R.string.cloud_names_smb) //
+ Builder(R.string.cloud_names_smb) //
.withCloudImageResource(R.drawable.smb) //
.withVaultImageResource(R.drawable.smb_vault) //
.withVaultSelectedImageResource(R.drawable.smb_vault_selected) //
- .withMultiInstances()
+ .withMultiInstances(),
), //
LOCAL(
- Builder("LOCAL", R.string.cloud_names_local_storage) //
+ Builder(R.string.cloud_names_local_storage) //
.withCloudImageResource(R.drawable.local_fs) //
.withVaultImageResource(R.drawable.local_fs_vault) //
.withVaultSelectedImageResource(R.drawable.local_fs_vault_selected) //
- .withMultiInstances()
+ .withMultiInstances(),
);
- val cloudName: String = builder.cloudName
val displayNameResource: Int = builder.displayNameResource
val cloudImageResource: Int = builder.cloudImageResource
val vaultImageResource: Int = builder.vaultImageResource
val vaultSelectedImageResource: Int = builder.vaultSelectedImageResource
val isMultiInstance: Boolean = builder.multiInstances
- private class Builder(val cloudName: String, val displayNameResource: Int) {
+ private class Builder(val displayNameResource: Int) {
var cloudImageResource = 0
var vaultImageResource = 0
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
index 48ff06e08e..9cd574e4bc 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/CloudConnectionListPresenter.kt
@@ -129,6 +129,7 @@ class CloudConnectionListPresenter @Inject constructor( //
CloudTypeModel.WEBDAV -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.webDavAddOrChangeIntent())
CloudTypeModel.PCLOUD -> requestActivityResult(ActivityResultCallbacks.pCloudAuthenticationFinished(), Intents.authenticatePCloudIntent())
CloudTypeModel.S3 -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.s3AddOrChangeIntent())
+ // Launches the SMB setup/edit screen
CloudTypeModel.SMB -> requestActivityResult(ActivityResultCallbacks.addChangeMultiCloud(), Intents.smbAddOrChangeIntent())
CloudTypeModel.LOCAL -> openDocumentTree()
else -> throw IllegalStateException("Cloud type is not supported")
@@ -187,6 +188,7 @@ class CloudConnectionListPresenter @Inject constructor( //
)
}
cloudModel.cloudType() == CloudTypeModel.SMB -> {
+ // Re-opens the SMB setup screen with existing configuration for editing
requestActivityResult(
ActivityResultCallbacks.addChangeMultiCloud(), //
Intents.smbAddOrChangeIntent() //
@@ -223,7 +225,7 @@ class CloudConnectionListPresenter @Inject constructor( //
if (!code.isNullOrEmpty() && !hostname.isNullOrEmpty()) {
Timber.tag("CloudConnectionListPresenter").i("PCloud OAuth code successfully retrieved")
val accessToken = CredentialCryptor.getInstance(this.context()).encrypt(code)
- val pCloudSkeleton = PCloud.aPCloud().withAccessToken(accessToken).withUrl(hostname).build();
+ val pCloudSkeleton = PCloud.aPCloud().withAccessToken(accessToken).withUrl(hostname).build()
getUsernameUseCase //
.withCloud(pCloudSkeleton) //
.run(object : DefaultResultHandler() {
diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
index b0643c618d..31f686bce2 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/SmbAddOrChangePresenter.kt
@@ -14,13 +14,21 @@ import org.cryptomator.presentation.ui.activity.view.SmbAddOrChangeView
import org.cryptomator.util.crypto.CredentialCryptor
import javax.inject.Inject
+/**
+ * Presenter for the SMB setup/edit screen.
+ * Handles validation of SMB URLs and credentials, and performs the connection test/authentication.
+ */
@PerView
-class SmbAddOrChangePresenter @Inject internal constructor( //
- private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase, //
- private val connectToSmbUseCase: ConnectToSmbUseCase, //
- exceptionMappings: ExceptionHandlers
+class SmbAddOrChangePresenter @Inject internal constructor(
+ private val addOrChangeCloudConnectionUseCase: AddOrChangeCloudConnectionUseCase,
+ private val connectToSmbUseCase: ConnectToSmbUseCase,
+ exceptionMappings: ExceptionHandlers,
) : Presenter(exceptionMappings) {
+ /**
+ * Validates the user input for the SMB connection.
+ * Ensures required fields are present and the URL follows the 'smb://' scheme and includes a share.
+ */
fun checkUserInput(urlPort: String, username: String, password: String, domain: String, cloudId: Long?) {
var statusMessage: String? = null
@@ -30,7 +38,7 @@ class SmbAddOrChangePresenter @Inject internal constructor( //
if (username.isEmpty()) {
statusMessage = getString(R.string.screen_webdav_settings_msg_username_must_not_be_empty)
}
- if (urlPort.isEmpty() || urlPort == "smb://") {
+ if (urlPort.isEmpty() || (urlPort == "smb://")) {
statusMessage = getString(R.string.screen_webdav_settings_msg_url_must_not_be_empty)
} else if (!isValid(urlPort)) {
statusMessage = getString(R.string.screen_webdav_settings_msg_url_is_invalid)
@@ -56,31 +64,36 @@ class SmbAddOrChangePresenter @Inject internal constructor( //
return urlPort.startsWith("smb://", ignoreCase = true)
}
+ /**
+ * Checks if the URL includes at least one path segment (representing the SMB share).
+ */
private fun hasShare(urlPort: String): Boolean {
- try {
+ return try {
val uri = java.net.URI(urlPort)
val path = uri.path ?: ""
- return path.split("/").any { it.isNotEmpty() }
- } catch (e: Exception) {
- return false
+ path.split("/").any { it.isNotEmpty() }
+ } catch (_: Exception) {
+ false
}
}
private fun mapToCloud(username: String, password: String, hostPort: String, domain: String, id: Long?): SmbCloud {
- var builder = SmbCloud //
+ val builder = SmbCloud //
.aSmbCloud() //
.withUrl(hostPort) //
.withUsername(username) //
.withPassword(password) //
.withDomain(domain)
- if (id != null) {
- builder = builder.withId(id)
- }
+ id?.let { builder.withId(it) }
return builder.build()
}
+ /**
+ * Attempts to connect to the SMB server using the provided credentials.
+ * If successful, the connection is saved to the local database.
+ */
fun authenticate(username: String, password: String, urlPort: String, domain: String, cloudId: Long?) {
authenticate(mapToCloud(username, password, urlPort, domain, cloudId))
}
diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt
index 2a94038dc8..98d67854c3 100644
--- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt
+++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SmbAddOrChangeFragment.kt
@@ -2,6 +2,7 @@ package org.cryptomator.presentation.ui.fragment
import android.os.Bundle
import android.view.inputmethod.EditorInfo
+import androidx.core.os.BundleCompat
import org.cryptomator.generator.Fragment
import org.cryptomator.presentation.databinding.FragmentSetupSmbBinding
import org.cryptomator.presentation.model.SmbCloudModel
@@ -11,6 +12,10 @@ import org.cryptomator.util.crypto.FatalCryptoException
import javax.inject.Inject
import timber.log.Timber
+/**
+ * Fragment responsible for the UI to add or edit an SMB connection.
+ * Handles user input for server URL, credentials, and domain.
+ */
@Fragment
class SmbAddOrChangeFragment : BaseFragment(FragmentSetupSmbBinding::inflate) {
@@ -20,7 +25,7 @@ class SmbAddOrChangeFragment : BaseFragment(FragmentSet
private var cloudId: Long? = null
private val smbCloudModel: SmbCloudModel?
- get() = arguments?.getSerializable(ARG_SMB_CLOUD) as? SmbCloudModel
+ get() = arguments?.let { BundleCompat.getSerializable(it, ARG_SMB_CLOUD, SmbCloudModel::class.java) }
override fun setupView() {
binding.createCloudButton.setOnClickListener { createCloud() }
@@ -35,6 +40,9 @@ class SmbAddOrChangeFragment : BaseFragment(FragmentSet
showEditableCloudContent(smbCloudModel)
}
+ /**
+ * Populates the UI fields with existing data if we are editing an existing connection.
+ */
private fun showEditableCloudContent(smbCloudModel: SmbCloudModel?) {
if (smbCloudModel != null) {
binding.urlPortEditText.setText(smbCloudModel.url())
@@ -45,6 +53,10 @@ class SmbAddOrChangeFragment : BaseFragment(FragmentSet
}
}
+ /**
+ * Decrypts the stored password for display in the edit field.
+ * If decryption fails, returns an empty string.
+ */
private fun getPassword(password: String?): String {
return if (password != null) {
try {
@@ -58,6 +70,9 @@ class SmbAddOrChangeFragment : BaseFragment(FragmentSet
} else ""
}
+ /**
+ * Collects user input and passes it to the presenter for validation and saving.
+ */
private fun createCloud() {
val urlPort = binding.urlPortEditText.text.toString().trim()
val username = binding.userNameEditText.text.toString().trim()