A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

430 lines
16 KiB

using System;
using System.Linq;
using System.Collections.Generic;
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;
using System.Runtime.Versioning;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.iOS.Storage;
internal class IOSStorageProvider : IStorageProvider
{
public static ReadOnlySpan<byte> PlatformKey => "ios"u8;
private readonly AvaloniaView _view;
public IOSStorageProvider(AvaloniaView view)
{
_view = view;
}
public bool CanOpen => true;
public bool CanSave => true;
public bool CanPickFolder => true;
public Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
if (!options.AllowMultiple
&& options.SuggestedStartLocation is IOSStorageFolder { WellKnownFolder: WellKnownFolder.Pictures })
{
return OpenImagePickerAsync(options);
}
else
{
return OpenDocumentsPickerAsync(options);
}
}
private async Task<IReadOnlyList<IStorageFile>> OpenImagePickerAsync(FilePickerOpenOptions options)
{
#pragma warning disable CA1422 // Validate platform compatibility - we can't use PHImagePicker here.
using var imagePicker = new UIImagePickerController();
imagePicker.SourceType = UIImagePickerControllerSourceType.PhotoLibrary;
imagePicker.MediaTypes = new string[] { UTTypeLegacy.Image };
imagePicker.AllowsEditing = false;
imagePicker.Title = options.Title;
#pragma warning restore CA1422 // Validate platform compatibility
var tcs = new TaskCompletionSource<NSUrl[]>();
imagePicker.Delegate = new ImageOpenPickerDelegate(urls => tcs.TrySetResult(urls));
var urls = await ShowPicker(imagePicker, tcs);
return urls.Select(u => new IOSStorageFile(u)).ToArray();
}
private async Task<IReadOnlyList<IStorageFile>> OpenDocumentsPickerAsync(FilePickerOpenOptions options)
{
UIDocumentPickerViewController documentPicker;
if (OperatingSystem.IsIOSVersionAtLeast(14))
{
var allowedTypes = FileTypesToUTType(options.FileTypeFilter);
documentPicker = new UIDocumentPickerViewController(allowedTypes!, false);
}
else
{
var allowedTypes = FileTypesToUTTypeLegacy(options.FileTypeFilter);
documentPicker = new UIDocumentPickerViewController(allowedTypes, UIDocumentPickerMode.Open);
}
using (documentPicker)
{
if (OperatingSystem.IsIOSVersionAtLeast(13))
{
documentPicker.DirectoryUrl = GetUrlFromFolder(options.SuggestedStartLocation);
}
if (OperatingSystem.IsIOSVersionAtLeast(11, 0))
{
documentPicker.AllowsMultipleSelection = options.AllowMultiple;
}
documentPicker.Title = options.Title;
var urls = await ShowDocumentPicker(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?> TryGetFileFromPathAsync(Uri filePath)
{
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<IStorageFile?>(file);
}
public Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{
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<IStorageFolder?>(folder);
}
public Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
var directoryType = wellKnownFolder switch
{
WellKnownFolder.Desktop => NSSearchPathDirectory.DesktopDirectory,
WellKnownFolder.Documents => NSSearchPathDirectory.DocumentDirectory,
WellKnownFolder.Downloads => NSSearchPathDirectory.DownloadsDirectory,
WellKnownFolder.Music => NSSearchPathDirectory.MusicDirectory,
WellKnownFolder.Pictures => NSSearchPathDirectory.PicturesDirectory,
WellKnownFolder.Videos => NSSearchPathDirectory.MoviesDirectory,
_ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null)
};
var uri = NSFileManager.DefaultManager.GetUrl(directoryType, NSSearchPathDomain.User, null, true, out var error);
if (error != null)
{
throw new NSErrorException(error);
}
if (uri is null)
{
return Task.FromResult<IStorageFolder?>(null);
}
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(uri, wellKnownFolder));
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
/*
This requires a bit of dialog here...
To save a file, we need to present the user with a document picker
This requires a temp file to be created and used to "export" the file to.
When the user picks the file location and name, UIDocumentPickerViewController
will give back the URI to the real file location, which we can then use
to give back as an IStorageFile.
https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller
Yes, it is weird, but without the temp file it will explode.
*/
// Create a temporary file to use with the document picker
var tempFileName = StorageProviderHelpers.NameWithExtension(
options.SuggestedFileName ?? "document",
options.DefaultExtension,
options.FileTypeChoices?.FirstOrDefault());
var tempDir = NSFileManager.DefaultManager.GetTemporaryDirectory().Append(Guid.NewGuid().ToString(), true);
if (tempDir == null)
{
throw new InvalidOperationException("Failed to get temporary directory for save file picker");
}
var isDirectoryCreated = NSFileManager.DefaultManager.CreateDirectory(tempDir, true, null, out var error);
if (!isDirectoryCreated)
{
throw new InvalidOperationException("Failed to create temporary directory for save file picker");
}
var tempFileUrl = tempDir.Append(tempFileName, false);
// Create an empty file at the temp location
NSData.FromBytes(0, 0).Save(tempFileUrl, false);
UIDocumentPickerViewController documentPicker;
if (OperatingSystem.IsIOSVersionAtLeast(14))
{
documentPicker = new UIDocumentPickerViewController(new[] { tempFileUrl }, asCopy: true);
}
else
{
#pragma warning disable CA1422
documentPicker = new UIDocumentPickerViewController(tempFileUrl, UIDocumentPickerMode.ExportToService);
#pragma warning restore CA1422
}
using (documentPicker)
{
if (OperatingSystem.IsIOSVersionAtLeast(13))
{
documentPicker.DirectoryUrl = GetUrlFromFolder(options.SuggestedStartLocation);
}
documentPicker.Title = options.Title;
var tcs = new TaskCompletionSource<NSUrl[]>();
documentPicker.Delegate = new PickerDelegate(urls => tcs.TrySetResult(urls));
var urls = await ShowPicker(documentPicker, tcs);
// Clean up the temporary directory
NSFileManager.DefaultManager.Remove(tempDir, out _);
return urls.FirstOrDefault() is { } url ? new IOSStorageFile(url) : null;
}
}
public async Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options)
{
var file = await SaveFilePickerAsync(options).ConfigureAwait(false);
return new SaveFilePickerResult(file);
}
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
using var documentPicker = OperatingSystem.IsIOSVersionAtLeast(14) ?
new UIDocumentPickerViewController(new[] { UTTypes.Folder }, false) :
#pragma warning disable CA1422
new UIDocumentPickerViewController(new string[] { UTTypeLegacy.Folder }, UIDocumentPickerMode.Open);
#pragma warning restore CA1422
if (OperatingSystem.IsIOSVersionAtLeast(13))
{
documentPicker.DirectoryUrl = GetUrlFromFolder(options.SuggestedStartLocation);
}
if (OperatingSystem.IsIOSVersionAtLeast(11))
{
documentPicker.AllowsMultipleSelection = options.AllowMultiple;
}
var urls = await ShowDocumentPicker(documentPicker);
return urls.Select(u => new IOSStorageFolder(u)).ToArray();
}
private static NSUrl? GetUrlFromFolder(IStorageFolder? folder)
{
return folder switch
{
IOSStorageFolder iosFolder => iosFolder.Url,
null => null,
_ => folder.Path
};
}
private Task<NSUrl[]> ShowDocumentPicker(UIDocumentPickerViewController documentPicker)
{
var tcs = new TaskCompletionSource<NSUrl[]>();
documentPicker.Delegate = new PickerDelegate(urls => tcs.TrySetResult(urls));
return ShowPicker(documentPicker, tcs);
}
private Task<NSUrl[]> ShowPicker(UIViewController picker, TaskCompletionSource<NSUrl[]> tcs)
{
if (picker.PresentationController != null)
{
picker.PresentationController.Delegate =
new UIPresentationControllerDelegate(() => tcs.TrySetResult(Array.Empty<NSUrl>()));
}
var controller = _view.Window?.RootViewController ?? throw new InvalidOperationException("RootViewController wasn't initialized");
controller.PresentViewController(picker, true, null);
return tcs.Task;
}
private unsafe NSUrl? GetBookmarkedUrl(string bookmark)
{
return StorageBookmarkHelper.TryDecodeBookmark(PlatformKey, bookmark, out var bytes) switch
{
StorageBookmarkHelper.DecodeResult.Success => DecodeFromBytes(bytes!),
// Attempt to decode 11.0 ios bookmarks
StorageBookmarkHelper.DecodeResult.InvalidFormat => DecodeFromNSData(new NSData(bookmark, NSDataBase64DecodingOptions.None)),
_ => null
};
NSUrl DecodeFromBytes(byte[] bytes)
{
fixed (byte* ptr = bytes)
{
using var data = new NSData(new IntPtr(ptr), new UIntPtr((uint)bytes.Length), null);
return DecodeFromNSData(data);
}
}
NSUrl DecodeFromNSData(NSData nsData)
{
var url = NSUrl.FromBookmarkData(nsData,
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 static string[] FileTypesToUTTypeLegacy(IReadOnlyList<FilePickerFileType>? filePickerFileTypes)
{
return filePickerFileTypes?
.SelectMany(f => f.AppleUniformTypeIdentifiers ?? Array.Empty<string>())
.ToArray() ?? new[]
{
#pragma warning disable CA1422
UTTypeLegacy.Content,
UTTypeLegacy.Item,
#pragma warning restore CA1422
"public.data"
};
}
[SupportedOSPlatform("ios14.0")]
private static UTType[] FileTypesToUTType(IReadOnlyList<FilePickerFileType>? filePickerFileTypes)
{
return filePickerFileTypes?.SelectMany(f =>
{
if (f.TryGetExtensions() is { } extensions && extensions.Any())
{
return extensions.Select(UTType.CreateFromExtension);
}
if (f.AppleUniformTypeIdentifiers?.Any() == true)
{
return f.AppleUniformTypeIdentifiers.Select(UTType.CreateFromIdentifier);
}
if (f.MimeTypes?.Any() == true)
{
return f.MimeTypes.Select(UTType.CreateFromMimeType);
}
return Array.Empty<UTType>();
})
.Where(id => id is not null)
.Cast<UTType>()
.ToArray() ?? new[]
{
UTTypes.Content,
UTTypes.Item,
UTTypes.Data
};
}
private class ImageOpenPickerDelegate : UIImagePickerControllerDelegate
{
private readonly Action<NSUrl[]>? _pickHandler;
internal ImageOpenPickerDelegate(Action<NSUrl[]> pickHandler)
=> _pickHandler = pickHandler;
public override void Canceled(UIImagePickerController picker)
=> _pickHandler?.Invoke(Array.Empty<NSUrl>());
public override void FinishedPickingMedia(UIImagePickerController picker, NSDictionary info)
{
if (info.ValueForKey(new NSString("UIImagePickerControllerImageURL")) is NSUrl nSUrl)
{
_pickHandler?.Invoke(new[] { nSUrl });
}
else
{
_pickHandler?.Invoke(Array.Empty<NSUrl>());
}
}
}
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);
}
}
}