From a51bfb102fda1f0f5a133b59eea3a807f296ceaa Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Wed, 5 Mar 2025 08:14:02 +0100 Subject: [PATCH 1/4] implements Samba support to close #82 --- src/Kp2aBusinessLogic/Io/SmbFileStorage.cs | 615 ++++++++++++++++++ .../Kp2aBusinessLogic.csproj | 1 + src/keepass2android-app/FileSelectHelper.cs | 47 +- .../drawable-xhdpi/ic_storage_smb.png | Bin 0 -> 5967 bytes .../Resources/layout/smbcredentials.axml | 51 ++ .../Resources/values/strings.xml | 6 + src/keepass2android-app/app/App.cs | 4 +- 7 files changed, 721 insertions(+), 3 deletions(-) create mode 100644 src/Kp2aBusinessLogic/Io/SmbFileStorage.cs create mode 100644 src/keepass2android-app/Resources/drawable-xhdpi/ic_storage_smb.png create mode 100644 src/keepass2android-app/Resources/layout/smbcredentials.axml diff --git a/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs new file mode 100644 index 00000000..b104ce10 --- /dev/null +++ b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs @@ -0,0 +1,615 @@ +using System.Net; +using Android.Content; +using keepass2android; +using keepass2android.Io; +using KeePassLib.Serialization; +using SMBLibrary.Client; +using SMBLibrary; +using FileAttributes = SMBLibrary.FileAttributes; +using KeePassLib.Utility; +using Java.Nio.FileNio; + +namespace Kp2aBusinessLogic.Io +{ + public class SmbFileStorage : IFileStorage + { + public IEnumerable SupportedProtocols + { + get { yield return "smb"; } + } + + public bool UserShouldBackup + { + get { return false; } + } + + public void Delete(IOConnectionInfo ioc) + { + throw new NotImplementedException(); + } + + public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion) + { + return false; + } + + public string GetCurrentFileVersionFast(IOConnectionInfo ioc) + { + return null; + } + + public struct SmbConnectionInfo + { + public string Host; + public string Username; + public string Password; + public string? Domain; + public string? Share; + public string? LocalPath; + + public static SmbConnectionInfo FromUrlAndCredentials(string url, string username, string password, string? domain) + { + string userDomain = username; + if (domain != null) + { + userDomain = domain + "\\" + username; + } + if (url.StartsWith("smb://")) + { + url = url.Substring(6); + } + + if (url.StartsWith("\\\\")) + { + url = url.Substring(2); + } + + url = url.Replace("\\", "/"); + + string fullPath = "smb://" + WebUtility.UrlEncode(userDomain) + ":" + WebUtility.UrlEncode(password) + "@" + url; + return new SmbConnectionInfo(new IOConnectionInfo() { Path = fullPath} ); + } + + + public SmbConnectionInfo(IOConnectionInfo ioc) + { + string fullpath = ioc.Path; + if (!fullpath.StartsWith("smb://")) + { + throw new Exception("Invalid smb path!"); + } + + fullpath = fullpath.Substring(6); + string[] authAndPath = fullpath.Split('@'); + if (authAndPath.Length != 2) + { + throw new Exception("Invalid smb path!"); + } + + string[] userAndPwd = authAndPath[0].Split(':'); + if (userAndPwd.Length != 2) + { + throw new Exception("Invalid smb path!"); + } + + string[] pathParts = authAndPath[1].Split('/'); + if (pathParts.Length < 1) + { + throw new Exception("Invalid smb path!"); + } + + Host = pathParts[0]; + if (pathParts.Length > 1) + { + Share = pathParts[1]; + } + LocalPath = String.Join("/", pathParts.Skip(2)); + if (LocalPath.EndsWith("/")) + { + LocalPath = LocalPath.Substring(0, LocalPath.Length - 1); + } + + Username = WebUtility.UrlDecode(userAndPwd[0]); + if (Username.Contains("\\")) + { + string[] domainAndUser = Username.Split('\\'); + Domain = domainAndUser[0]; + Username = domainAndUser[1]; + } + else Domain = null; + + Password = WebUtility.UrlDecode(userAndPwd[1]); + } + + public string ToPath() + { + string domainUser = Username; + if (Domain != null) + { + domainUser = Domain + "\\" + Username; + } + + return "smb://" + WebUtility.UrlEncode(domainUser) + ":" + WebUtility.UrlEncode(Password) + "@" + Host + + "/" + Share + "/" + LocalPath; + } + + public string GetPathWithoutCredentials() + { + return "smb://" + Host + "/" + Share + "/" + LocalPath; + } + + public string GetLocalSmbPath() + { + return LocalPath?.Replace("/", "\\") ?? ""; + } + + public SmbConnectionInfo GetParent() + { + SmbConnectionInfo parent = new SmbConnectionInfo + { + Host = Host, + Username = Username, + Password = Password, + Domain = Domain, + Share = Share + }; + string[] pathParts = LocalPath?.Split('/') ?? []; + if (pathParts.Length > 0) + { + parent.LocalPath = string.Join("/", pathParts.Take(pathParts.Length - 1)); + } + else + { + parent.LocalPath = ""; + parent.Share = ""; + } + + return parent; + } + + public string Stem() + { + return LocalPath?.Split('/').Last() ?? ""; + } + + + public SmbConnectionInfo GetChild(string childName) + { + SmbConnectionInfo child = new SmbConnectionInfo(); + child.Host = Host; + child.Username = Username; + child.Password = Password; + child.Domain = Domain; + if (string.IsNullOrEmpty(Share)) + { + child.Share = childName; + } + else + { + + child.Share = Share; + var pathPartsList = LocalPath?.Split('/').Where(p => !string.IsNullOrEmpty(p)).ToList() ?? []; + pathPartsList.Add(childName); + child.LocalPath = string.Join("/", pathPartsList); + } + + return child; + } + + public string ToDisplayString() + { + return "smb://" + Host + "/" + Share + "/" + LocalPath; + + } + } + + + class SmbConnection: IDisposable + { + public SmbConnection(SmbConnectionInfo info) + { + _isLoggedIn = false; + var isConnected = Client.Connect(info.Host, SMBTransportType.DirectTCPTransport); + if (!isConnected) + { + throw new Exception($"Failed to connect to SMB server {info.Host}"); + } + + var status = Client.Login(info.Domain ?? string.Empty, info.Username, info.Password); + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Failed to login to SMB as {info.Username}"); + } + + _isLoggedIn = true; + + if (!string.IsNullOrEmpty(info.Share)) + { + FileStore = Client.TreeConnect(info.Share, out status); + } + + } + + + + public readonly SMB2Client Client = new SMB2Client(); + + + public readonly ISMBFileStore? FileStore; + private readonly bool _isLoggedIn; + + public void Dispose() + { + FileStore?.Disconnect(); + + if (_isLoggedIn) + Client.Logoff(); + + if (!Client.IsConnected) return; + Client.Disconnect(); + + + } + } + + + + public Stream OpenFileForRead(IOConnectionInfo ioc) + { + + SmbConnectionInfo info = new SmbConnectionInfo(ioc); + using SmbConnection conn = new SmbConnection(info); + + if (conn.FileStore == null) + { + throw new Exception($"Failed to read to {info.GetPathWithoutCredentials()}"); + } + + + NTStatus status = conn.FileStore.CreateFile(out var fileHandle, out _, info.GetLocalSmbPath(), + AccessMask.GENERIC_READ | AccessMask.SYNCHRONIZE, FileAttributes.Normal, ShareAccess.Read, + CreateDisposition.FILE_OPEN, + CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, null); + + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Failed to open file {info.LocalPath}"); + } + + var stream = new MemoryStream(); + long bytesRead = 0; + while (true) + { + status = conn.FileStore.ReadFile(out var data, fileHandle, bytesRead, (int)conn.Client.MaxReadSize); + if (status != NTStatus.STATUS_SUCCESS && status != NTStatus.STATUS_END_OF_FILE) + { + throw new Exception("Failed to read from file"); + } + + if (status == NTStatus.STATUS_END_OF_FILE || data.Length == 0) + { + break; + } + + bytesRead += data.Length; + stream.Write(data, 0, data.Length); + } + + stream.Seek(0, SeekOrigin.Begin); + return stream; + } + + + class SmbFileStorageWriteTransaction : IWriteTransaction + { + private bool UseFileTransaction { get; } + private readonly string _path; + private readonly string _uploadPath; + private readonly SmbFileStorage _fileStorage; + private MemoryStream? _memoryStream; + + public SmbFileStorageWriteTransaction(string path, SmbFileStorage fileStorage, bool useFileTransaction) + { + UseFileTransaction = useFileTransaction; + _path = path; + if (useFileTransaction) + { + _uploadPath = _path + Guid.NewGuid().ToString().Substring(0, 8) + ".tmp"; + } + else + { + _uploadPath = _path; + } + + + _fileStorage = fileStorage; + _memoryStream = null; + } + + public void Dispose() + { + _memoryStream?.Dispose(); + } + + public Stream OpenFile() + { + _memoryStream = new MemoryStream(); + return _memoryStream; + } + + public void CommitWrite() + { + _fileStorage.UploadData(new MemoryStream(_memoryStream!.ToArray()), new SmbConnectionInfo(new IOConnectionInfo() { Path = _uploadPath})); + if (UseFileTransaction) + { + SmbConnectionInfo uploadPath = new SmbConnectionInfo(new IOConnectionInfo() { Path = _uploadPath }); + SmbConnectionInfo finalPath = new SmbConnectionInfo(new IOConnectionInfo() { Path = _path }); + _fileStorage.RenameFile(uploadPath, finalPath); + } + + + } + } + + private void RenameFile(SmbConnectionInfo fromPath, SmbConnectionInfo toPath) + { + using var connection = new SmbConnection(fromPath); + + // Open existing file + var status = connection.FileStore!.CreateFile(out var handle, out _, fromPath.GetLocalSmbPath(), AccessMask.MAXIMUM_ALLOWED, 0, ShareAccess.Read, CreateDisposition.FILE_OPEN, CreateOptions.FILE_NON_DIRECTORY_FILE, null); + if (status != NTStatus.STATUS_SUCCESS) + throw new Exception($"Failed to open {fromPath.LocalPath} for renaming!"); + + FileRenameInformationType2 renameInfo = new FileRenameInformationType2 + { + FileName = toPath.GetLocalSmbPath(), + ReplaceIfExists = true + }; + connection.FileStore.SetFileInformation(handle, renameInfo); + connection.FileStore.CloseFile(handle); + + } + + private void UploadData(Stream data, SmbConnectionInfo uploadPath) + { + using var connection = new SmbConnection(uploadPath); + var status = connection.FileStore!.CreateFile(out var fileHandle, out _, uploadPath.GetLocalSmbPath(), AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE, FileAttributes.Normal, ShareAccess.None, CreateDisposition.FILE_CREATE, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, null); + if (status == NTStatus.STATUS_OBJECT_NAME_COLLISION) + status = connection.FileStore!.CreateFile(out fileHandle, out _, uploadPath.GetLocalSmbPath(), AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE, FileAttributes.Normal, ShareAccess.None, CreateDisposition.FILE_OVERWRITE, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, null); + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Failed to open {uploadPath.LocalPath} for writing!"); + } + + long writeOffset = 0; + while (data.Position < data.Length) + { + byte[] buffer = new byte[(int)connection.Client.MaxWriteSize]; + int bytesRead = data.Read(buffer, 0, buffer.Length); + if (bytesRead < (int)connection.Client.MaxWriteSize) + { + Array.Resize(ref buffer, bytesRead); + } + + status = connection.FileStore.WriteFile(out _, fileHandle, writeOffset, buffer); + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception("Failed to write to file"); + } + writeOffset += bytesRead; + } + + } + + public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) + { + return new SmbFileStorageWriteTransaction(ioc.Path, this, useFileTransaction); + } + + public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) + { + return UrlUtil.StripExtension( + UrlUtil.GetFileName(ioc.Path)); + + } + + public string GetFileExtension(IOConnectionInfo ioc) + { + return UrlUtil.GetExtension(ioc.Path); + } + + public bool RequiresCredentials(IOConnectionInfo ioc) + { + return false; + } + + public void CreateDirectory(IOConnectionInfo ioc, string newDirName) + { + throw new NotImplementedException(); + } + + private static IEnumerable ListShares(SmbConnection conn, SmbConnectionInfo parent) + { + foreach (string share in conn.Client.ListShares(out _)) + { + yield return new FileDescription() + { + CanRead = true, + CanWrite = true, + DisplayName = share, + IsDirectory = true, + Path = parent.GetChild(share).ToPath() + }; + } + + } + + public IEnumerable ListContents(IOConnectionInfo ioc) + { + List result = []; + SmbConnectionInfo info = new SmbConnectionInfo(ioc); + using SmbConnection conn = new SmbConnection(info); + if (string.IsNullOrEmpty(info.Share)) + { + var shares = ListShares(conn, info).ToList(); + return shares; + } + + NTStatus status = conn.FileStore!.CreateFile(out var directoryHandle, out _, info.GetLocalSmbPath(), AccessMask.GENERIC_READ, FileAttributes.Directory, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null); + if (status == NTStatus.STATUS_SUCCESS) + { + conn.FileStore.QueryDirectory(out List fileList, directoryHandle, "*", FileInformationClass.FileDirectoryInformation); + foreach (var fi in fileList) + { + var fileDirectoryInformation = fi as FileDirectoryInformation; + if (fileDirectoryInformation == null) + continue; + + if (fileDirectoryInformation.FileName is "." or "..") + continue; + + var fileDescription = FileDescriptionConvert(ioc, fileDirectoryInformation); + + result.Add(fileDescription); + } + conn.FileStore.CloseFile(directoryHandle); + } + + return result; + } + + private FileDescription FileDescriptionConvert(IOConnectionInfo parentIoc, + FileDirectoryInformation fileDirectoryInformation) + { + FileDescription fileDescription = new FileDescription + { + CanRead = true, + CanWrite = true, + IsDirectory = (fileDirectoryInformation.FileAttributes & FileAttributes.Directory) != 0, + DisplayName = fileDirectoryInformation.FileName + }; + fileDescription.Path = CreateFilePath(parentIoc.Path, fileDescription.DisplayName); + fileDescription.LastModified = fileDirectoryInformation.LastWriteTime; + + fileDescription.SizeInBytes = fileDirectoryInformation.EndOfFile; + return fileDescription; + } + + public FileDescription GetFileDescription(IOConnectionInfo ioc) + { + SmbConnectionInfo info = new SmbConnectionInfo(ioc); + + if (string.IsNullOrEmpty(info.Share)) + { + return new FileDescription + { + CanRead = true, CanWrite = true, + DisplayName = info.Host, + IsDirectory = true, + Path = info.ToPath() + }; + } + + using SmbConnection conn = new SmbConnection(info); + NTStatus status = conn.FileStore!.CreateFile(out var directoryHandle, out _, info.GetParent().GetLocalSmbPath(), AccessMask.GENERIC_READ, FileAttributes.Directory, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null); + if (status != NTStatus.STATUS_SUCCESS) throw new Exception($"Failed to query details for {info.LocalPath}"); + conn.FileStore.QueryDirectory(out List fileList, directoryHandle, info.Stem(), FileInformationClass.FileDirectoryInformation); + foreach (var fi in fileList) + { + var fileDirectoryInformation = fi as FileDirectoryInformation; + if (fileDirectoryInformation == null) + continue; + + if (fileDirectoryInformation.FileName is "." or "..") + continue; + + return FileDescriptionConvert(ioc, fileDirectoryInformation); + + + } + conn.FileStore.CloseFile(directoryHandle); + + throw new Exception($"Failed to query details for {info.LocalPath}"); + } + + public bool RequiresSetup(IOConnectionInfo ioConnection) + { + return false; + } + + public string IocToPath(IOConnectionInfo ioc) + { + return ioc.Path; + } + + public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId) + { + activity.PerformManualFileSelect(isForSave, requestCode, protocolId); + + } + + public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode, + bool alwaysReturnSuccess) + { + Intent intent = new Intent(); + activity.IocToIntent(intent, ioc); + activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileUsagePrepared, intent); + } + + public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc) + { + + } + + public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState) + { + + } + + public void OnResume(IFileStorageSetupActivity activity) + { + + } + + public void OnStart(IFileStorageSetupActivity activity) + { + + } + + public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) + { + + } + + public string GetDisplayName(IOConnectionInfo ioc) + { + return new SmbConnectionInfo(ioc).ToDisplayString(); + } + + public string CreateFilePath(string parent, string newFilename) + { + return new SmbConnectionInfo(new IOConnectionInfo() { Path = parent}).GetChild(newFilename).ToPath(); + } + + public IOConnectionInfo GetParentPath(IOConnectionInfo ioc) + { + SmbConnectionInfo connectionInfo = new SmbConnectionInfo(ioc); + return new IOConnectionInfo() { Path = connectionInfo.GetParent().ToPath() }; + } + + public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename) + { + return new IOConnectionInfo() { Path = CreateFilePath(folderPath.Path, filename)}; + } + + public bool IsPermanentLocation(IOConnectionInfo ioc) + { + return true; + } + + public bool IsReadOnly(IOConnectionInfo ioc, OptionalOut reason = null) + { + return false; + } + } +} diff --git a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj index e518e764..7f880dae 100644 --- a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj +++ b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj @@ -13,6 +13,7 @@ + diff --git a/src/keepass2android-app/FileSelectHelper.cs b/src/keepass2android-app/FileSelectHelper.cs index 9e528808..249efd8c 100644 --- a/src/keepass2android-app/FileSelectHelper.cs +++ b/src/keepass2android-app/FileSelectHelper.cs @@ -22,6 +22,7 @@ using Keepass2android.Javafilestorage; #endif using KeePassLib.Serialization; using KeePassLib.Utility; +using static Kp2aBusinessLogic.Io.SmbFileStorage; namespace keepass2android { @@ -350,7 +351,47 @@ namespace keepass2android #endif } - private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath) + + private void ShowSmbDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath) + { +#if !EXCLUDE_JAVAFILESTORAGE && !NoNet + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); + View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.smbcredentials, null); + if (!defaultPath.EndsWith(_schemeSeparator)) + { + SmbConnectionInfo ci = new SmbConnectionInfo(new IOConnectionInfo() { Path = defaultPath }); + + dlgContents.FindViewById(Resource.Id.smb_url).Text = ci.GetPathWithoutCredentials(); + dlgContents.FindViewById(Resource.Id.smb_domain).Text = ci.Domain; + dlgContents.FindViewById(Resource.Id.smb_user).Text = ci.Username; + dlgContents.FindViewById(Resource.Id.smb_password).Text = ci.Password; + + + } + builder.SetView(dlgContents); + builder.SetPositiveButton(Android.Resource.String.Ok, + (sender, args) => + { + string url = dlgContents.FindViewById(Resource.Id.smb_url).Text; + + string user = dlgContents.FindViewById(Resource.Id.smb_user).Text; + string password = dlgContents.FindViewById(Resource.Id.smb_password).Text; + string domain = dlgContents.FindViewById(Resource.Id.smb_domain).Text; + + string fullPath = SmbConnectionInfo.FromUrlAndCredentials(url, user, password, domain).ToPath(); + onStartBrowse(fullPath); + }); + EventHandler evtH = new EventHandler((sender, e) => onCancel()); + + builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); + builder.SetTitle(activity.GetString(Resource.String.enter_smb_login_title)); + Dialog dialog = builder.Create(); + + dialog.Show(); +#endif + } + + private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath) { #if !NoNet MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); @@ -476,7 +517,9 @@ namespace keepass2android ShowFtpDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath); else if ((defaultPath.StartsWith("http://")) || (defaultPath.StartsWith("https://"))) ShowHttpDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath); - else if (defaultPath.StartsWith("owncloud://")) + else if ((defaultPath.StartsWith("smb://"))) + ShowSmbDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath); + else if (defaultPath.StartsWith("owncloud://")) ShowOwncloudDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath, "owncloud"); else if (defaultPath.StartsWith("nextcloud://")) ShowOwncloudDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath, "nextcloud"); diff --git a/src/keepass2android-app/Resources/drawable-xhdpi/ic_storage_smb.png b/src/keepass2android-app/Resources/drawable-xhdpi/ic_storage_smb.png new file mode 100644 index 0000000000000000000000000000000000000000..40387b21b467a8595b2cd5a595c4650d9e971ab2 GIT binary patch literal 5967 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 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/keepass2android-app/Resources/values/strings.xml b/src/keepass2android-app/Resources/values/strings.xml index f4b28c49..02de2d7b 100644 --- a/src/keepass2android-app/Resources/values/strings.xml +++ b/src/keepass2android-app/Resources/values/strings.xml @@ -504,7 +504,12 @@ Tell me more! No, I don\'t like it that much Enter WebDav login data: + Enter Samba login data: URL of folder or file (ex: mycloud.me.com/webdav/) + URL of folder or file (ex: 192.168.1.10/share/folder/) + Samba user\'s domain + Samba username + Enter OwnCloud login data: OwnCloud URL (ex: owncloud.me.com) Enter Nextcloud login data: @@ -542,6 +547,7 @@ FTP HTTP (WebDav) HTTPS (WebDav) + Samba (Windows Share) OwnCloud Nextcloud Dropbox diff --git a/src/keepass2android-app/app/App.cs b/src/keepass2android-app/app/App.cs index 00f73cf2..84b2d8a4 100644 --- a/src/keepass2android-app/app/App.cs +++ b/src/keepass2android-app/app/App.cs @@ -45,6 +45,7 @@ using keepass2android.database.edit; using keepass2android; using KeePassLib.Interfaces; using KeePassLib.Utility; +using Kp2aBusinessLogic.Io; #if !NoNet #if !EXCLUDE_JAVAFILESTORAGE using Android.Gms.Common; @@ -836,7 +837,8 @@ namespace keepass2android new SftpFileStorage(LocaleManager.LocalizedAppContext, this, IsFtpDebugEnabled()), new NetFtpFileStorage(LocaleManager.LocalizedAppContext, this, IsFtpDebugEnabled), new WebDavFileStorage(this), - new PCloudFileStorage(LocaleManager.LocalizedAppContext, this), + new SmbFileStorage(), + new PCloudFileStorage(LocaleManager.LocalizedAppContext, this), new PCloudFileStorageAll(LocaleManager.LocalizedAppContext, this), new MegaFileStorage(App.Context), //new LegacyWebDavStorage(this), From b83c4b377209753525c614a51bbc71b9ee0eb7e4 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 11:51:15 +0200 Subject: [PATCH 2/4] improve Samba dialog. fix NoNet build --- src/Kp2aBusinessLogic/Io/SmbFileStorage.cs | 3 ++- src/keepass2android-app/FileSelectHelper.cs | 5 ++++- src/keepass2android-app/Resources/layout/smbcredentials.axml | 4 ++++ src/keepass2android-app/Resources/values/strings.xml | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs index b104ce10..fb23d112 100644 --- a/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs @@ -8,7 +8,7 @@ using SMBLibrary; using FileAttributes = SMBLibrary.FileAttributes; using KeePassLib.Utility; using Java.Nio.FileNio; - +#if !NoNet namespace Kp2aBusinessLogic.Io { public class SmbFileStorage : IFileStorage @@ -613,3 +613,4 @@ namespace Kp2aBusinessLogic.Io } } } +#endif \ No newline at end of file diff --git a/src/keepass2android-app/FileSelectHelper.cs b/src/keepass2android-app/FileSelectHelper.cs index 542c3183..be3e4d57 100644 --- a/src/keepass2android-app/FileSelectHelper.cs +++ b/src/keepass2android-app/FileSelectHelper.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; #if !NoNet using FluentFTP; +using static Kp2aBusinessLogic.Io.SmbFileStorage; #endif using System.Text; @@ -22,7 +23,7 @@ using Keepass2android.Javafilestorage; #endif using KeePassLib.Serialization; using KeePassLib.Utility; -using static Kp2aBusinessLogic.Io.SmbFileStorage; + namespace keepass2android { @@ -384,6 +385,8 @@ namespace keepass2android string fullPath = SmbConnectionInfo.FromUrlAndCredentials(url, user, password, domain).ToPath(); onStartBrowse(fullPath); }); + builder.SetCancelable(false); + EventHandler evtH = new EventHandler((sender, e) => onCancel()); builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); diff --git a/src/keepass2android-app/Resources/layout/smbcredentials.axml b/src/keepass2android-app/Resources/layout/smbcredentials.axml index 99c1445c..cbde2d3c 100644 --- a/src/keepass2android-app/Resources/layout/smbcredentials.axml +++ b/src/keepass2android-app/Resources/layout/smbcredentials.axml @@ -3,8 +3,12 @@ android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" + android:padding="12dp" android:layout_margin="12dip" > + Entry notifications Notification to simplify access to the currently selected entry. Close database after three failed biometric unlock attempts. + If you want to access files from your personal Windows computer, use the computer name or local IP address as host URL; enter the computer name as domain and the Windows login (often the same as your Microsoft account) as username and password. Warning! Biometric authentication can be invalidated by Android, e.g. after adding a new fingerprint in your device settings. Make sure you always know how to unlock with your master password! From 13306a9076d74627d865b86920c7b261745796b3 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 12:20:25 +0200 Subject: [PATCH 3/4] fix another build issue in NoNet --- src/Kp2aBusinessLogic/Io/SmbFileStorage.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs index fb23d112..11eec514 100644 --- a/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs @@ -1,4 +1,5 @@ -using System.Net; +#if !NoNet +using System.Net; using Android.Content; using keepass2android; using keepass2android.Io; @@ -8,7 +9,7 @@ using SMBLibrary; using FileAttributes = SMBLibrary.FileAttributes; using KeePassLib.Utility; using Java.Nio.FileNio; -#if !NoNet + namespace Kp2aBusinessLogic.Io { public class SmbFileStorage : IFileStorage From d40b3dc15c2d36d86fe47e6e4c447940f320bcbe Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 13:16:44 +0200 Subject: [PATCH 4/4] fix build issue with NoNet --- src/keepass2android-app/app/App.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/keepass2android-app/app/App.cs b/src/keepass2android-app/app/App.cs index da7499bd..e39cd2ae 100644 --- a/src/keepass2android-app/app/App.cs +++ b/src/keepass2android-app/app/App.cs @@ -46,10 +46,11 @@ using keepass2android; using keepass2android.Utils; using KeePassLib.Interfaces; using KeePassLib.Utility; -using Kp2aBusinessLogic.Io; + using Message = keepass2android.Utils.Message; #if !NoNet #if !EXCLUDE_JAVAFILESTORAGE +using Kp2aBusinessLogic.Io; using Android.Gms.Common; using Keepass2android.Javafilestorage; using GoogleDriveFileStorage = keepass2android.Io.GoogleDriveFileStorage;