diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index bd6eb4e903..b0b0712480 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -8,8 +8,6 @@ - - diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs index b5e4270028..d0538b6304 100644 --- a/src/Android/Avalonia.Android/AvaloniaActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs @@ -8,6 +8,7 @@ using Android.OS; using Android.Runtime; using Android.Views; using AndroidX.AppCompat.App; +using Avalonia.Android.Platform.Storage; using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Android; @@ -80,12 +81,23 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity _listener = new GlobalLayoutListener(_view); _view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); - + + // TODO: we probably don't need to create AvaloniaView, if it's just a protocol activation, and main activity is already created. if (Intent?.Data is {} androidUri && androidUri.IsAbsolute - && Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var protocolUri)) + && Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var uri)) { - _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, protocolUri)); + if (uri.Scheme == Uri.UriSchemeFile) + { + if (AndroidStorageItem.CreateItem(this, androidUri) is { } item) + { + _onActivated?.Invoke(this, new FileActivatedEventArgs(new [] { item })); + } + } + else + { + _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(uri)); + } } } diff --git a/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs b/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs index 62f347e9bf..a09c0ea88a 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs @@ -1,10 +1,9 @@ -using System; using Android.App; using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Android.Platform; -internal class AndroidActivatableLifetime : IActivatableLifetime +internal class AndroidActivatableLifetime : ActivatableLifetimeBase { private IAvaloniaActivity? _activity; @@ -28,20 +27,10 @@ internal class AndroidActivatableLifetime : IActivatableLifetime } } } - - public event EventHandler? Activated; - public event EventHandler? Deactivated; - public bool TryLeaveBackground() => false; - public bool TryEnterBackground() => (_activity as Activity)?.MoveTaskToBack(true) == true; + public override bool TryEnterBackground() => (_activity as Activity)?.MoveTaskToBack(true) == true; - private void ActivityOnDeactivated(object? sender, ActivatedEventArgs e) - { - Deactivated?.Invoke(this, e); - } + private void ActivityOnDeactivated(object? sender, ActivatedEventArgs e) => OnDeactivated(e); - private void ActivityOnActivated(object? sender, ActivatedEventArgs e) - { - Activated?.Invoke(this, e); - } + private void ActivityOnActivated(object? sender, ActivatedEventArgs e) => OnActivated(e); } diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 0c606ee07f..952717729f 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -8,7 +8,6 @@ using Android.App; using Android.Content; using Android.Provider; using Android.Webkit; -using AndroidX.DocumentFile.Provider; using Avalonia.Logging; using Avalonia.Platform.Storage; using Java.Lang; @@ -38,8 +37,8 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem)); - public virtual string Name => GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName) - ?? Document?.Name + public virtual string Name => GetColumnValue(Activity, Uri, DocumentsContract.Document.ColumnDisplayName) + ?? GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName) ?? Uri.PathSegments?.LastOrDefault()?.Split("/", StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? string.Empty; public Uri Path => new(Uri.ToString()!); @@ -69,7 +68,7 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem public abstract Task GetBasicPropertiesAsync(); - protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null) + protected static string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null) { try { @@ -84,7 +83,7 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(this, "File metadata reader failed: '{Exception}'", ex); + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(null, "File metadata reader failed: '{Exception}'", ex); } return null; @@ -102,18 +101,6 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem return _parent; } - var document = Document; - - if (document == null) - { - return null; - } - - if(document.ParentFile != null) - { - return new AndroidStorageFolder(Activity, document.ParentFile.Uri, true); - } - using var javaFile = new JavaFile(Uri.Path!); // Java file represents files AND directories. Don't be confused. @@ -140,27 +127,25 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem { _activity = null; } - - internal DocumentFile? Document - { - get - { - if (this is AndroidStorageFile) - { - return DocumentFile.FromSingleUri(Activity, Uri); - } - else - { - return DocumentFile.FromTreeUri(Activity, Uri); - } - } - } - + internal AndroidUri? PermissionRoot => _permissionRoot; public abstract Task DeleteAsync(); public abstract Task MoveAsync(IStorageFolder destination); + + public static IStorageItem CreateItem(Activity activity, AndroidUri uri) + { + var mimeType = GetColumnValue(activity, uri, DocumentsContract.Document.ColumnMimeType); + if (mimeType == DocumentsContract.Document.MimeTypeDir) + { + return new AndroidStorageFolder(activity, uri, false); + } + else + { + return new AndroidStorageFile(activity, uri); + } + } } internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder @@ -172,26 +157,24 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder public Task CreateFileAsync(string name) { var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream"; - var newFile = Document?.CreateFile(mimeType, name); - + var newFile = DocumentsContract.CreateDocument(Activity.ContentResolver!, Uri, mimeType, name); if(newFile == null) { return Task.FromResult(null); } - return Task.FromResult(new AndroidStorageFile(Activity, newFile.Uri, this)); + return Task.FromResult(new AndroidStorageFile(Activity, newFile, this)); } public Task CreateFolderAsync(string name) { - var newFolder = Document?.CreateDirectory(name); - + var newFolder = DocumentsContract.CreateDocument(Activity.ContentResolver!, Uri, DocumentsContract.Document.MimeTypeDir, name); if (newFolder == null) { return Task.FromResult(null); } - return Task.FromResult(new AndroidStorageFolder(Activity, newFolder.Uri, false, this, PermissionRoot)); + return Task.FromResult(new AndroidStorageFolder(Activity, newFolder, false, this, PermissionRoot)); } public override async Task DeleteAsync() @@ -220,7 +203,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder } } - DocumentFile.FromTreeUri(Activity, storageFolder.Uri)?.Delete(); + DocumentsContract.DeleteDocument(Activity.ContentResolver!, storageFolder.Uri); } } @@ -484,7 +467,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF try { if (Activity.ContentResolver is { } contentResolver && - storageFolder.Document?.Uri is { } targetParentUri && + storageFolder.Uri is { } targetParentUri && await GetParentAsync() is AndroidStorageFolder parentFolder) { movedUri = DocumentsContract.MoveDocument(contentResolver, Uri, parentFolder.Uri, targetParentUri); @@ -492,8 +475,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF } catch (Exception) { - // There are many reason why DocumentContract will fail to move a file. We fallback to copying. - return await MoveFileByCopy(); + // There are many reason why DocumentContract will fail to move a file. We fallback to copying below. } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ActivatableLifetimeBase.cs b/src/Avalonia.Controls/ApplicationLifetimes/ActivatableLifetimeBase.cs new file mode 100644 index 0000000000..036e68e558 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivatableLifetimeBase.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls.Platform; +using Avalonia.Metadata; +using Avalonia.Platform.Storage; +using Avalonia.Threading; + +namespace Avalonia.Controls.ApplicationLifetimes; + +[PrivateApi] +public abstract class ActivatableLifetimeBase : IActivatableLifetime +{ + public event EventHandler? Activated; + public event EventHandler? Deactivated; + + public virtual bool TryLeaveBackground() => false; + public virtual bool TryEnterBackground() => false; + + protected internal void OnActivated(ActivationKind kind) => OnActivated(new ActivatedEventArgs(kind)); + + protected internal void OnActivated(ActivatedEventArgs eventArgs) => + Dispatcher.UIThread.Send(_ => Activated?.Invoke(this, eventArgs)); + + protected internal void OnDeactivated(ActivationKind kind) => OnDeactivated(new ActivatedEventArgs(kind)); + + protected internal void OnDeactivated(ActivatedEventArgs eventArgs) => + Dispatcher.UIThread.Send(_ => Deactivated?.Invoke(this, eventArgs)); +} diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs index fb2eae1b52..79f3079e30 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Avalonia.Controls.ApplicationLifetimes; @@ -15,7 +16,7 @@ public class ActivatedEventArgs : EventArgs { Kind = kind; } - + /// /// The that this event represents. /// diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs b/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs index 6d72b56921..408387ffd3 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs @@ -3,10 +3,15 @@ namespace Avalonia.Controls.ApplicationLifetimes; public enum ActivationKind { /// - /// When the application is passed a URI to open. + /// When the application is passed a file to open. /// - OpenUri = 20, - + File = 10, + + /// + /// When the application is passed a URI to open, protocol activation. + /// + OpenUri = 20, + /// /// When the application is asked to reopen. /// An example of this is on MacOS when all the windows are closed, diff --git a/src/Avalonia.Controls/ApplicationLifetimes/FileActivatedEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/FileActivatedEventArgs.cs new file mode 100644 index 0000000000..8d7b268b91 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/FileActivatedEventArgs.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Avalonia.Platform.Storage; + +namespace Avalonia.Controls.ApplicationLifetimes; + +public sealed class FileActivatedEventArgs : ActivatedEventArgs +{ + public FileActivatedEventArgs(IReadOnlyList files) : base(ActivationKind.File) + { + Files = files; + } + + public IReadOnlyList Files { get; } +} diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs index e9706f1cf9..6456d76345 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs @@ -1,10 +1,12 @@ using System; +using System.Linq; +using Avalonia.Metadata; namespace Avalonia.Controls.ApplicationLifetimes; -public class ProtocolActivatedEventArgs : ActivatedEventArgs +public sealed class ProtocolActivatedEventArgs : ActivatedEventArgs { - public ProtocolActivatedEventArgs(ActivationKind kind, Uri uri) : base(kind) + public ProtocolActivatedEventArgs(Uri uri) : base(ActivationKind.OpenUri) { Uri = uri; } diff --git a/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs b/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs index 99bbb8b56d..dade84afc4 100644 --- a/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs +++ b/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs @@ -2,7 +2,7 @@ using Avalonia.Metadata; namespace Avalonia.Platform { - [Unstable] + [Unstable("This interface will be removed in 12.0.")] public interface IApplicationPlatformEvents { void RaiseUrlsOpened(string[] urls); diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index 298ef5619a..0a735f8f9e 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Native.Interop; using Avalonia.Platform; +using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; namespace Avalonia.Native { @@ -13,46 +16,85 @@ namespace Avalonia.Native void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) { ((IApplicationPlatformEvents)Application.Current)?.RaiseUrlsOpened(urls.ToStringArray()); + + if (AvaloniaLocator.Current.GetService() is ActivatableLifetimeBase lifetime) + { + var filePaths = urls.ToStringArray(); + var files = new List(filePaths.Length); + foreach (var filePath in filePaths) + { + if (StorageProviderHelpers.TryCreateBclStorageItem(filePath) is { } file) + { + files.Add(file); + } + } + + if (files.Count > 0) + { + lifetime.OnActivated(new FileActivatedEventArgs(files)); + } + } } - + void IAvnApplicationEvents.UrlsOpened(IAvnStringArray urls) { // Raise the urls opened event to be compatible with legacy behavior. ((IApplicationPlatformEvents)Application.Current)?.RaiseUrlsOpened(urls.ToStringArray()); - if (AvaloniaLocator.Current.GetService() is MacOSActivatableLifetime lifetime) + if (AvaloniaLocator.Current.GetService() is ActivatableLifetimeBase lifetime) { + var files = new List(); + var uris = new List(); foreach (var url in urls.ToStringArray()) { if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri)) { - lifetime.RaiseUrl(uri); + if (uri.Scheme == Uri.UriSchemeFile) + { + if (StorageProviderHelpers.TryCreateBclStorageItem(uri.LocalPath) is { } file) + { + files.Add(file); + } + } + else + { + uris.Add(uri); + } } } + + foreach (var uri in uris) + { + lifetime.OnActivated(new ProtocolActivatedEventArgs(uri)); + } + if (files.Count > 0) + { + lifetime.OnActivated(new FileActivatedEventArgs(files)); + } } } void IAvnApplicationEvents.OnReopen() { - if (AvaloniaLocator.Current.GetService() is MacOSActivatableLifetime lifetime) + if (AvaloniaLocator.Current.GetService() is ActivatableLifetimeBase lifetime) { - lifetime.RaiseActivated(ActivationKind.Reopen); + lifetime.OnActivated(ActivationKind.Reopen); } } void IAvnApplicationEvents.OnHide() { - if (AvaloniaLocator.Current.GetService() is MacOSActivatableLifetime lifetime) + if (AvaloniaLocator.Current.GetService() is ActivatableLifetimeBase lifetime) { - lifetime.RaiseDeactivated(ActivationKind.Background); + lifetime.OnActivated(ActivationKind.Background); } } void IAvnApplicationEvents.OnUnhide() { - if (AvaloniaLocator.Current.GetService() is MacOSActivatableLifetime lifetime) + if (AvaloniaLocator.Current.GetService() is ActivatableLifetimeBase lifetime) { - lifetime.RaiseActivated(ActivationKind.Background); + lifetime.OnActivated(ActivationKind.Background); } } diff --git a/src/Avalonia.Native/MacOSActivatableLifetime.cs b/src/Avalonia.Native/MacOSActivatableLifetime.cs index 54dfab6132..c96e43f724 100644 --- a/src/Avalonia.Native/MacOSActivatableLifetime.cs +++ b/src/Avalonia.Native/MacOSActivatableLifetime.cs @@ -6,16 +6,9 @@ namespace Avalonia.Native; #nullable enable -internal class MacOSActivatableLifetime : IActivatableLifetime +internal class MacOSActivatableLifetime : ActivatableLifetimeBase { - /// - public event EventHandler? Activated; - - /// - public event EventHandler? Deactivated; - - /// - public bool TryLeaveBackground() + public override bool TryLeaveBackground() { var nativeApplicationCommands = AvaloniaLocator.Current.GetService(); nativeApplicationCommands?.ShowApp(); @@ -23,27 +16,11 @@ internal class MacOSActivatableLifetime : IActivatableLifetime return true; } - /// - public bool TryEnterBackground() + public override bool TryEnterBackground() { var nativeApplicationCommands = AvaloniaLocator.Current.GetService(); nativeApplicationCommands?.HideApp(); return true; } - - internal void RaiseUrl(Uri uri) - { - Activated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, uri)); - } - - internal void RaiseActivated(ActivationKind kind) - { - Activated?.Invoke(this, new ActivatedEventArgs(kind)); - } - - internal void RaiseDeactivated(ActivationKind kind) - { - Deactivated?.Invoke(this, new ActivatedEventArgs(kind)); - } } diff --git a/src/iOS/Avalonia.iOS/ActivatableLifetime.cs b/src/iOS/Avalonia.iOS/ActivatableLifetime.cs index c3e2ddedde..58a8808002 100644 --- a/src/iOS/Avalonia.iOS/ActivatableLifetime.cs +++ b/src/iOS/Avalonia.iOS/ActivatableLifetime.cs @@ -3,16 +3,11 @@ using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.iOS; -internal class ActivatableLifetime : IActivatableLifetime +internal class ActivatableLifetime : ActivatableLifetimeBase { public ActivatableLifetime(IAvaloniaAppDelegate avaloniaAppDelegate) { - avaloniaAppDelegate.Activated += (_, args) => Activated?.Invoke(this, args); - avaloniaAppDelegate.Deactivated += (_, args) => Deactivated?.Invoke(this, args); + avaloniaAppDelegate.Activated += (_, args) => OnActivated(args); + avaloniaAppDelegate.Deactivated += (_, args) => OnDeactivated(args); } - - public event EventHandler? Activated; - public event EventHandler? Deactivated; - public bool TryLeaveBackground() => false; - public bool TryEnterBackground() => false; } diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index 6ea5d5f762..99e97a5631 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -1,7 +1,6 @@ using System; using Foundation; using Avalonia.Controls.ApplicationLifetimes; - using UIKit; namespace Avalonia.iOS @@ -74,7 +73,16 @@ namespace Avalonia.iOS { if (Uri.TryCreate(url.ToString(), UriKind.Absolute, out var uri)) { - _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, uri)); +#if !TVOS + if (uri.Scheme == Uri.UriSchemeFile) + { + _onActivated?.Invoke(this, new FileActivatedEventArgs(new[] { Storage.IOSStorageItem.CreateItem(url) })); + } + else +#endif + { + _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(uri)); + } return true; } diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index f9a7aaf445..cf124aa101 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -29,6 +29,13 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem ?? string.Empty; } } + + public static IStorageItem CreateItem(NSUrl url, NSUrl? securityScopedAncestorUrl = null) + { + return url.HasDirectoryPath ? + new IOSStorageFolder(url, securityScopedAncestorUrl) : + new IOSStorageFile(url, securityScopedAncestorUrl); + } internal NSUrl Url { get; } // Calling StartAccessingSecurityScopedResource on items retrieved from, or created in a folder @@ -161,7 +168,7 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile public IOSStorageFile(NSUrl url, NSUrl? securityScopedAncestorUrl = null) : base(url, securityScopedAncestorUrl) { } - + public Task OpenReadAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, SecurityScopedAncestorUrl, FileAccess.Read)); @@ -208,9 +215,7 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder else { var items = content - .Select(u => u.HasDirectoryPath ? - (IStorageItem)new IOSStorageFolder(u, SecurityScopedAncestorUrl) : - new IOSStorageFile(u, SecurityScopedAncestorUrl)) + .Select(u => CreateItem(u, SecurityScopedAncestorUrl)) .ToArray(); tcs.TrySetResult(items); } diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index 4e812b5d98..6d9d2b1e37 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -104,14 +104,31 @@ internal class IOSStorageProvider : IStorageProvider public Task TryGetFileFromPathAsync(Uri filePath) { - // TODO: research if it's possible, maybe with additional permissions. - return Task.FromResult(null); + var fileUrl = (NSUrl?)filePath; + var isDirectory = false; + var file = fileUrl is not null + && fileUrl.Path is { } path + && NSFileManager.DefaultManager.FileExists(path, ref isDirectory) + && !isDirectory + && NSFileManager.DefaultManager.IsReadableFile(path) ? + new IOSStorageFile(fileUrl) : + null; + + return Task.FromResult(file); } public Task TryGetFolderFromPathAsync(Uri folderPath) { - // TODO: research if it's possible, maybe with additional permissions. - return Task.FromResult(null); + var folderUrl = (NSUrl?)folderPath; + var isDirectory = false; + var folder = folderUrl is not null + && folderUrl.Path is { } path + && NSFileManager.DefaultManager.FileExists(path, ref isDirectory) + && isDirectory ? + new IOSStorageFolder(folderUrl) : + null; + + return Task.FromResult(folder); } public Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)