Browse Source

Add iOS implementation

pull/8303/head
Max Katz 4 years ago
parent
commit
3ddd068687
  1. 9
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  2. 66
      src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs
  3. 121
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
  4. 212
      src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs

9
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -6,7 +6,9 @@ using Avalonia.Controls.Platform;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using Avalonia.iOS.Storage;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering; using Avalonia.Rendering;
using CoreAnimation; using CoreAnimation;
using Foundation; using Foundation;
@ -41,9 +43,10 @@ namespace Avalonia.iOS
); );
_topLevelImpl.Surfaces = new[] {new EaglLayerSurface(l)}; _topLevelImpl.Surfaces = new[] {new EaglLayerSurface(l)};
MultipleTouchEnabled = true; MultipleTouchEnabled = true;
AddSubviews(new UIView[] { new UIKit.UIButton(UIButtonType.InfoDark) });
} }
internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
{ {
private readonly AvaloniaView _view; private readonly AvaloniaView _view;
public AvaloniaView View => _view; public AvaloniaView View => _view;
@ -52,6 +55,7 @@ namespace Avalonia.iOS
{ {
_view = view; _view = view;
NativeControlHost = new NativeControlHostImpl(_view); NativeControlHost = new NativeControlHostImpl(_view);
StorageProvider = new IOSStorageProvider(view);
} }
public void Dispose() public void Dispose()
@ -113,7 +117,8 @@ namespace Avalonia.iOS
new AcrylicPlatformCompensationLevels(); new AcrylicPlatformCompensationLevels();
public ITextInputMethodImpl? TextInputMethod => _view; public ITextInputMethodImpl? TextInputMethod => _view;
public INativeControlHostImpl NativeControlHost { get; } public INativeControlHostImpl NativeControlHost { get; }
public IStorageProvider StorageProvider { get; }
} }
[Export("layerClass")] [Export("layerClass")]

66
src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs

@ -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();
}
}
}

121
src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

@ -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)
{
}
}

212
src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs

@ -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…
Cancel
Save