Browse Source

Some iOS/Android file picker fixes (#14677)

* Use NSSearchPathDomain.User when opening a known folder on iOS

* Use UIImagePickerController for WellKnownFolder.Pictures

* Improve how Android handles read permissions
pull/14744/head
Max Katz 2 years ago
committed by GitHub
parent
commit
b6ee7b90d0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 40
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
  2. 7
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
  3. 175
      src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs

40
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs

@ -55,12 +55,8 @@ internal class AndroidStorageProvider : IStorageProvider
return null;
}
var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
if (!hasPerms)
{
throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it.");
}
await EnsureUriReadPermission(androidUri);
var javaFile = new JavaFile(androidUriPath);
if (javaFile.Exists() && javaFile.IsFile)
{
@ -88,11 +84,7 @@ internal class AndroidStorageProvider : IStorageProvider
return null;
}
var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
if (!hasPerms)
{
throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it.");
}
await EnsureUriReadPermission(androidUri);
var javaFile = new JavaFile(androidUriPath);
if (javaFile.Exists() && javaFile.IsDirectory)
@ -283,4 +275,30 @@ internal class AndroidStorageProvider : IStorageProvider
return null;
}
private async Task EnsureUriReadPermission(AndroidUri androidUri)
{
bool hasPerms = false;
Exception? innerEx = null;
try
{
hasPerms = _activity.CheckUriPermission(androidUri,
global::Android.OS.Process.MyPid(),
global::Android.OS.Process.MyUid(),
ActivityFlags.GrantReadUriPermission)
== global::Android.Content.PM.Permission.Granted;
// TODO: call RequestPermission or add proper permissions API, something like in Browser File API.
hasPerms = hasPerms || await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
}
catch (Exception ex)
{
innerEx = ex;
}
if (!hasPerms)
{
throw new InvalidOperationException("Application doesn't have READ_EXTERNAL_STORAGE permission. Make sure android manifest has this permission defined and user allowed it.", innerEx);
}
}
}

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

@ -179,6 +179,13 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
{
}
public IOSStorageFolder(NSUrl url, WellKnownFolder wellKnownFolder) : base(url, null)
{
WellKnownFolder = wellKnownFolder;
}
public WellKnownFolder? WellKnownFolder { get; }
public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
{
try

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

@ -1,9 +1,7 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Logging;
using Avalonia.Platform.Storage;
using UIKit;
@ -11,6 +9,7 @@ using Foundation;
using UniformTypeIdentifiers;
using UTTypeLegacy = MobileCoreServices.UTType;
using UTType = UniformTypeIdentifiers.UTType;
using System.Runtime.Versioning;
namespace Avalonia.iOS.Storage;
@ -28,51 +27,48 @@ internal class IOSStorageProvider : IStorageProvider
public bool CanPickFolder => true;
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
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 allowedUtils = options.FileTypeFilter?.SelectMany(f =>
{
// We check for OS version outside of the lambda, it's safe.
#pragma warning disable CA1416
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>();
#pragma warning restore CA1416
})
.Where(id => id is not null)
.ToArray() ?? new[]
{
UTTypes.Content,
UTTypes.Item,
UTTypes.Data
};
documentPicker = new UIDocumentPickerViewController(allowedUtils!, false);
var allowedTypes = FileTypesToUTType(options.FileTypeFilter);
documentPicker = new UIDocumentPickerViewController(allowedTypes!, false);
}
else
{
var allowedUtils = options.FileTypeFilter?.SelectMany(f => f.AppleUniformTypeIdentifiers ?? Array.Empty<string>())
.ToArray() ?? new[]
{
#pragma warning disable CA1422
UTTypeLegacy.Content,
UTTypeLegacy.Item,
#pragma warning restore CA1422
"public.data"
};
documentPicker = new UIDocumentPickerViewController(allowedUtils, UIDocumentPickerMode.Open);
var allowedTypes = FileTypesToUTTypeLegacy(options.FileTypeFilter);
documentPicker = new UIDocumentPickerViewController(allowedTypes, UIDocumentPickerMode.Open);
}
using (documentPicker)
@ -87,7 +83,9 @@ internal class IOSStorageProvider : IStorageProvider
documentPicker.AllowsMultipleSelection = options.AllowMultiple;
}
var urls = await ShowPicker(documentPicker);
documentPicker.Title = options.Title;
var urls = await ShowDocumentPicker(documentPicker);
return urls.Select(u => new IOSStorageFile(u)).ToArray();
}
}
@ -128,14 +126,19 @@ internal class IOSStorageProvider : IStorageProvider
WellKnownFolder.Videos => NSSearchPathDirectory.MoviesDirectory,
_ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null)
};
var uri = NSFileManager.DefaultManager.GetUrl(directoryType, NSSearchPathDomain.Local, null, true, out var error);
var uri = NSFileManager.DefaultManager.GetUrl(directoryType, NSSearchPathDomain.User, null, true, out var error);
if (error != null)
{
throw new NSErrorException(error);
}
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(uri));
if (uri is null)
{
return Task.FromResult<IStorageFolder?>(null);
}
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(uri, wellKnownFolder));
}
public Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
@ -162,7 +165,7 @@ internal class IOSStorageProvider : IStorageProvider
documentPicker.AllowsMultipleSelection = options.AllowMultiple;
}
var urls = await ShowPicker(documentPicker);
var urls = await ShowDocumentPicker(documentPicker);
return urls.Select(u => new IOSStorageFolder(u)).ToArray();
}
@ -176,20 +179,24 @@ internal class IOSStorageProvider : IStorageProvider
};
}
private Task<NSUrl[]> ShowPicker(UIDocumentPickerViewController documentPicker)
private Task<NSUrl[]> ShowDocumentPicker(UIDocumentPickerViewController documentPicker)
{
var tcs = new TaskCompletionSource<NSUrl[]>();
documentPicker.Delegate = new PickerDelegate(urls => tcs.TrySetResult(urls));
return ShowPicker(documentPicker, tcs);
}
if (documentPicker.PresentationController != null)
private Task<NSUrl[]> ShowPicker(UIViewController picker, TaskCompletionSource<NSUrl[]> tcs)
{
if (picker.PresentationController != null)
{
documentPicker.PresentationController.Delegate =
picker.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);
controller.PresentViewController(picker, true, null);
return tcs.Task;
}
@ -208,7 +215,73 @@ internal class IOSStorageProvider : IStorageProvider
}
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;

Loading…
Cancel
Save