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 3 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; return null;
} }
var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage); await EnsureUriReadPermission(androidUri);
if (!hasPerms)
{
throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it.");
}
var javaFile = new JavaFile(androidUriPath); var javaFile = new JavaFile(androidUriPath);
if (javaFile.Exists() && javaFile.IsFile) if (javaFile.Exists() && javaFile.IsFile)
{ {
@ -88,11 +84,7 @@ internal class AndroidStorageProvider : IStorageProvider
return null; return null;
} }
var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage); await EnsureUriReadPermission(androidUri);
if (!hasPerms)
{
throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it.");
}
var javaFile = new JavaFile(androidUriPath); var javaFile = new JavaFile(androidUriPath);
if (javaFile.Exists() && javaFile.IsDirectory) if (javaFile.Exists() && javaFile.IsDirectory)
@ -283,4 +275,30 @@ internal class AndroidStorageProvider : IStorageProvider
return null; 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() public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
{ {
try try

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

@ -1,9 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using UIKit; using UIKit;
@ -11,6 +9,7 @@ using Foundation;
using UniformTypeIdentifiers; using UniformTypeIdentifiers;
using UTTypeLegacy = MobileCoreServices.UTType; using UTTypeLegacy = MobileCoreServices.UTType;
using UTType = UniformTypeIdentifiers.UTType; using UTType = UniformTypeIdentifiers.UTType;
using System.Runtime.Versioning;
namespace Avalonia.iOS.Storage; namespace Avalonia.iOS.Storage;
@ -28,51 +27,48 @@ internal class IOSStorageProvider : IStorageProvider
public bool CanPickFolder => true; 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; UIDocumentPickerViewController documentPicker;
if (OperatingSystem.IsIOSVersionAtLeast(14)) if (OperatingSystem.IsIOSVersionAtLeast(14))
{ {
var allowedUtils = options.FileTypeFilter?.SelectMany(f => var allowedTypes = FileTypesToUTType(options.FileTypeFilter);
{ documentPicker = new UIDocumentPickerViewController(allowedTypes!, false);
// 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);
} }
else else
{ {
var allowedUtils = options.FileTypeFilter?.SelectMany(f => f.AppleUniformTypeIdentifiers ?? Array.Empty<string>()) var allowedTypes = FileTypesToUTTypeLegacy(options.FileTypeFilter);
.ToArray() ?? new[] documentPicker = new UIDocumentPickerViewController(allowedTypes, UIDocumentPickerMode.Open);
{
#pragma warning disable CA1422
UTTypeLegacy.Content,
UTTypeLegacy.Item,
#pragma warning restore CA1422
"public.data"
};
documentPicker = new UIDocumentPickerViewController(allowedUtils, UIDocumentPickerMode.Open);
} }
using (documentPicker) using (documentPicker)
@ -87,7 +83,9 @@ internal class IOSStorageProvider : IStorageProvider
documentPicker.AllowsMultipleSelection = options.AllowMultiple; 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(); return urls.Select(u => new IOSStorageFile(u)).ToArray();
} }
} }
@ -128,14 +126,19 @@ internal class IOSStorageProvider : IStorageProvider
WellKnownFolder.Videos => NSSearchPathDirectory.MoviesDirectory, WellKnownFolder.Videos => NSSearchPathDirectory.MoviesDirectory,
_ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null) _ => 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) if (error != null)
{ {
throw new NSErrorException(error); 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) public Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
@ -162,7 +165,7 @@ internal class IOSStorageProvider : IStorageProvider
documentPicker.AllowsMultipleSelection = options.AllowMultiple; documentPicker.AllowsMultipleSelection = options.AllowMultiple;
} }
var urls = await ShowPicker(documentPicker); var urls = await ShowDocumentPicker(documentPicker);
return urls.Select(u => new IOSStorageFolder(u)).ToArray(); 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[]>(); var tcs = new TaskCompletionSource<NSUrl[]>();
documentPicker.Delegate = new PickerDelegate(urls => tcs.TrySetResult(urls)); 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>())); new UIPresentationControllerDelegate(() => tcs.TrySetResult(Array.Empty<NSUrl>()));
} }
var controller = _view.Window?.RootViewController ?? throw new InvalidOperationException("RootViewController wasn't initialized"); 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; return tcs.Task;
} }
@ -208,7 +215,73 @@ internal class IOSStorageProvider : IStorageProvider
} }
return url; 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 class PickerDelegate : UIDocumentPickerDelegate
{ {
private readonly Action<NSUrl[]>? _pickHandler; private readonly Action<NSUrl[]>? _pickHandler;

Loading…
Cancel
Save