diff --git a/.gitignore b/.gitignore
index 20de9957c..43c3fe5c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
ApiKeys.Private.cs
SecureFolderFS.WinUI/settings/
SecureFolderFS.AvaloniaUI/settings/
+pdfjs53/
# User-specific files
*.rsuser
diff --git a/.gitmodules b/.gitmodules
index 1f2e435bb..44f18cf87 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
[submodule "lib/Tmds.Fuse"]
path = lib/Tmds.Fuse
url = https://github.com/securefolderfs-community/Tmds.Fuse
+[submodule "src/Platforms/SecureFolderFS.Dashboard"]
+ path = src/Platforms/SecureFolderFS.Dashboard
+ url = git@github.com:securefolderfs-community/SecureFolderFS.Dashboard.git
diff --git a/SecureFolderFS.sln b/SecureFolderFS.sln
index 588353e7a..740359e3b 100644
--- a/SecureFolderFS.sln
+++ b/SecureFolderFS.sln
@@ -64,6 +64,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{724C2A9B
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureFolderFS.Tests", "tests\SecureFolderFS.Tests\SecureFolderFS.Tests.csproj", "{67ED86B1-D287-4F36-A8BE-189F68502B4C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureFolderFS.Dashboard", "src\Platforms\SecureFolderFS.Dashboard\SecureFolderFS.Dashboard.csproj", "{9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -396,6 +398,22 @@ Global
{67ED86B1-D287-4F36-A8BE-189F68502B4C}.Release|x64.Build.0 = Release|Any CPU
{67ED86B1-D287-4F36-A8BE-189F68502B4C}.Release|x86.ActiveCfg = Release|Any CPU
{67ED86B1-D287-4F36-A8BE-189F68502B4C}.Release|x86.Build.0 = Release|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Debug|arm64.ActiveCfg = Debug|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Debug|arm64.Build.0 = Debug|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Debug|x64.Build.0 = Debug|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Debug|x86.Build.0 = Debug|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Release|arm64.ActiveCfg = Release|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Release|arm64.Build.0 = Release|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Release|x64.ActiveCfg = Release|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Release|x64.Build.0 = Release|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Release|x86.ActiveCfg = Release|Any CPU
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -425,6 +443,7 @@ Global
{2EAA63F7-399A-4079-9F70-E9D318812ADC} = {66BC1E2B-D99A-49E2-8B8F-EF7851493CB0}
{F6C5CDF6-D86A-4C78-8649-F5AF983DA3A8} = {F2ACE2B7-1599-4769-8FF4-41FA03B25D26}
{67ED86B1-D287-4F36-A8BE-189F68502B4C} = {724C2A9B-20B9-4FBA-B0F7-F2BFDB10B52C}
+ {9CF66911-1E7E-4A82-B7B4-97B2DE8BA9B0} = {66BC1E2B-D99A-49E2-8B8F-EF7851493CB0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A1906FD8-BB54-4688-BC0F-9ED7532D2CB0}
diff --git a/global.json b/global.json
index f42869719..4d9e028b1 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,5 @@
{
"msbuild-sdks": {
- "Uno.Sdk": "5.4.10"
+ "Uno.Sdk": "6.0.96"
}
-}
+}
\ No newline at end of file
diff --git a/lib/Tmds.Fuse b/lib/Tmds.Fuse
index 73663e78b..e4bf64788 160000
--- a/lib/Tmds.Fuse
+++ b/lib/Tmds.Fuse
@@ -1 +1 @@
-Subproject commit 73663e78ba98e20850ecad10489028f2afedd60f
+Subproject commit e4bf6478812ba61773f0a7e6f42710a2b4a70bdd
diff --git a/lib/nwebdav b/lib/nwebdav
index 17fcebe4d..733265e9b 160000
--- a/lib/nwebdav
+++ b/lib/nwebdav
@@ -1 +1 @@
-Subproject commit 17fcebe4d06338f94122400c2c9cfdbb5ea8c77e
+Subproject commit 733265e9b66044efb7b76b9d7b07aca437632614
diff --git a/src/Platforms/Directory.Packages.props b/src/Platforms/Directory.Packages.props
index 6cbaa6ad1..06bdb9946 100644
--- a/src/Platforms/Directory.Packages.props
+++ b/src/Platforms/Directory.Packages.props
@@ -2,31 +2,29 @@
-
-
-
+
+
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
-
-
+
@@ -35,6 +33,9 @@
-
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/Platforms/SecureFolderFS.Dashboard b/src/Platforms/SecureFolderFS.Dashboard
new file mode 160000
index 000000000..6b605e61d
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Dashboard
@@ -0,0 +1 @@
+Subproject commit 6b605e61dcdf739c78a660738740087d0cb521ce
diff --git a/src/Platforms/SecureFolderFS.Maui/App.xaml b/src/Platforms/SecureFolderFS.Maui/App.xaml
index 11e594fae..cd4219349 100644
--- a/src/Platforms/SecureFolderFS.Maui/App.xaml
+++ b/src/Platforms/SecureFolderFS.Maui/App.xaml
@@ -1,17 +1,14 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
-
+
+
-
-
-
diff --git a/src/Platforms/SecureFolderFS.Maui/App.xaml.cs b/src/Platforms/SecureFolderFS.Maui/App.xaml.cs
index 4fa5aaaa5..2aadcab1a 100644
--- a/src/Platforms/SecureFolderFS.Maui/App.xaml.cs
+++ b/src/Platforms/SecureFolderFS.Maui/App.xaml.cs
@@ -1,4 +1,7 @@
-using CommunityToolkit.Mvvm.DependencyInjection;
+using APES.UI.XF;
+using SecureFolderFS.Maui.Extensions.Mappers;
+using SecureFolderFS.Maui.Helpers;
+using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Shared;
using SecureFolderFS.UI.Helpers;
@@ -6,8 +9,8 @@ namespace SecureFolderFS.Maui
{
public partial class App : Application
{
- public static App Instance => (App)Application.Current!;
-
+ public static App Instance => (App)Current!;
+
public IServiceProvider? ServiceProvider { get; private set; }
public BaseLifecycleHelper ApplicationLifecycle { get; } =
@@ -18,7 +21,7 @@ public partial class App : Application
#else
null;
#endif
-
+
public event EventHandler? AppResumed;
public event EventHandler? AppPutToForeground;
@@ -26,6 +29,16 @@ public App()
{
InitializeComponent();
+#if ANDROID
+ // Load Android-specific resource dictionaries
+ Resources.MergedDictionaries.Add(new Platforms.Android.Templates.AndroidDataTemplates());
+#endif
+
+ // Configure mappers
+ CustomMappers.AddEntryMappers();
+ CustomMappers.AddLabelMappers();
+ CustomMappers.AddPickerMappers();
+
// Configure exception handlers
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
@@ -33,8 +46,8 @@ public App()
protected override Window CreateWindow(IActivationState? activationState)
{
- APES.UI.XF.ContextMenuContainer.Init();
-
+ ContextMenuContainer.Init();
+
var appShell = Task.Run(GetAppShellAsync).ConfigureAwait(false).GetAwaiter().GetResult();
return new Window(appShell);
}
@@ -50,9 +63,16 @@ private async Task GetAppShellAsync()
// Register IoC
DI.Default.SetServiceProvider(ServiceProvider);
+ // Initialize Telemetry
+ var telemetryService = DI.Service();
+ await telemetryService.EnableTelemetryAsync();
+
// Create and initialize AppShell
var appShell = new AppShell();
- await appShell.MainViewModel.InitAsync();
+ await appShell.MainViewModel.InitAsync().ConfigureAwait(false);
+
+ // Initialize ThemeHelper
+ await MauiThemeHelper.Instance.InitAsync().ConfigureAwait(false);
return appShell;
}
diff --git a/src/Platforms/SecureFolderFS.Maui/AppModels/CorrectedPanGestureRecognizer.cs b/src/Platforms/SecureFolderFS.Maui/AppModels/CorrectedPanGestureRecognizer.cs
new file mode 100644
index 000000000..f69e9efc6
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/AppModels/CorrectedPanGestureRecognizer.cs
@@ -0,0 +1,49 @@
+// Some parts of the following code were used from https://github.com/dotnet/maui/issues/20772#issuecomment-2030914069
+
+using Microsoft.Maui.Platform;
+
+namespace SecureFolderFS.Maui.AppModels
+{
+ internal sealed class CorrectedPanGestureRecognizer : PanGestureRecognizer, IPanGestureController
+ {
+#if ANDROID
+ // Index 0 is X, index 1 is Y
+ private readonly int[] startingLocation = new int[2];
+ private readonly int[] currentLocation = new int[2];
+#endif
+ public new event EventHandler? PanUpdated;
+
+ void IPanGestureController.SendPan(Element sender, double totalX, double totalY, int gestureId)
+ {
+#if ANDROID
+ ArgumentNullException.ThrowIfNull(sender.Handler.MauiContext?.Context);
+ Android.Views.View view = sender.ToPlatform(sender.Handler.MauiContext);
+ view.GetLocationOnScreen(currentLocation);
+ totalX += sender.Handler.MauiContext.Context.FromPixels(currentLocation[0] - startingLocation[0]);
+ totalY += sender.Handler.MauiContext.Context.FromPixels(currentLocation[1] - startingLocation[1]);
+
+#endif
+ PanUpdated?.Invoke(sender, new PanUpdatedEventArgs(GestureStatus.Running, gestureId, totalX, totalY));
+ }
+
+ void IPanGestureController.SendPanCanceled(Element sender, int gestureId)
+ {
+ PanUpdated?.Invoke(sender, new PanUpdatedEventArgs(GestureStatus.Canceled, gestureId));
+ }
+
+ void IPanGestureController.SendPanCompleted(Element sender, int gestureId)
+ {
+ PanUpdated?.Invoke(sender, new PanUpdatedEventArgs(GestureStatus.Completed, gestureId));
+ }
+
+ void IPanGestureController.SendPanStarted(Element sender, int gestureId)
+ {
+#if ANDROID
+ ArgumentNullException.ThrowIfNull(sender.Handler.MauiContext);
+ Android.Views.View view = sender.ToPlatform(sender.Handler.MauiContext);
+ view.GetLocationOnScreen(startingLocation);
+#endif
+ PanUpdated?.Invoke(sender, new PanUpdatedEventArgs(GestureStatus.Started, gestureId));
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/AppModels/ImageStream.cs b/src/Platforms/SecureFolderFS.Maui/AppModels/ImageStream.cs
index d80b3989a..bced85220 100644
--- a/src/Platforms/SecureFolderFS.Maui/AppModels/ImageStream.cs
+++ b/src/Platforms/SecureFolderFS.Maui/AppModels/ImageStream.cs
@@ -1,25 +1,38 @@
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Shared.Models;
using IImage = SecureFolderFS.Shared.ComponentModel.IImage;
namespace SecureFolderFS.Maui.AppModels
{
///
- internal sealed class ImageStream : IImage
+ internal sealed class ImageStream : IImageStream
{
- private readonly Stream _stream;
-
+ public Stream Stream { get; }
+
public StreamImageSource Source { get; }
public ImageStream(Stream stream)
{
- _stream = stream;
+ Stream = stream;
Source = new();
- Source.Stream = (cancellationToken) => Task.FromResult(stream);
+ Source.Stream = (ct) => Task.FromResult(stream);
+ }
+
+ ///
+ public async Task CopyToAsync(Stream destination, CancellationToken cancellationToken = default)
+ {
+ var savedPosition = Stream.Position;
+ await Stream.CopyToAsync(destination, cancellationToken);
+ Stream.Position = savedPosition;
}
///
public void Dispose()
{
- _stream.Dispose();
+ if (Stream is OnDemandDisposableStream onDemandDisposableStream)
+ onDemandDisposableStream.ForceClose();
+ else
+ Stream.Dispose();
}
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/AppModels/PdfStreamServer.cs b/src/Platforms/SecureFolderFS.Maui/AppModels/PdfStreamServer.cs
new file mode 100644
index 000000000..ec1c10253
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/AppModels/PdfStreamServer.cs
@@ -0,0 +1,186 @@
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using SecureFolderFS.Shared.ComponentModel;
+
+#if ANDROID
+using Android.Content.Res;
+using SecureFolderFS.Shared.Helpers;
+#endif
+
+namespace SecureFolderFS.Maui.AppModels
+{
+ internal sealed class PdfStreamServer : IAsyncInitialize, IDisposable
+ {
+ private readonly HttpListener _httpListener;
+ private readonly Stream _fileStream;
+ private readonly string _mimeType;
+ private readonly int _port;
+ private bool _disposed;
+
+ public string BaseAddress => $"http://localhost:{_port}";
+
+ public PdfStreamServer(Stream fileStream, string mimeType)
+ {
+ if (!fileStream.CanSeek)
+ throw new ArgumentException("Stream must be seekable.", nameof(fileStream));
+
+ _fileStream = fileStream;
+ _mimeType = mimeType;
+
+ // Automatically find a free port
+ _port = GetAvailablePort();
+
+ // Initialize the HttpListener with the free port
+ _httpListener = new HttpListener();
+ _httpListener.Prefixes.Add($"http://localhost:{_port}/");
+ }
+
+ ///
+ public Task InitAsync(CancellationToken cancellationToken = default)
+ {
+ _httpListener.Start();
+ _ = BeginListeningAsync();
+
+ return Task.CompletedTask;
+
+ async Task BeginListeningAsync()
+ {
+ try
+ {
+ while (!_disposed && _httpListener.IsListening && await _httpListener.GetContextAsync() is var context)
+ {
+ var response = context.Response;
+ var absolutePath = context.Request.Url?.AbsolutePath ?? string.Empty;
+
+ try
+ {
+ if (absolutePath == "/app_file")
+ {
+ response.ContentType = _mimeType;
+ response.Headers["Accept-Ranges"] = "bytes";
+ response.ContentLength64 = _fileStream.Length;
+
+ await _fileStream.CopyToAsync(response.OutputStream, cancellationToken);
+ if (_fileStream.CanSeek)
+ _fileStream.Position = 0L;
+
+ response.StatusCode = (int)HttpStatusCode.OK;
+ response.StatusDescription = "OK";
+ }
+ else if (absolutePath.StartsWith("/pdfjs53"))
+ {
+#if ANDROID
+ var relativePath = absolutePath.TrimStart('/');
+ var contentType = FileTypeHelper.GetMimeType(relativePath);
+ response.ContentType = contentType;
+ response.Headers["Accept-Ranges"] = "bytes";
+
+ await using var assetStream = Android.App.Application.Context.Assets?.Open(relativePath, Access.Random);
+ if (assetStream is null)
+ {
+ response.StatusCode = (int)HttpStatusCode.NotFound;
+ continue;
+ }
+
+ // All this double-copying of data is needed for setting the ContentLength tag
+
+ // Copy to temporary MemoryStream
+ await using var memoryStream = new MemoryStream();
+ await assetStream.CopyToAsync(memoryStream, cancellationToken);
+ await memoryStream.FlushAsync(cancellationToken);
+ memoryStream.Position = 0L;
+
+ // Set the ContentLength tag
+ response.ContentLength64 = memoryStream.Length;
+
+ // Copy back to the OutputStream
+ await memoryStream.CopyToAsync(response.OutputStream, cancellationToken);
+ await response.OutputStream.FlushAsync(cancellationToken);
+ response.StatusCode = (int)HttpStatusCode.OK;
+ response.StatusDescription = "OK";
+
+#endif
+ }
+ }
+ catch (Exception ex)
+ {
+ var title = "Internal Server Error";
+ var message = WebUtility.HtmlEncode(ex.Message);
+ var stackTrace = WebUtility.HtmlEncode(ex.StackTrace ?? "");
+
+ var html = $$"""
+
+
+
+
+ {{title}}
+
+
+
+ {{title}}
+ {{message}}
+ {{stackTrace}}
+
+
+ """;
+
+ try
+ {
+ var buffer = Encoding.UTF8.GetBytes(html);
+ response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ response.ContentType = "text/html";
+ response.ContentLength64 = buffer.Length;
+ await response.OutputStream.WriteAsync(buffer, cancellationToken);
+ }
+ catch (Exception) { }
+ }
+ finally
+ {
+ response.Close();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _ = ex;
+ }
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ _disposed = true;
+ _fileStream.Dispose();
+ _httpListener.Abort();
+ }
+
+ private static int GetAvailablePort()
+ {
+ using var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+
+ return port;
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/AppModels/VideoStreamServer.cs b/src/Platforms/SecureFolderFS.Maui/AppModels/VideoStreamServer.cs
deleted file mode 100644
index 794d66bdc..000000000
--- a/src/Platforms/SecureFolderFS.Maui/AppModels/VideoStreamServer.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-using System.Net;
-using System.Net.Sockets;
-using SecureFolderFS.Shared.ComponentModel;
-
-namespace SecureFolderFS.Maui.AppModels
-{
- internal sealed class VideoStreamServer : IAsyncInitialize, IDisposable
- {
- private readonly HttpListener _httpListener;
- private readonly Stream _videoStream;
- private readonly string _mimeType;
- private readonly int _port;
- private bool _disposed;
-
- public string BaseAddress => $"http://localhost:{_port}/video";
-
- public VideoStreamServer(Stream videoStream, string mimeType)
- {
- _videoStream = videoStream;
- _mimeType = mimeType;
-
- // Automatically find a free port
- _port = GetAvailablePort();
-
- // Initialize the HttpListener with the free port
- _httpListener = new HttpListener();
- _httpListener.Prefixes.Add($"http://localhost:{_port}/");
- }
-
- ///
- public Task InitAsync(CancellationToken cancellationToken = default)
- {
- _httpListener.Start();
- _ = BeginListeningAsync();
-
- return Task.CompletedTask;
-
- async Task BeginListeningAsync()
- {
- while (!_disposed && _httpListener.IsListening && await _httpListener.GetContextAsync() is var context)
- {
- try
- {
- if (context.Request.RawUrl != "/video")
- {
- context.Response.StatusCode = (int)HttpStatusCode.NotFound;
- context.Response.Close();
- continue;
- }
-
- context.Response.StatusCode = (int)HttpStatusCode.OK;
- context.Response.ContentType = _mimeType;
- context.Response.ContentLength64 = _videoStream.Length;
- context.Response.Headers["Accept-Ranges"] = "bytes";
-
- await _videoStream.CopyToAsync(context.Response.OutputStream, 64 * 1024, cancellationToken);
- _videoStream.Position = 0L;
- break;
- }
- catch (Exception)
- {
- break;
- }
- }
- }
- }
-
- ///
- public void Dispose()
- {
- _disposed = true;
- _videoStream.Dispose();
- _httpListener.Abort();
- }
-
- private static int GetAvailablePort()
- {
- using var listener = new TcpListener(IPAddress.Loopback, 0);
- listener.Start();
-
- var port = ((IPEndPoint)listener.LocalEndpoint).Port;
- listener.Stop();
-
- return port;
- }
- }
-}
diff --git a/src/Platforms/SecureFolderFS.Maui/AppShell.xaml.cs b/src/Platforms/SecureFolderFS.Maui/AppShell.xaml.cs
index 76aa8d21c..b055c874e 100644
--- a/src/Platforms/SecureFolderFS.Maui/AppShell.xaml.cs
+++ b/src/Platforms/SecureFolderFS.Maui/AppShell.xaml.cs
@@ -1,11 +1,12 @@
using SecureFolderFS.Maui.Views.Vault;
+using SecureFolderFS.Sdk.AppModels;
using SecureFolderFS.Sdk.ViewModels;
namespace SecureFolderFS.Maui
{
public partial class AppShell : Shell
{
- public MainViewModel MainViewModel { get; } = new();
+ public MainViewModel MainViewModel { get; } = new(new VaultCollectionModel());
public AppShell()
{
@@ -15,6 +16,7 @@ public AppShell()
Routing.RegisterRoute("LoginPage", typeof(LoginPage));
Routing.RegisterRoute("OverviewPage", typeof(OverviewPage));
Routing.RegisterRoute("BrowserPage", typeof(BrowserPage));
+ Routing.RegisterRoute("HealthPage", typeof(HealthPage));
}
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Extensions/IocExtensions.cs b/src/Platforms/SecureFolderFS.Maui/Extensions/IocExtensions.cs
index e156409ce..a73063c16 100644
--- a/src/Platforms/SecureFolderFS.Maui/Extensions/IocExtensions.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Extensions/IocExtensions.cs
@@ -1,9 +1,14 @@
using OwlCore.Storage;
+using Plugin.Maui.BottomSheet.Hosting;
+using Plugin.Maui.BottomSheet.Navigation;
using SecureFolderFS.Maui.ServiceImplementation;
using SecureFolderFS.Maui.ServiceImplementation.Settings;
+using SecureFolderFS.Maui.Sheets;
using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Shared.Extensions;
using SecureFolderFS.UI.ServiceImplementation;
using SecureFolderFS.UI.ServiceImplementation.Settings;
+using AddService = Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions;
namespace SecureFolderFS.Maui.Extensions
{
@@ -12,12 +17,15 @@ internal static class IocExtensions
public static IServiceCollection WithMauiServices(this IServiceCollection serviceCollection, IModifiableFolder settingsFolder)
{
return serviceCollection
- .AddSingleton(_ => new(new MauiAppSettings(settingsFolder), new UserSettings(settingsFolder)))
- .AddSingleton()
- .AddSingleton()
- .AddTransient()
- .AddSingleton()
- //.AddSingleton()
+ .Foundation(AddService.AddSingleton, _ => new(new MauiAppSettings(settingsFolder), new UserSettings(settingsFolder)))
+ .Foundation(AddService.AddSingleton)
+ .Foundation(AddService.AddSingleton)
+ .Foundation(AddService.AddSingleton)
+ .Foundation(AddService.AddSingleton)
+ .Foundation(AddService.AddSingleton)
+ .Foundation(AddService.AddTransient)
+
+ .AddBottomSheet(nameof(ViewOptionsSheet))
;
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Entry.cs b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Entry.cs
new file mode 100644
index 000000000..0ce7d7ac8
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Entry.cs
@@ -0,0 +1,48 @@
+using Microsoft.Maui.Handlers;
+using Microsoft.Maui.Platform;
+using SecureFolderFS.Maui.Helpers;
+using SecureFolderFS.Maui.UserControls.Common;
+using SecureFolderFS.UI.Enums;
+#if ANDROID
+using Android.Graphics.Drawables.Shapes;
+using Paint = Android.Graphics.Paint;
+using ShapeDrawable = Android.Graphics.Drawables.ShapeDrawable;
+#endif
+
+namespace SecureFolderFS.Maui.Extensions.Mappers
+{
+ public static partial class CustomMappers
+ {
+ public static void AddEntryMappers()
+ {
+ EntryHandler.Mapper.AppendToMapping($"{nameof(CustomMappers)}.{nameof(Entry)}", (handler, view) =>
+ {
+ if (view is not ModernEntry)
+ return;
+#if ANDROID
+ const float R = 24f;
+ var outerRadii = new[] { R, R, R, R, R, R, R, R };
+ var roundRectShape = new RoundRectShape(outerRadii, null, null);
+ var shape = new ShapeDrawable(roundRectShape);
+
+ shape.Paint!.Color = (App.Instance.Resources[MauiThemeHelper.Instance.CurrentTheme switch
+ {
+ ThemeType.Dark => "BorderDarkColor",
+ _ => "BorderLightColor"
+ }] as Color)!.ToPlatform();
+ shape.Paint.StrokeWidth = 4;
+ shape.Paint.SetStyle(Paint.Style.Stroke);
+ handler.PlatformView.Background = shape;
+ handler.PlatformView.SetPadding(40, 32,40, 32);
+#elif IOS
+ handler.PlatformView.Layer.BorderColor = (App.Current.Resources[MauiThemeHelper.Instance.CurrentTheme switch
+ {
+ ThemeType.Dark => "BorderDarkColor",
+ _ => "BorderLightColor"
+ }] as Color).ToPlatform().CGColor;
+ handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.RoundedRect;
+#endif
+ });
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Label.cs b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Label.cs
new file mode 100644
index 000000000..6a996d42f
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Label.cs
@@ -0,0 +1,21 @@
+using Microsoft.Maui.Handlers;
+using SecureFolderFS.Maui.UserControls.Common;
+
+namespace SecureFolderFS.Maui.Extensions.Mappers
+{
+ public static partial class CustomMappers
+ {
+ public static void AddLabelMappers()
+ {
+ LabelHandler.Mapper.AppendToMapping($"{nameof(CustomMappers)}.{nameof(Label)}", (handler, view) =>
+ {
+ if (view is not SelectableLabel)
+ return;
+
+#if ANDROID
+ handler.PlatformView.SetTextIsSelectable(true);
+#endif
+ });
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Picker.cs b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Picker.cs
new file mode 100644
index 000000000..af7eec791
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Picker.cs
@@ -0,0 +1,45 @@
+using Microsoft.Maui.Handlers;
+using Microsoft.Maui.Platform;
+using SecureFolderFS.Maui.UserControls.Common;
+using SecureFolderFS.UI.Enums;
+#if ANDROID
+using Android.Graphics.Drawables.Shapes;
+using SecureFolderFS.Maui.Helpers;
+using Paint = Android.Graphics.Paint;
+using ShapeDrawable = Android.Graphics.Drawables.ShapeDrawable;
+#endif
+
+namespace SecureFolderFS.Maui.Extensions.Mappers
+{
+ public static partial class CustomMappers
+ {
+ public static void AddPickerMappers()
+ {
+ PickerHandler.Mapper.AppendToMapping($"{nameof(CustomMappers)}.{nameof(Picker)}", (handler, view) =>
+ {
+ if (view is not ModernPicker modernPicker)
+ return;
+
+#if ANDROID
+ const float R = 24f;
+ var outerRadii = new[] { R, R, R, R, R, R, R, R };
+ var roundRectShape = new RoundRectShape(outerRadii, null, null);
+ var shape = new ShapeDrawable(roundRectShape);
+
+ shape.Paint!.Color = modernPicker.IsTransparent
+ ? Colors.Transparent.ToPlatform()
+ : (App.Instance.Resources["ThemeSecondaryColorBrush"] as SolidColorBrush)!.Color.ToPlatform();
+ shape.Paint.StrokeWidth = 0;
+ shape.Paint.SetStyle(Paint.Style.FillAndStroke);
+ handler.PlatformView.SetTextColor((App.Instance.Resources[MauiThemeHelper.Instance.CurrentTheme switch
+ {
+ ThemeType.Dark => "QuarternaryDarkColor",
+ _ => "QuarternaryLightColor"
+ }] as Color)!.ToPlatform());
+ handler.PlatformView.Background = shape;
+ handler.PlatformView.SetPadding(32, 24, 32, 24);
+#endif
+ });
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Helpers/DeferredInitialization.cs b/src/Platforms/SecureFolderFS.Maui/Helpers/DeferredInitialization.cs
new file mode 100644
index 000000000..0caf1a4df
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Helpers/DeferredInitialization.cs
@@ -0,0 +1,81 @@
+using SecureFolderFS.Shared.ComponentModel;
+
+namespace SecureFolderFS.Maui.Helpers
+{
+ internal sealed class DeferredInitialization : IDisposable
+ where T : notnull
+ {
+ private T? _context;
+ private bool _isProcessing;
+ private readonly List _initializations = new();
+ private readonly SemaphoreSlim _semaphore = new(1, 1);
+
+ public void SetContext(T context)
+ {
+ if (context.Equals(_context))
+ return;
+
+ _context = context;
+ lock (_initializations)
+ _initializations.Clear();
+ }
+
+ public void Enqueue(IAsyncInitialize asyncInitialize)
+ {
+ if (_context is null)
+ return;
+
+ lock (_initializations)
+ _initializations.Add(asyncInitialize);
+
+ _ = StartProcessingAsync();
+ }
+
+ private async Task StartProcessingAsync()
+ {
+ await _semaphore.WaitAsync();
+
+ try
+ {
+ if (_isProcessing)
+ return;
+
+ _isProcessing = true;
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+
+ try
+ {
+ while (true)
+ {
+ IAsyncInitialize[] batch;
+ lock (_initializations)
+ {
+ if (_initializations.Count == 0)
+ break;
+
+ batch = _initializations.Take(UI.Constants.Browser.THUMBNAIL_MAX_PARALLELISATION).ToArray();
+ _initializations.RemoveRange(0, batch.Length);
+ }
+
+ var tasks = batch.Select(init => init.InitAsync());
+ await Task.Run(async () => await Task.WhenAll(tasks));
+ }
+ }
+ finally
+ {
+ _isProcessing = false;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ _initializations.Clear();
+ _semaphore.Dispose();
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Helpers/MauiThemeHelper.cs b/src/Platforms/SecureFolderFS.Maui/Helpers/MauiThemeHelper.cs
new file mode 100644
index 000000000..81929caec
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Helpers/MauiThemeHelper.cs
@@ -0,0 +1,22 @@
+using SecureFolderFS.UI.Helpers;
+
+namespace SecureFolderFS.Maui.Helpers
+{
+ ///
+ internal sealed class MauiThemeHelper : ThemeHelper
+ {
+ ///
+ /// Gets the singleton instance of .
+ ///
+ public static MauiThemeHelper Instance { get; } = new();
+
+ ///
+ protected override void UpdateTheme()
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ App.Instance.UserAppTheme = (AppTheme)(int)CurrentTheme;
+ });
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Localization/ResourceString.cs b/src/Platforms/SecureFolderFS.Maui/Localization/ResourceString.cs
index b96c46b33..c8b755050 100644
--- a/src/Platforms/SecureFolderFS.Maui/Localization/ResourceString.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Localization/ResourceString.cs
@@ -5,6 +5,7 @@
namespace SecureFolderFS.Maui.Localization
{
[EditorBrowsable(EditorBrowsableState.Never)]
+ [AcceptEmptyServiceProvider]
internal sealed class ResourceString : IMarkupExtension
{
private static ILocalizationService? LocalizationService { get; set; }
diff --git a/src/Platforms/SecureFolderFS.Maui/MauiProgram.cs b/src/Platforms/SecureFolderFS.Maui/MauiProgram.cs
index 93b27c8a9..5e9376633 100644
--- a/src/Platforms/SecureFolderFS.Maui/MauiProgram.cs
+++ b/src/Platforms/SecureFolderFS.Maui/MauiProgram.cs
@@ -1,10 +1,8 @@
using APES.UI.XF;
using CommunityToolkit.Maui;
-using Microsoft.Extensions.Logging;
-using The49.Maui.BottomSheet;
-
+using LibVLCSharp.MAUI;
+using Plugin.Maui.BottomSheet.Hosting;
#if ANDROID
-using Material.Components.Maui.Extensions;
using MauiIcons.Material;
#elif IOS
using MauiIcons.Cupertino;
@@ -28,16 +26,16 @@ public static MauiApp CreateMauiApp()
// Plugins
.UseMauiCommunityToolkitMediaElement() // https://github.com/CommunityToolkit/Maui
.UseMauiCommunityToolkit() // https://github.com/CommunityToolkit/Maui
- .UseBottomSheet() // https://github.com/the49ltd/The49.Maui.BottomSheet
+ .UseBottomSheet() // https://github.com/lucacivale/Maui.BottomSheet
.ConfigureContextMenuContainer() // https://github.com/anpin/ContextMenuContainer
+ .UseLibVLCSharp() // https://github.com/videolan/libvlcsharp
#if ANDROID
.UseMaterialMauiIcons() // https://github.com/AathifMahir/MauiIcons
- .UseMaterialComponents() // https://github.com/mdc-maui/mdc-maui
#elif IOS
.UseCupertinoMauiIcons()
#endif
-
+
// Handlers
.ConfigureMauiHandlers(handlers =>
{
@@ -45,7 +43,7 @@ public static MauiApp CreateMauiApp()
handlers.AddHandler();
#endif
})
-
+
; // Finish initialization
return builder.Build();
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AndroidManifest.xml b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AndroidManifest.xml
index 19750b79f..444981d92 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AndroidManifest.xml
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AndroidManifest.xml
@@ -1,6 +1,10 @@
-
+
@@ -16,5 +20,6 @@
+
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/BiometricPromptCallback.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/BiometricPromptCallback.cs
new file mode 100644
index 000000000..a43df0c62
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/BiometricPromptCallback.cs
@@ -0,0 +1,41 @@
+using Android.Hardware.Biometrics;
+using Java.Lang;
+
+namespace SecureFolderFS.Maui.AppModels
+{
+ ///
+ internal sealed class BiometricPromptCallback : BiometricPrompt.AuthenticationCallback
+ {
+ private readonly Action? _onSuccess;
+ private readonly Action? _onError;
+ private readonly Action? _onFailure;
+
+ public BiometricPromptCallback(
+ Action? onSuccess,
+ Action? onError,
+ Action? onFailure)
+ {
+ _onSuccess = onSuccess;
+ _onError = onError;
+ _onFailure = onFailure;
+ }
+
+ ///
+ public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult? result)
+ {
+ _onSuccess?.Invoke(result);
+ }
+
+ ///
+ public override void OnAuthenticationError(BiometricErrorCode errorCode, ICharSequence? errString)
+ {
+ _onError?.Invoke(errorCode, errString?.ToString());
+ }
+
+ ///
+ public override void OnAuthenticationFailed()
+ {
+ _onFailure?.Invoke();
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/DialogOnClickListener.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/DialogOnClickListener.cs
new file mode 100644
index 000000000..7876743dc
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/DialogOnClickListener.cs
@@ -0,0 +1,21 @@
+using Android.Content;
+
+namespace SecureFolderFS.Maui.AppModels
+{
+ ///
+ internal sealed class DialogOnClickListener : Java.Lang.Object, IDialogInterfaceOnClickListener
+ {
+ private readonly Action _onClick;
+
+ public DialogOnClickListener(Action onClick)
+ {
+ _onClick = onClick;
+ }
+
+ ///
+ public void OnClick(IDialogInterface? dialog, int which)
+ {
+ _onClick.Invoke(dialog, which);
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/StreamedMediaSource.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/StreamedMediaSource.cs
new file mode 100644
index 000000000..bff7afcbf
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/StreamedMediaSource.cs
@@ -0,0 +1,35 @@
+using Android.Media;
+using Stream = System.IO.Stream;
+
+namespace SecureFolderFS.Maui.AppModels
+{
+ ///
+ internal sealed class StreamedMediaSource : MediaDataSource
+ {
+ private readonly Stream _sourceStream;
+
+ ///
+ public override long Size => _sourceStream.Length;
+
+ public StreamedMediaSource(Stream sourceStream)
+ {
+ _sourceStream = sourceStream;
+ }
+
+ ///
+ public override int ReadAt(long position, byte[]? buffer, int offset, int size)
+ {
+ if (buffer is null)
+ return 0;
+
+ _sourceStream.Position = position;
+ return _sourceStream.Read(buffer, offset, size);
+ }
+
+ ///
+ public override void Close()
+ {
+ _sourceStream.Dispose();
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Assets/pdfjs53.zip b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Assets/pdfjs53.zip
new file mode 100644
index 000000000..107b9009b
Binary files /dev/null and b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Assets/pdfjs53.zip differ
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidBiometricHelpers.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidBiometricHelpers.cs
new file mode 100644
index 000000000..e79b2cb1e
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidBiometricHelpers.cs
@@ -0,0 +1,32 @@
+using Android.Security.Keystore;
+using Java.Security;
+
+namespace SecureFolderFS.Maui.Platforms.Android.Helpers
+{
+ internal static class AndroidBiometricHelpers
+ {
+ public static KeyPairGenerator? GetKeyPairGenerator(string keyName, string keyStoreProvider)
+ {
+ var keyPairGenerator = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, keyStoreProvider);
+ if (keyPairGenerator is null)
+ return null;
+
+ var builder = new KeyGenParameterSpec.Builder(
+ keyName,
+ KeyStorePurpose.Sign)
+ .SetDigests(KeyProperties.DigestSha256)
+ .SetSignaturePaddings(KeyProperties.SignaturePaddingRsaPkcs1)
+ .SetUserAuthenticationRequired(true)
+ .SetInvalidatedByBiometricEnrollment(false);
+
+ keyPairGenerator.Initialize(builder.Build());
+ return keyPairGenerator;
+ }
+
+ public static byte[]? SignData(Signature signature, byte[] data)
+ {
+ signature.Update(data);
+ return signature.Sign();
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidLifecycleHelper.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidLifecycleHelper.cs
index 6d111f2fa..8f527955f 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidLifecycleHelper.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidLifecycleHelper.cs
@@ -1,12 +1,13 @@
-using CommunityToolkit.Maui.Alerts;
-using CommunityToolkit.Maui.Core;
using OwlCore.Storage;
using OwlCore.Storage.System.IO;
using SecureFolderFS.Maui.Extensions;
using SecureFolderFS.Maui.Platforms.Android.ServiceImplementation;
using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Shared.Extensions;
+using SecureFolderFS.UI;
using SecureFolderFS.UI.Helpers;
using SecureFolderFS.UI.ServiceImplementation;
+using AddService = Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions;
namespace SecureFolderFS.Maui.Platforms.Android.Helpers
{
@@ -14,13 +15,13 @@ namespace SecureFolderFS.Maui.Platforms.Android.Helpers
internal sealed class AndroidLifecycleHelper : BaseLifecycleHelper
{
///
- protected override string AppDirectory { get; } = Microsoft.Maui.Storage.FileSystem.Current.AppDataDirectory;
+ protected override string AppDirectory { get; } = FileSystem.Current.AppDataDirectory;
///
public override Task InitAsync(CancellationToken cancellationToken = default)
{
// Initialize settings
- var settingsFolderPath = Path.Combine(AppDirectory, SecureFolderFS.UI.Constants.FileNames.SETTINGS_FOLDER_NAME);
+ var settingsFolderPath = Path.Combine(AppDirectory, Constants.FileNames.SETTINGS_FOLDER_NAME);
var settingsFolder = new SystemFolder(Directory.CreateDirectory(settingsFolderPath));
ConfigureServices(settingsFolder);
@@ -37,58 +38,19 @@ public override void LogExceptionToFile(Exception? ex)
protected override IServiceCollection ConfigureServices(IModifiableFolder settingsFolder)
{
return base.ConfigureServices(settingsFolder)
- //.AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
-
+ //.Override(AddService.AddSingleton)
+ .Override(AddService.AddSingleton)
+ .Override(AddService.AddSingleton)
+ .Override(AddService.AddSingleton)
+ .Override(AddService.AddSingleton)
+ .Override(AddService.AddSingleton)
+ .Override(AddService.AddSingleton)
+ .Override(AddService.AddSingleton)
+ .Override(AddService.AddSingleton)
+ .Override(AddService.AddSingleton)
+
.WithMauiServices(settingsFolder)
;
}
-
- private async Task RequestPermissionsAsync()
- where TPermission : Permissions.BasePermission, new()
- {
- var permissionStatus = await Permissions.CheckStatusAsync();
- switch (permissionStatus)
- {
- case PermissionStatus.Denied:
- if (Permissions.ShouldShowRationale())
- {
- await Shell.Current.DisplayAlert("Action required",
- "For SecureFolderFS to function correctly, you'll need to grant the storage permission.",
- "Ok");
- }
-
- if (await Permissions.RequestAsync() != PermissionStatus.Granted)
- {
- await Toast.Make("Storage permissions are required for SecureFolderFS", ToastDuration.Long).Show();
- Application.Current?.Quit();
- }
-
- return;
-
- case PermissionStatus.Disabled:
- case PermissionStatus.Restricted:
- await Toast.Make("Storage permissions are required for SecureFolderFS", ToastDuration.Long).Show();
- Application.Current?.Quit();
- return;
-
- case PermissionStatus.Granted:
- return;
-
- default:
- case PermissionStatus.Limited:
- case PermissionStatus.Unknown:
- return;
- }
- }
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/MainActivity.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/MainActivity.cs
index f55b81860..84b675fef 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/MainActivity.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/MainActivity.cs
@@ -1,6 +1,12 @@
+using System.ComponentModel;
using Android.App;
using Android.Content;
using Android.Content.PM;
+using Android.OS;
+using AndroidX.Activity;
+using Microsoft.Maui.Platform;
+using SecureFolderFS.Maui.Helpers;
+using SecureFolderFS.UI.Enums;
namespace SecureFolderFS.Maui
{
@@ -22,11 +28,45 @@ public MainActivity()
Instance ??= this;
}
+ ///
+ protected override void OnCreate(Bundle? savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+
+ // Enable edge to edge
+ EdgeToEdge.Enable(this);
+
+ // Configure StatusBar color
+ ApplyStatusBarColor(MauiThemeHelper.Instance.CurrentTheme);
+
+ // Always listen for theme changes
+ MauiThemeHelper.Instance.PropertyChanged += ThemeHelper_PropertyChanged;
+ }
+
///
protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data)
{
base.OnActivityResult(requestCode, resultCode, data);
ActivityResult?.Invoke(requestCode, resultCode, data);
}
+
+ private void ThemeHelper_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(MauiThemeHelper.CurrentTheme))
+ return;
+
+ ApplyStatusBarColor(MauiThemeHelper.Instance.CurrentTheme);
+ }
+
+ private void ApplyStatusBarColor(ThemeType themeType)
+ {
+#pragma warning disable CA1422
+ Window?.SetStatusBarColor((App.Instance.Resources[themeType switch
+ {
+ ThemeType.Dark => "PrimaryDarkColor",
+ _ => "PrimaryLightColor"
+ }] as Color)!.ToPlatform());
+#pragma warning restore CA1422
+ }
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/colors.xml b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/colors.xml
index 491dfad4d..01e1a4a59 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/colors.xml
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/colors.xml
@@ -1,6 +1,6 @@
- #32608d
- #1791ff
- #106bbf
+ #1791FF
+ #106bbf
+ #1375d1
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/styles.xml b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/styles.xml
deleted file mode 100644
index 49937c5ed..000000000
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/styles.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/xml/network_security_config.xml b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/xml/network_security_config.xml
new file mode 100644
index 000000000..d4950ca4e
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/xml/network_security_config.xml
@@ -0,0 +1,6 @@
+
+
+
+ localhost
+
+
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidApplicationService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidApplicationService.cs
index dada41b18..a9cb589d8 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidApplicationService.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidApplicationService.cs
@@ -13,7 +13,7 @@ internal sealed class AndroidApplicationService : BaseApplicationService
///
public override string Platform { get; } = "Android - MAUI";
-
+
///
public override Task OpenUriAsync(Uri uri)
{
@@ -30,11 +30,11 @@ public override Task OpenUriAsync(Uri uri)
return Task.CompletedTask;
}
-
+
///
public override string GetSystemVersion()
{
- return DeviceInfo.VersionString;
+ return $"Android {DeviceInfo.VersionString}";
}
///
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidFileExplorerService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidFileExplorerService.cs
index 00506ff75..63cc85c5b 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidFileExplorerService.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidFileExplorerService.cs
@@ -1,12 +1,13 @@
using System.Web;
-using CommunityToolkit.Maui.Core.Extensions;
-using CommunityToolkit.Maui.Storage;
-using OwlCore.Storage;
-using SecureFolderFS.Sdk.Services;
using Android.App;
using Android.Content;
using Android.Provider;
+using CommunityToolkit.Maui.Core.Extensions;
+using CommunityToolkit.Maui.Storage;
+using OwlCore.Storage;
using SecureFolderFS.Maui.Platforms.Android.Storage;
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Storage.Pickers;
using AndroidUri = Android.Net.Uri;
using AOSEnvironment = Android.OS.Environment;
@@ -16,23 +17,7 @@ namespace SecureFolderFS.Maui.Platforms.Android.ServiceImplementation
internal sealed class AndroidFileExplorerService : IFileExplorerService
{
///
- public Task TryOpenInFileExplorerAsync(IFolder folder, CancellationToken cancellationToken = default)
- {
- // TODO: Try to implement opening in android file explorer
- return Task.CompletedTask;
- }
-
- ///
- public async Task SaveFileAsync(string suggestedName, Stream dataStream, IDictionary? filter, CancellationToken cancellationToken = default)
- {
- var fileSaver = FileSaver.Default;
- var result = await fileSaver.SaveAsync(suggestedName, dataStream, cancellationToken);
-
- return result.IsSuccessful;
- }
-
- ///
- public async Task PickFileAsync(IEnumerable? filter, bool persist = true, CancellationToken cancellationToken = default)
+ public async Task PickFileAsync(PickerOptions? options, bool offerPersistence = true, CancellationToken cancellationToken = default)
{
var intent = new Intent(Intent.ActionOpenDocument)
.AddCategory(Intent.CategoryOpenable)
@@ -41,20 +26,18 @@ public async Task SaveFileAsync(string suggestedName, Stream dataStream, I
var pickerIntent = Intent.CreateChooser(intent, "Select file");
+ // TODO: Determine if GrantReadUriPermission and GrantWriteUriPermission are needed for access persistence
+
// FilePicker 0x2AF9
var result = await StartActivityAsync(pickerIntent, 0x2AF9);
if (result is null || MainActivity.Instance is null)
return null;
- var file = new AndroidFile(result, MainActivity.Instance);
- if (persist)
- await file.AddBookmarkAsync(cancellationToken);
-
- return file;
+ return new AndroidFile(result, MainActivity.Instance);
}
///
- public async Task PickFolderAsync(bool persist = true, CancellationToken cancellationToken = default)
+ public async Task PickFolderAsync(PickerOptions? options, bool offerPersistence = true, CancellationToken cancellationToken = default)
{
var initialPath = AndroidPathExtensions.GetExternalDirectory();
if (AOSEnvironment.ExternalStorageDirectory is not null)
@@ -64,7 +47,7 @@ public async Task SaveFileAsync(string suggestedName, Stream dataStream, I
var intent = new Intent(Intent.ActionOpenDocumentTree);
intent.PutExtra(DocumentsContract.ExtraInitialUri, initialFolderUri);
- if (persist)
+ if (offerPersistence)
{
intent.AddFlags(ActivityFlags.GrantReadUriPermission);
intent.AddFlags(ActivityFlags.GrantWriteUriPermission);
@@ -75,11 +58,40 @@ public async Task SaveFileAsync(string suggestedName, Stream dataStream, I
if (result is null || MainActivity.Instance is null)
return null;
- var folder = new AndroidFolder(result, MainActivity.Instance);
- if (persist)
- await folder.AddBookmarkAsync(cancellationToken);
+ return new AndroidFolder(result, MainActivity.Instance);
+ }
- return folder;
+ ///
+ public Task TryOpenInFileExplorerAsync(IFolder folder, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var context = MainActivity.Instance;
+ if (context is null)
+ return Task.CompletedTask;
+
+ var intent = new Intent(Intent.ActionView);
+ intent.SetType("*/*");
+ intent.AddCategory(Intent.CategoryDefault);
+ intent.AddFlags(ActivityFlags.NewTask);
+ context.StartActivity(intent);
+
+ return Task.CompletedTask;
+ }
+ catch (Exception ex)
+ {
+ _ = ex;
+ return Task.CompletedTask;
+ }
+ }
+
+ ///
+ public async Task SaveFileAsync(string suggestedName, Stream dataStream, IDictionary? filter, CancellationToken cancellationToken = default)
+ {
+ var fileSaver = FileSaver.Default;
+ var result = await fileSaver.SaveAsync(suggestedName, dataStream, cancellationToken);
+
+ return result.IsSuccessful;
}
private async Task StartActivityAsync(Intent? pickerIntent, int requestCode)
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidMediaService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidMediaService.cs
new file mode 100644
index 000000000..014f23153
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidMediaService.cs
@@ -0,0 +1,59 @@
+using Android.Media;
+using OwlCore.Storage;
+using SecureFolderFS.Core.MobileFS.Platforms.Android.Helpers;
+using SecureFolderFS.Maui.AppModels;
+using SecureFolderFS.Maui.ServiceImplementation;
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Shared.Enums;
+using SecureFolderFS.Shared.Helpers;
+using SecureFolderFS.UI;
+using Stream = System.IO.Stream;
+
+namespace SecureFolderFS.Maui.Platforms.Android.ServiceImplementation
+{
+ ///
+ internal sealed class AndroidMediaService : BaseMauiMediaService
+ {
+ ///
+ public override async Task GenerateThumbnailAsync(IFile file, TypeHint typeHint = default, CancellationToken cancellationToken = default)
+ {
+ if (typeHint == default)
+ typeHint = FileTypeHelper.GetTypeHint(file);
+
+ switch (typeHint)
+ {
+ case TypeHint.Image:
+ {
+ await using var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false);
+ var imageStream = await ThumbnailHelpers.GenerateImageThumbnailAsync(stream, Constants.Browser.IMAGE_THUMBNAIL_MAX_SIZE).ConfigureAwait(false);
+
+ return new ImageStream(imageStream);
+ }
+
+ case TypeHint.Media:
+ {
+ await using var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false);
+ var imageStream = await GenerateVideoThumbnailAsync(stream, TimeSpan.FromSeconds(0)).ConfigureAwait(false);
+
+ return new ImageStream(imageStream);
+ }
+
+ default: throw new InvalidOperationException("The provided file type is invalid.");
+ }
+ }
+
+ private static async Task GenerateVideoThumbnailAsync(Stream stream, TimeSpan captureTime)
+ {
+ using var retriever = new MediaMetadataRetriever();
+ await retriever.SetDataSourceAsync(new StreamedMediaSource(stream)).ConfigureAwait(false);
+
+ // Use scaled frame for efficiency
+ using var bitmap = retriever.GetScaledFrameAtTime(captureTime.Ticks, Option.ClosestSync, 320, 240);
+ if (bitmap is null)
+ throw new NotSupportedException("Could not retrieve scaled frame.");
+
+ return await ThumbnailHelpers.CompressBitmapAsync(bitmap).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidSystemService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidSystemService.cs
index 0b71db01b..d1e381cd1 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidSystemService.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidSystemService.cs
@@ -1,3 +1,5 @@
+using OwlCore.Storage;
+using SecureFolderFS.Maui.Platforms.Android.Storage;
using SecureFolderFS.Sdk.Services;
namespace SecureFolderFS.Maui.Platforms.Android.ServiceImplementation
@@ -6,8 +8,47 @@ namespace SecureFolderFS.Maui.Platforms.Android.ServiceImplementation
internal sealed class AndroidSystemService : ISystemService
{
// TODO: Use BroadcastReceiver - ActionScreenOff, ActionUserPresent
-
+
+ ///
+ public event EventHandler? DeviceLocked;
+
///
- public event EventHandler? DesktopLocked;
+ public Task GetAvailableFreeSpaceAsync(IFolder storageRoot, CancellationToken cancellationToken = default)
+ {
+#if ANDROID
+ if (storageRoot is not AndroidFolder androidFolder)
+ return Task.FromException(new ArgumentNullException(nameof(storageRoot)));
+
+ // fallback: if we can't resolve, assume /storage/emulated/0
+ var realPath = TryGetRealPath(androidFolder) ?? "/storage/emulated/0";
+ var stat = new global::Android.OS.StatFs(realPath);
+
+ long availableBytes;
+#if ANDROID33_0_OR_GREATER
+ availableBytes = stat.AvailableBytes;
+#else
+ availableBytes = (long)stat.BlockSizeLong * (long)stat.AvailableBlocksLong;
+#endif
+
+ return Task.FromResult(availableBytes);
+
+ static string? TryGetRealPath(AndroidFolder androidFolder)
+ {
+ if (androidFolder.Inner is not { Scheme: "content", Authority: "com.android.externalstorage.documents" })
+ return null; // Couldn't resolve
+
+ var split = androidFolder.Inner.Path?.Split(':');
+ var relativePath = string.Join('/', split?.Skip(1).Take(Range.All) ?? []);
+ var type = split?.FirstOrDefault();
+ if (type?.Contains("primary", StringComparison.OrdinalIgnoreCase) ?? true)
+ return $"/storage/emulated/0/{relativePath}";
+
+ return $"/storage/{type}/{relativePath}";
+ }
+#else
+ throw new PlatformNotSupportedException("Only implemented on Android.");
+#endif
+ }
+
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs
index 89360c239..4cebf3485 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs
@@ -2,6 +2,7 @@
using OwlCore.Storage;
using SecureFolderFS.Core.Cryptography;
using SecureFolderFS.Core.VaultAccess;
+using SecureFolderFS.Maui.Platforms.Android.ViewModels;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Sdk.ViewModels.Controls.Authentication;
using SecureFolderFS.Shared.Models;
@@ -22,6 +23,10 @@ public override IEnumerable GetContentCiphers()
// - https://nsec.rocks/docs/install#supported-platforms
yield return Constants.CipherId.AES_GCM;
+
+#if DEBUG
+ yield return Constants.CipherId.NONE;
+#endif
}
///
@@ -29,14 +34,15 @@ public override async IAsyncEnumerable GetLoginAsync(IF
{
var vaultReader = new VaultReader(vaultFolder, StreamSerializer.Instance);
var config = await vaultReader.ReadConfigurationAsync(cancellationToken);
- var authenticationMethods = config.AuthenticationMethod.Split(Core.Constants.Vault.Authentication.SEPARATOR);
+ var authenticationMethod = AuthenticationMethod.FromString(config.AuthenticationMethod);
- foreach (var item in authenticationMethods)
+ foreach (var item in authenticationMethod.Methods)
{
yield return item switch
{
Core.Constants.Vault.Authentication.AUTH_PASSWORD => new PasswordLoginViewModel(),
Core.Constants.Vault.Authentication.AUTH_KEYFILE => new KeyFileLoginViewModel(vaultFolder),
+ Core.Constants.Vault.Authentication.AUTH_ANDROID_BIOMETRIC => new AndroidBiometricLoginViewModel(vaultFolder, config.Uid),
_ => throw new NotSupportedException($"The authentication method '{item}' is not supported by the platform.")
};
}
@@ -49,6 +55,9 @@ public override async IAsyncEnumerable GetCreationAsync
// Password
yield return new PasswordCreationViewModel();
+ // Android Biometric
+ yield return new AndroidBiometricCreationViewModel(vaultFolder, vaultId);
+
// Key File
yield return new KeyFileCreationViewModel(vaultId);
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFile.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFile.cs
index 2d06088bf..a5888f02e 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFile.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFile.cs
@@ -1,8 +1,8 @@
using Android.App;
using Android.Content;
using Android.Provider;
-using Android.Runtime;
using AndroidX.DocumentFile.Provider;
+using Java.IO;
using OwlCore.Storage;
using SecureFolderFS.Core.MobileFS.Platforms.Android.Streams;
using SecureFolderFS.Maui.Platforms.Android.Storage.StorageProperties;
@@ -30,10 +30,9 @@ public AndroidFile(AndroidUri uri, Activity activity, AndroidFolder? parent = nu
///
public Task OpenStreamAsync(FileAccess accessMode, CancellationToken cancellationToken = default)
{
- Stream? stream;
if (IsVirtualFile(activity, Inner))
{
- stream = GetVirtualFileStream(activity, Inner, accessMode != FileAccess.Read);
+ var stream = GetVirtualFileStream(activity, Inner, accessMode != FileAccess.Read);
if (stream is null)
return Task.FromException(new ArgumentException("No stream types available for '*/*' mime type."));
@@ -42,25 +41,27 @@ public Task OpenStreamAsync(FileAccess accessMode, CancellationToken can
if (accessMode == FileAccess.Read)
{
- stream = activity.ContentResolver?.OpenInputStream(Inner);
+ var inStream = activity.ContentResolver?.OpenInputStream(Inner);
+ if (inStream is null)
+ return Task.FromException(new UnauthorizedAccessException("Could not open input stream."));
+
+ return Task.FromResult(inStream);
}
else
{
- var inputStream = (activity.ContentResolver?.OpenInputStream(Inner) as InputStreamInvoker)?.BaseInputStream;
- var outputStream = (activity.ContentResolver?.OpenOutputStream(Inner) as OutputStreamInvoker)?.BaseOutputStream;
- if (inputStream is null || outputStream is null)
- return Task.FromException(new UnauthorizedAccessException($"Could not open a stream to: '{Id}'."));
+ var fd = activity.ContentResolver?.OpenFileDescriptor(Inner, "rwt");
+ var fInChannel = new FileInputStream(fd.FileDescriptor).Channel;
+ var fOutChannel = new FileOutputStream(fd.FileDescriptor).Channel;
- var combinedInputStream = new InputOutputStream(inputStream, outputStream, GetFileSize(activity.ContentResolver, Inner));
- stream = combinedInputStream;
- }
-
- if (stream is null)
- return Task.FromException(new UnauthorizedAccessException($"Could not open a stream to: '{Id}'."));
+ if (fInChannel is null || fOutChannel is null)
+ return Task.FromException(
+ new ArgumentException("Could not open input and output streams."));
- return Task.FromResult(stream);
+ var channelledStream = new ChannelledStream(fInChannel, fOutChannel);
+ return Task.FromResult(channelledStream);
+ }
}
-
+
///
public override Task GetPropertiesAsync()
{
@@ -90,47 +91,16 @@ private static bool IsVirtualFile(Context context, AndroidUri uri)
private static Stream? GetVirtualFileStream(Context context, AndroidUri uri, bool isOutput)
{
var mimeTypes = context.ContentResolver?.GetStreamTypes(uri, "*/*");
- if (mimeTypes?.Length >= 1)
- {
- var mimeType = mimeTypes[0];
- var asset = context.ContentResolver!
- .OpenTypedAssetFileDescriptor(uri, mimeType, null);
-
- var stream = isOutput
- ? asset?.CreateOutputStream()
- : asset?.CreateInputStream();
-
- return stream;
- }
+ if (!(mimeTypes?.Length >= 1))
+ return null;
- return null;
- }
-
- private static long GetFileSize(ContentResolver? contentResolver, AndroidUri uri)
- {
- if (contentResolver is null)
- return 0L;
-
- try
- {
- // Try to get file size using content resolver
- using var cursor = contentResolver.Query(uri, null, null, null, null);
- if (cursor != null && cursor.MoveToFirst())
- {
- int sizeIndex = cursor.GetColumnIndex(IOpenableColumns.Size);
- if (sizeIndex != -1)
- {
- return cursor.GetLong(sizeIndex);
- }
- }
- }
- catch
- {
- // Fallback method if content resolver fails
- }
+ var mimeType = mimeTypes[0];
+ var asset = context.ContentResolver!.OpenTypedAssetFileDescriptor(uri, mimeType, null);
+ var stream = isOutput
+ ? asset?.CreateOutputStream()
+ : asset?.CreateInputStream();
- // If size can't be determined, return -1
- return -1;
+ return stream;
}
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFolder.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFolder.cs
index eb88cd176..a2f757599 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFolder.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFolder.cs
@@ -2,7 +2,6 @@
using Android.Provider;
using Android.Webkit;
using AndroidX.DocumentFile.Provider;
-using AndroidX.Navigation;
using OwlCore.Storage;
using SecureFolderFS.Maui.Platforms.Android.Storage.StorageProperties;
using SecureFolderFS.Storage.Renamable;
@@ -16,10 +15,10 @@ namespace SecureFolderFS.Maui.Platforms.Android.Storage
internal sealed class AndroidFolder : AndroidStorable, IModifiableFolder, IChildFolder, IGetFirstByName, IRenamableFolder // TODO: Implement: IGetFirstByName, IGetItem
{
private static Exception RenameException { get; } = new IOException("Could not rename the item.");
-
+
///
public override string Name { get; }
-
+
///
public override DocumentFile? Document { get; }
@@ -154,6 +153,8 @@ public Task CreateFileAsync(string name, bool overwrite = false, Can
if (overwrite && existingFile is not null)
existingFile.Delete();
+ else if (existingFile is not null)
+ return Task.FromResult(new AndroidFile(existingFile.Uri, activity, this, permissionRoot));
var newFile = Document?.CreateFile(mimeType, name);
if (newFile is null)
@@ -186,14 +187,14 @@ public async Task GetFirstByNameAsync(string name, CancellationT
return target;
}
-
+
///
public override Task GetPropertiesAsync()
{
if (Document is null)
return Task.FromException(new ArgumentNullException(nameof(Document)));
- properties ??= new AndroidFileProperties(Document);
+ properties ??= new AndroidFolderProperties(Document);
return Task.FromResult(properties);
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidStorable.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidStorable.cs
index 3e37cad66..ddb5874d1 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidStorable.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidStorable.cs
@@ -43,9 +43,9 @@ protected AndroidStorable(AndroidUri uri, Activity activity, AndroidFolder? pare
this.permissionRoot = permissionRoot ?? uri;
Inner = uri;
- BookmarkId = bookmarkId;
- Id = Inner.ToString() ?? string.Empty;
+ Id = uri.ToString() ?? string.Empty;
Name = GetFileName(uri);
+ BookmarkId = bookmarkId;
}
///
@@ -89,7 +89,7 @@ public Task RemoveBookmarkAsync(CancellationToken cancellationToken = default)
return Task.CompletedTask;
}
-
+
///
public abstract Task GetPropertiesAsync();
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/StorageProperties/AndroidFileProperties.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/StorageProperties/AndroidFileProperties.cs
index 4f145d407..f911eba97 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/StorageProperties/AndroidFileProperties.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/StorageProperties/AndroidFileProperties.cs
@@ -6,7 +6,7 @@
namespace SecureFolderFS.Maui.Platforms.Android.Storage.StorageProperties
{
///
- internal sealed class AndroidFileProperties : ISizeProperties, IBasicProperties
+ internal sealed class AndroidFileProperties : ISizeProperties, IDateProperties, IBasicProperties
{
private readonly DocumentFile _document;
@@ -19,13 +19,32 @@ public AndroidFileProperties(DocumentFile document)
public Task?> GetSizeAsync(CancellationToken cancellationToken = default)
{
var sizeProperty = new GenericProperty(_document.Length());
- return Task.FromResult>(sizeProperty);
+ return Task.FromResult?>(sizeProperty);
+ }
+
+ ///
+ public Task> GetDateCreatedAsync(CancellationToken cancellationToken = default)
+ {
+ // Created date is not available on Android
+ return GetDateModifiedAsync(cancellationToken);
+ }
+
+ ///
+ public Task> GetDateModifiedAsync(CancellationToken cancellationToken = default)
+ {
+ var timestamp = _document.LastModified();
+ var dateModified = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).UtcDateTime;
+ var dateProperty = new GenericProperty(dateModified);
+
+ return Task.FromResult>(dateProperty);
}
///
public async IAsyncEnumerable> GetPropertiesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
yield return await GetSizeAsync(cancellationToken) as IStorageProperty