4 changed files with 406 additions and 2 deletions
@ -0,0 +1,66 @@ |
|||
using System.IO; |
|||
|
|||
using Foundation; |
|||
|
|||
using UIKit; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.iOS.Storage; |
|||
|
|||
internal sealed class IOSSecurityScopedStream : Stream |
|||
{ |
|||
private readonly UIDocument _document; |
|||
private readonly FileStream _stream; |
|||
private readonly NSUrl _url; |
|||
|
|||
internal IOSSecurityScopedStream(NSUrl url, FileAccess access) |
|||
{ |
|||
_document = new UIDocument(url); |
|||
var path = _document.FileUrl.Path; |
|||
_url = url; |
|||
_url.StartAccessingSecurityScopedResource(); |
|||
_stream = File.Open(path, FileMode.Open, access); |
|||
} |
|||
|
|||
public override bool CanRead => _stream.CanRead; |
|||
|
|||
public override bool CanSeek => _stream.CanSeek; |
|||
|
|||
public override bool CanWrite => _stream.CanWrite; |
|||
|
|||
public override long Length => _stream.Length; |
|||
|
|||
public override long Position |
|||
{ |
|||
get => _stream.Position; |
|||
set => _stream.Position = value; |
|||
} |
|||
|
|||
public override void Flush() => |
|||
_stream.Flush(); |
|||
|
|||
public override int Read(byte[] buffer, int offset, int count) => |
|||
_stream.Read(buffer, offset, count); |
|||
|
|||
public override long Seek(long offset, SeekOrigin origin) => |
|||
_stream.Seek(offset, origin); |
|||
|
|||
public override void SetLength(long value) => |
|||
_stream.SetLength(value); |
|||
|
|||
public override void Write(byte[] buffer, int offset, int count) => |
|||
_stream.Write(buffer, offset, count); |
|||
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
base.Dispose(disposing); |
|||
|
|||
if (disposing) |
|||
{ |
|||
_stream.Dispose(); |
|||
_document.Dispose(); |
|||
_url.StopAccessingSecurityScopedResource(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Platform.Storage; |
|||
using Foundation; |
|||
|
|||
using UIKit; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.iOS.Storage; |
|||
|
|||
internal abstract class IOSStorageItem : IStorageBookmarkItem |
|||
{ |
|||
private readonly string _filePath; |
|||
|
|||
protected IOSStorageItem(NSUrl url) |
|||
{ |
|||
Url = url ?? throw new ArgumentNullException(nameof(url)); |
|||
|
|||
using (var doc = new UIDocument(url)) |
|||
{ |
|||
_filePath = doc.FileUrl?.Path ?? url.FilePathUrl.Path; |
|||
Name = doc.LocalizedName ?? Path.GetFileName(_filePath) ?? url.FilePathUrl.LastPathComponent; |
|||
} |
|||
} |
|||
|
|||
internal NSUrl Url { get; } |
|||
|
|||
public bool CanBookmark => true; |
|||
|
|||
public string Name { get; } |
|||
|
|||
public Task<StorageItemProperties> GetBasicPropertiesAsync() |
|||
{ |
|||
var attributes = NSFileManager.DefaultManager.GetAttributes(_filePath, out var error); |
|||
if (error is not null) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Error, LogArea.IOSPlatform)?. |
|||
Log(this, "GetBasicPropertiesAsync returned an error: {ErrorCode} {ErrorMessage}", error.Code, error.LocalizedFailureReason); |
|||
} |
|||
return Task.FromResult(new StorageItemProperties(attributes?.Size, (DateTime)attributes?.CreationDate, (DateTime)attributes?.ModificationDate)); |
|||
} |
|||
|
|||
public Task<IStorageFolder?> GetParentAsync() |
|||
{ |
|||
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(Url.RemoveLastPathComponent())); |
|||
} |
|||
|
|||
public Task ReleaseBookmark() |
|||
{ |
|||
// no-op
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task<string?> SaveBookmark() |
|||
{ |
|||
try |
|||
{ |
|||
if (!Url.StartAccessingSecurityScopedResource()) |
|||
{ |
|||
return Task.FromResult<string?>(null); |
|||
} |
|||
|
|||
var newBookmark = Url.CreateBookmarkData(NSUrlBookmarkCreationOptions.SuitableForBookmarkFile, Array.Empty<string>(), null, out var bookmarkError); |
|||
if (bookmarkError is not null) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Error, LogArea.IOSPlatform)?. |
|||
Log(this, "SaveBookmark returned an error: {ErrorCode} {ErrorMessage}", bookmarkError.Code, bookmarkError.LocalizedFailureReason); |
|||
return Task.FromResult<string?>(null); |
|||
} |
|||
|
|||
return Task.FromResult<string?>( |
|||
newBookmark.GetBase64EncodedString(NSDataBase64EncodingOptions.None)); |
|||
} |
|||
finally |
|||
{ |
|||
Url.StopAccessingSecurityScopedResource(); |
|||
} |
|||
} |
|||
|
|||
public bool TryGetUri([NotNullWhen(true)] out Uri uri) |
|||
{ |
|||
uri = Url; |
|||
return uri is not null; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
} |
|||
} |
|||
|
|||
internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile |
|||
{ |
|||
public IOSStorageFile(NSUrl url) : base(url) |
|||
{ |
|||
} |
|||
|
|||
public bool CanOpenRead => true; |
|||
|
|||
public bool CanOpenWrite => true; |
|||
|
|||
public Task<Stream> OpenRead() |
|||
{ |
|||
return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Read)); |
|||
} |
|||
|
|||
public Task<Stream> OpenWrite() |
|||
{ |
|||
return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Write)); |
|||
} |
|||
} |
|||
|
|||
internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder |
|||
{ |
|||
public IOSStorageFolder(NSUrl url) : base(url) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,212 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Platform.Storage; |
|||
using UIKit; |
|||
using Foundation; |
|||
using UniformTypeIdentifiers; |
|||
using UTTypeLegacy = MobileCoreServices.UTType; |
|||
using UTType = UniformTypeIdentifiers.UTType; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.iOS.Storage; |
|||
|
|||
internal class IOSStorageProvider : IStorageProvider |
|||
{ |
|||
private readonly AvaloniaView _view; |
|||
public IOSStorageProvider(AvaloniaView view) |
|||
{ |
|||
_view = view; |
|||
} |
|||
|
|||
public bool CanOpen => true; |
|||
|
|||
public bool CanSave => false; |
|||
|
|||
public bool CanPickFolder => true; |
|||
|
|||
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) |
|||
{ |
|||
UIDocumentPickerViewController documentPicker; |
|||
if (OperatingSystem.IsIOSVersionAtLeast(14)) |
|||
{ |
|||
var allowedUtis = options.FileTypeFilter?.SelectMany(f => |
|||
{ |
|||
// We check for OS version outside of the lambda, it's safe.
|
|||
#pragma warning disable CA1416
|
|||
if (f.AppleUniformTypeIdentifiers?.Any() == true) |
|||
{ |
|||
return f.AppleUniformTypeIdentifiers.Select(id => UTType.CreateFromIdentifier(id)); |
|||
} |
|||
if (f.MimeTypes?.Any() == true) |
|||
{ |
|||
return f.MimeTypes.Select(id => UTType.CreateFromMimeType(id)); |
|||
} |
|||
return Array.Empty<UTType>(); |
|||
#pragma warning restore CA1416
|
|||
}) |
|||
.Where(id => id is not null) |
|||
.ToArray() ?? new[] |
|||
{ |
|||
UTTypes.Content, |
|||
UTTypes.Item, |
|||
UTTypes.Data |
|||
}; |
|||
documentPicker = new UIDocumentPickerViewController(allowedUtis!, false); |
|||
} |
|||
else |
|||
{ |
|||
var allowedUtis = options.FileTypeFilter?.SelectMany(f => f.AppleUniformTypeIdentifiers ?? Array.Empty<string>()) |
|||
.ToArray() ?? new[] |
|||
{ |
|||
UTTypeLegacy.Content, |
|||
UTTypeLegacy.Item, |
|||
"public.data" |
|||
}; |
|||
documentPicker = new UIDocumentPickerViewController(allowedUtis, UIDocumentPickerMode.Open); |
|||
} |
|||
|
|||
using (documentPicker) |
|||
{ |
|||
if (OperatingSystem.IsIOSVersionAtLeast(13)) |
|||
{ |
|||
documentPicker.DirectoryUrl = GetUrlFromFolder(options.SuggestedStartLocation); |
|||
} |
|||
|
|||
if (OperatingSystem.IsIOSVersionAtLeast(11, 0)) |
|||
{ |
|||
documentPicker.AllowsMultipleSelection = options.AllowMultiple; |
|||
} |
|||
|
|||
var urls = await ShowPicker(documentPicker); |
|||
return urls.Select(u => new IOSStorageFile(u)).ToArray(); |
|||
} |
|||
} |
|||
|
|||
public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark) |
|||
{ |
|||
return Task.FromResult<IStorageBookmarkFile?>(GetBookmarkedUrl(bookmark) is { } url |
|||
? new IOSStorageFile(url) : null); |
|||
} |
|||
|
|||
public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) |
|||
{ |
|||
return Task.FromResult<IStorageBookmarkFolder?>(GetBookmarkedUrl(bookmark) is { } url |
|||
? new IOSStorageFolder(url) : null); |
|||
} |
|||
|
|||
public Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) |
|||
{ |
|||
return Task.FromException<IStorageFile?>( |
|||
new PlatformNotSupportedException("Save file picker is not supported by iOS")); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) |
|||
{ |
|||
using var documentPicker = OperatingSystem.IsIOSVersionAtLeast(14) ? |
|||
new UIDocumentPickerViewController(new[] { UTTypes.Folder }, false) : |
|||
new UIDocumentPickerViewController(new string[] { UTTypeLegacy.Folder }, UIDocumentPickerMode.Open); |
|||
|
|||
if (OperatingSystem.IsIOSVersionAtLeast(13)) |
|||
{ |
|||
documentPicker.DirectoryUrl = GetUrlFromFolder(options.SuggestedStartLocation); |
|||
} |
|||
|
|||
if (OperatingSystem.IsIOSVersionAtLeast(11)) |
|||
{ |
|||
documentPicker.AllowsMultipleSelection = options.AllowMultiple; |
|||
} |
|||
|
|||
var urls = await ShowPicker(documentPicker); |
|||
return urls.Select(u => new IOSStorageFolder(u)).ToArray(); |
|||
} |
|||
|
|||
private static NSUrl? GetUrlFromFolder(IStorageFolder? folder) |
|||
{ |
|||
if (folder is IOSStorageFolder iosFolder) |
|||
{ |
|||
return iosFolder.Url; |
|||
} |
|||
|
|||
if (folder?.TryGetUri(out var fullPath) == true) |
|||
{ |
|||
return fullPath; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private Task<NSUrl[]> ShowPicker(UIDocumentPickerViewController documentPicker) |
|||
{ |
|||
var tcs = new TaskCompletionSource<NSUrl[]>(); |
|||
documentPicker.Delegate = new PickerDelegate(urls => tcs.TrySetResult(urls)); |
|||
|
|||
if (documentPicker.PresentationController != null) |
|||
{ |
|||
documentPicker.PresentationController.Delegate = |
|||
new UIPresentationControllerDelegate(() => tcs.TrySetResult(Array.Empty<NSUrl>())); |
|||
} |
|||
|
|||
var controller = _view.Window?.RootViewController ?? throw new InvalidOperationException("RootViewController wasn't initialized"); |
|||
controller.PresentViewController(documentPicker, true, null); |
|||
|
|||
return tcs.Task; |
|||
} |
|||
|
|||
private NSUrl? GetBookmarkedUrl(string bookmark) |
|||
{ |
|||
var url = NSUrl.FromBookmarkData(new NSData(bookmark, NSDataBase64DecodingOptions.None), |
|||
NSUrlBookmarkResolutionOptions.WithoutUI, null, out var isStale, out var error); |
|||
if (isStale) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Warning, LogArea.IOSPlatform)?.Log(this, "Stale bookmark detected"); |
|||
} |
|||
|
|||
if (error != null) |
|||
{ |
|||
throw new NSErrorException(error); |
|||
} |
|||
return url; |
|||
} |
|||
|
|||
private class PickerDelegate : UIDocumentPickerDelegate |
|||
{ |
|||
private readonly Action<NSUrl[]>? _pickHandler; |
|||
|
|||
internal PickerDelegate(Action<NSUrl[]> pickHandler) |
|||
=> _pickHandler = pickHandler; |
|||
|
|||
public override void WasCancelled(UIDocumentPickerViewController controller) |
|||
=> _pickHandler?.Invoke(Array.Empty<NSUrl>()); |
|||
|
|||
public override void DidPickDocument(UIDocumentPickerViewController controller, NSUrl[] urls) |
|||
=> _pickHandler?.Invoke(urls); |
|||
|
|||
public override void DidPickDocument(UIDocumentPickerViewController controller, NSUrl url) |
|||
=> _pickHandler?.Invoke(new[] { url }); |
|||
} |
|||
|
|||
private class UIPresentationControllerDelegate : UIAdaptivePresentationControllerDelegate |
|||
{ |
|||
private Action? _dismissHandler; |
|||
|
|||
internal UIPresentationControllerDelegate(Action dismissHandler) |
|||
=> this._dismissHandler = dismissHandler; |
|||
|
|||
public override void DidDismiss(UIPresentationController presentationController) |
|||
{ |
|||
_dismissHandler?.Invoke(); |
|||
_dismissHandler = null; |
|||
} |
|||
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
_dismissHandler?.Invoke(); |
|||
base.Dispose(disposing); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue