From 8e2013fd9c62f9f43c79b85f2230456591e6c2e3 Mon Sep 17 00:00:00 2001 From: Dima Dimov Date: Wed, 28 Apr 2021 00:19:12 +0300 Subject: [PATCH] Refactoring MediaPicker on iOS --- .../{IMediaFile.cs => IMediaFile.shared.cs} | 11 +- MediaGallery/MediaFile/MediaFile.android.cs | 14 +- MediaGallery/MediaFile/MediaFile.ios.cs | 71 ++++++- .../MediaFile/MediaFile.netstandard.cs | 14 ++ MediaGallery/MediaFile/MediaFile.shared.cs | 31 ++- MediaGallery/MediaGallery.csproj | 11 +- MediaGallery/MediaGallery/MediaGallery.ios.cs | 8 +- .../MediaGallery/MediaGallery.netstandard.cs | 2 +- MediaGallery/MediaGallery/PickMedia.ios.cs | 180 ++++++------------ README.md | 9 +- Sample/Sample/Sample.csproj | 8 - Sample/Sample/ViewModels/MediaFileInfoVM.cs | 8 +- Sample/Sample/ViewModels/PickVM.cs | 4 + Sample/Sample/Views/MediaFileInfoPage.xaml | 8 +- Sample/Sample/Views/PickPage.xaml | 14 +- 15 files changed, 208 insertions(+), 185 deletions(-) rename MediaGallery/MediaFile/{IMediaFile.cs => IMediaFile.shared.cs} (75%) create mode 100644 MediaGallery/MediaFile/MediaFile.netstandard.cs diff --git a/MediaGallery/MediaFile/IMediaFile.cs b/MediaGallery/MediaFile/IMediaFile.shared.cs similarity index 75% rename from MediaGallery/MediaFile/IMediaFile.cs rename to MediaGallery/MediaFile/IMediaFile.shared.cs index 3035ab1..07510c2 100644 --- a/MediaGallery/MediaFile/IMediaFile.cs +++ b/MediaGallery/MediaFile/IMediaFile.shared.cs @@ -1,16 +1,19 @@ -using System.IO; +using System; +using System.IO; using System.Threading.Tasks; namespace Xamarin.MediaGallery { - public interface IMediaFile + public interface IMediaFile : IDisposable { - string FileName { get; } string FileNameWithoutExtension { get; } + string Extension { get; } + string ContentType { get; } + MediaFileType? Type { get; } Task OpenReadAsync(); } -} \ No newline at end of file +} diff --git a/MediaGallery/MediaFile/MediaFile.android.cs b/MediaGallery/MediaFile/MediaFile.android.cs index ec63b91..9af109c 100644 --- a/MediaGallery/MediaFile/MediaFile.android.cs +++ b/MediaGallery/MediaFile/MediaFile.android.cs @@ -5,15 +5,21 @@ namespace Xamarin.MediaGallery { - public partial class MediaFile + internal partial class MediaFile { + readonly Uri uri; + internal MediaFile(string fileName, Uri uri) { FileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); - Extension = Path.GetExtension(fileName)?.TrimStart('.'); + Extension = Path.GetExtension(fileName); ContentType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(Extension); - openReadAsync = - () => Task.FromResult(Platform.AppActivity.ContentResolver.OpenInputStream(uri)); + this.uri = uri; } + + Task PlatformOpenReadAsync() + => Task.FromResult(Platform.AppActivity.ContentResolver.OpenInputStream(uri)); + + void PlatformDispose() { } } } diff --git a/MediaGallery/MediaFile/MediaFile.ios.cs b/MediaGallery/MediaFile/MediaFile.ios.cs index 928959d..9e9d643 100644 --- a/MediaGallery/MediaFile/MediaFile.ios.cs +++ b/MediaGallery/MediaFile/MediaFile.ios.cs @@ -1,20 +1,75 @@ -using System; -using System.IO; +using System.IO; using System.Linq; using System.Threading.Tasks; +using Foundation; using MobileCoreServices; +using UIKit; namespace Xamarin.MediaGallery { - public partial class MediaFile + internal partial class MediaFile { - internal static MediaFile Create(string fileName, Func> openReadAsync, string typeId, string extension = null) + protected virtual Task PlatformOpenReadAsync() + => Task.FromResult(null); + + protected virtual void PlatformDispose() { } + + protected string GetExtension(string identifier) + => UTType.CopyAllTags(identifier, UTType.TagClassFilenameExtension)?.FirstOrDefault(); + + protected string GetMIMEType(string identifier) + => UTType.CopyAllTags(identifier, UTType.TagClassMIMEType)?.FirstOrDefault(); + } + + internal class PHPickerFile : MediaFile + { + readonly string identifier; + NSItemProvider provider; + + internal PHPickerFile(NSItemProvider provider) + { + this.provider = provider; + FileNameWithoutExtension = provider?.SuggestedName; + identifier = provider?.RegisteredTypeIdentifiers?.FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(identifier)) + return; + + Extension = GetExtension(identifier); + ContentType = GetMIMEType(identifier); + } + + protected override async Task PlatformOpenReadAsync() + => (await provider?.LoadDataRepresentationAsync(identifier))?.AsStream(); + + protected override void PlatformDispose() { - typeId ??= UTType.CreatePreferredIdentifier(UTType.TagClassFilenameExtension, extension, null); - extension ??= UTType.CopyAllTags(typeId, UTType.TagClassFilenameExtension)?.FirstOrDefault(); - var mimeType = UTType.CopyAllTags(typeId, UTType.TagClassMIMEType).FirstOrDefault(); + provider?.Dispose(); + provider = null; + base.PlatformDispose(); + } + } + + internal class UIDocumentFile : MediaFile + { + UIDocument document; - return new MediaFile(fileName, extension, mimeType, openReadAsync); + internal UIDocumentFile(NSUrl assetUrl) + { + document = new UIDocument(assetUrl); + Extension = document.FileUrl.PathExtension; + ContentType = GetMIMEType(document.FileType); + FileNameWithoutExtension = document.LocalizedName; + } + + protected override Task PlatformOpenReadAsync() + => Task.FromResult(File.OpenRead(document.FileUrl.Path)); + + protected override void PlatformDispose() + { + document?.Dispose(); + document = null; + base.PlatformDispose(); } } } diff --git a/MediaGallery/MediaFile/MediaFile.netstandard.cs b/MediaGallery/MediaFile/MediaFile.netstandard.cs new file mode 100644 index 0000000..f61011f --- /dev/null +++ b/MediaGallery/MediaFile/MediaFile.netstandard.cs @@ -0,0 +1,14 @@ +using System.IO; +using System.Threading.Tasks; + +namespace Xamarin.MediaGallery +{ + internal partial class MediaFile : IMediaFile + { + Task PlatformOpenReadAsync() + => throw MediaGallery.NotSupportedOrImplementedException; + + void PlatformDispose() + => throw MediaGallery.NotSupportedOrImplementedException; + } +} diff --git a/MediaGallery/MediaFile/MediaFile.shared.cs b/MediaGallery/MediaFile/MediaFile.shared.cs index 4949a4b..710d6cc 100644 --- a/MediaGallery/MediaFile/MediaFile.shared.cs +++ b/MediaGallery/MediaFile/MediaFile.shared.cs @@ -1,28 +1,22 @@ -using System; -using System.IO; +using System.IO; using System.Threading.Tasks; namespace Xamarin.MediaGallery { - public partial class MediaFile : IMediaFile + internal partial class MediaFile : IMediaFile { - readonly Func> openReadAsync; + private string extension; - internal MediaFile(string fileName, string extension, string contentType, Func> openReadAsync) + public string FileNameWithoutExtension { get; protected internal set; } + + public string Extension { - FileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); - Extension = extension.TrimStart('.'); - ContentType = contentType.ToLower(); - this.openReadAsync = openReadAsync; + get => extension; + protected internal set + => extension = value?.TrimStart('.')?.ToLower(); } - public string FileName => $"{FileNameWithoutExtension}.{Extension}"; - - public string FileNameWithoutExtension { get; } - - public string Extension { get; } - - public string ContentType { get; } + public string ContentType { get; protected internal set; } public MediaFileType? Type => ContentType.StartsWith("image") ? MediaFileType.Image @@ -31,6 +25,9 @@ internal MediaFile(string fileName, string extension, string contentType, Func OpenReadAsync() - => openReadAsync?.Invoke(); + => PlatformOpenReadAsync(); + + public void Dispose() + => PlatformDispose(); } } diff --git a/MediaGallery/MediaGallery.csproj b/MediaGallery/MediaGallery.csproj index 6c51d07..2819955 100644 --- a/MediaGallery/MediaGallery.csproj +++ b/MediaGallery/MediaGallery.csproj @@ -13,7 +13,7 @@ 1.0.0.0 1.0.0.0 1.0.0.0 - 1.0.0.0-alpha001 + 1.0.0.0-alpha003 dimonovdd dimonovdd https://github.com/dimonovdd/Xamarin.MediaGallery @@ -41,12 +41,6 @@ - - - - - - @@ -60,7 +54,4 @@ - - - diff --git a/MediaGallery/MediaGallery/MediaGallery.ios.cs b/MediaGallery/MediaGallery/MediaGallery.ios.cs index f6c729c..0f185e0 100644 --- a/MediaGallery/MediaGallery/MediaGallery.ios.cs +++ b/MediaGallery/MediaGallery/MediaGallery.ios.cs @@ -1,4 +1,6 @@ -using UIKit; +using System; +using UIKit; +using Xamarin.Essentials; namespace Xamarin.MediaGallery { @@ -6,5 +8,9 @@ public static partial class MediaGallery { internal static bool HasOSVersion(int major) => UIDevice.CurrentDevice.CheckSystemVersion(major, 0); + + static UIViewController GetCurrentUIViewController() + => Platform.GetCurrentUIViewController() + ?? throw new InvalidOperationException("Could not find current view controller."); } } \ No newline at end of file diff --git a/MediaGallery/MediaGallery/MediaGallery.netstandard.cs b/MediaGallery/MediaGallery/MediaGallery.netstandard.cs index 51cdcfe..5dfb54a 100644 --- a/MediaGallery/MediaGallery/MediaGallery.netstandard.cs +++ b/MediaGallery/MediaGallery/MediaGallery.netstandard.cs @@ -19,7 +19,7 @@ static Task PlatformSaveAsync(MediaFileType type, string filePath) static Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName) => throw NotSupportedOrImplementedException; - static Exception NotSupportedOrImplementedException + internal static Exception NotSupportedOrImplementedException => new NotImplementedException("This functionality is not implemented in the portable version of this assembly. " + "You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); } diff --git a/MediaGallery/MediaGallery/PickMedia.ios.cs b/MediaGallery/MediaGallery/PickMedia.ios.cs index fc6f30c..65e1cde 100644 --- a/MediaGallery/MediaGallery/PickMedia.ios.cs +++ b/MediaGallery/MediaGallery/PickMedia.ios.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Foundation; using MobileCoreServices; -using Photos; using PhotosUI; using UIKit; using Xamarin.Essentials; @@ -35,14 +34,10 @@ static async Task> PlatformPickAsync(int selectionLimit, ? PHPickerFilter.VideosFilter : PHPickerFilter.ImagesFilter; - var picker = new PHPickerViewController(config); - picker.Delegate = new PPD + pickerRef = new PHPickerViewController(config) { - CompletedHandler = res => - tcs.TrySetResult(PickerResultsToMediaFile(res)) + Delegate = new PHPickerDelegate(tcs) }; - - pickerRef = picker; } else { @@ -57,26 +52,21 @@ static async Task> PlatformPickAsync(int selectionLimit, if (!(isVideo || isImage)) throw new FeatureNotSupportedException(); - var picker = new UIImagePickerController(); - picker.SourceType = sourceType; - picker.MediaTypes = isVideo && isImage - ? new string[] { UTType.Movie, UTType.Image } - : new string[] { isVideo ? UTType.Movie : UTType.Image }; - picker.AllowsEditing = false; - picker.AllowsImageEditing = false; - - picker.Delegate = new PhotoPickerDelegate + pickerRef = new UIImagePickerController { - CompletedHandler = res => - { - var result = DictionaryToMediaFile(res); - tcs.TrySetResult(result == null ? null : new IMediaFile[] { result }); - } + SourceType = sourceType, + AllowsEditing = false, + AllowsImageEditing = false, + Delegate = new PhotoPickerDelegate(tcs), + MediaTypes = isVideo && isImage + ? new string[] { UTType.Movie, UTType.Image } + : new string[] { isVideo ? UTType.Movie : UTType.Image } }; - - pickerRef = picker; } + if (pickerRef.PresentationController != null) + pickerRef.PresentationController.Delegate = new PresentatControllerDelegate(tcs); + if (DeviceInfo.Idiom == DeviceIdiom.Tablet && pickerRef.PopoverPresentationController != null && vc.View != null) pickerRef.PopoverPresentationController.SourceRect = vc.View.Bounds; @@ -84,133 +74,79 @@ static async Task> PlatformPickAsync(int selectionLimit, var result = await tcs.Task; - await vc.DismissViewControllerAsync(true); - pickerRef?.Dispose(); pickerRef = null; return result; } - static IMediaFile DictionaryToMediaFile(NSDictionary info) + static IMediaFile ConvertPickerResults(NSDictionary info) { if (info == null) return null; - PHAsset phAsset = null; - NSUrl assetUrl = null; + using var assetUrl = (info.ValueForKey(UIImagePickerController.ImageUrl) + ?? info.ValueForKey(UIImagePickerController.MediaURL)) as NSUrl; - if (HasOSVersion(11)) - { - assetUrl = info[UIImagePickerController.ImageUrl] as NSUrl; - - // Try the MediaURL sometimes used for videos - if (assetUrl == null) - assetUrl = info[UIImagePickerController.MediaURL] as NSUrl; - - if (assetUrl != null) - { - if (!assetUrl.Scheme.Equals("assets-library", StringComparison.InvariantCultureIgnoreCase)) - { - var doc = new UIDocument(assetUrl); - var fullPath = doc.FileUrl?.Path; - - return MediaFile.Create( - doc.LocalizedName ?? Path.GetFileNameWithoutExtension(fullPath), - () => Task.FromResult(File.OpenRead(fullPath)), - null, - assetUrl.PathExtension); - } - - phAsset = info.ValueForKey(UIImagePickerController.PHAsset) as PHAsset; - } - } + var path = assetUrl?.Path; - if (phAsset == null) - { - assetUrl = info[UIImagePickerController.ReferenceUrl] as NSUrl; - - if (assetUrl != null) - phAsset = PHAsset.FetchAssets(new NSUrl[] { assetUrl }, null)?.LastObject as PHAsset; - } - - if (phAsset == null || assetUrl == null) - { - var img = info.ValueForKey(UIImagePickerController.OriginalImage) as UIImage; - - if (img != null) - return MediaFile.Create( - Guid.NewGuid().ToString(), - () => Task.FromResult(img.AsJPEG().AsStream()), - UTType.JPEG); - } - - if (phAsset == null || assetUrl == null) + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return null; - string originalFilename; - - if (HasOSVersion(9)) - originalFilename = PHAssetResource.GetAssetResources(phAsset).FirstOrDefault()?.OriginalFilename; - else - originalFilename = phAsset.ValueForKey(new NSString("filename")) as NSString; - - return MediaFile.Create( - originalFilename, - () => - { - var tcsStream = new TaskCompletionSource(); - - PHImageManager.DefaultManager.RequestImageData(phAsset, null, new PHImageDataHandler((data, str, orientation, dict) => - tcsStream.TrySetResult(data.AsStream()))); - - return tcsStream.Task; - }, - null, - assetUrl.PathExtension); + return new UIDocumentFile(assetUrl); } - static IEnumerable PickerResultsToMediaFile(PHPickerResult[] results) + static IEnumerable ConvertPickerResults(PHPickerResult[] results) { - if (results != null) - foreach (var res in results) - { - var item = res.ItemProvider; - var identifier = item.RegisteredTypeIdentifiers?.FirstOrDefault(); - - yield return MediaFile.Create( - item.SuggestedName, - async () => - { - var data = await item.LoadDataRepresentationAsync(identifier); - var stream = data.AsStream(); - return stream; - }, - identifier); - } + foreach (var res in results) + if(res?.ItemProvider != null) + yield return new PHPickerFile(res.ItemProvider); } class PhotoPickerDelegate : UIImagePickerControllerDelegate { - public Action CompletedHandler { get; set; } + TaskCompletionSource> tcs; + + internal PhotoPickerDelegate(TaskCompletionSource> tcs) + => this.tcs = tcs; - public override void FinishedPickingMedia(UIImagePickerController picker, NSDictionary info) => - CompletedHandler?.Invoke(info); + public override void FinishedPickingMedia(UIImagePickerController picker, NSDictionary info) + { + picker.DismissViewController(true, null); + var result = ConvertPickerResults(info); + tcs.TrySetResult(result == null ? null : new IMediaFile[] { result }); + } - public override void Canceled(UIImagePickerController picker) => - CompletedHandler?.Invoke(null); + public override void Canceled(UIImagePickerController picker) + { + picker.DismissViewController(true, null); + tcs?.TrySetResult(null); + } } - class PPD : PHPickerViewControllerDelegate + class PHPickerDelegate : PHPickerViewControllerDelegate { - public Action CompletedHandler { get; set; } + TaskCompletionSource> tcs; + + internal PHPickerDelegate(TaskCompletionSource> tcs) + => this.tcs = tcs; - public override void DidFinishPicking(PHPickerViewController picker, PHPickerResult[] results) => - CompletedHandler?.Invoke(results); + public override void DidFinishPicking(PHPickerViewController picker, PHPickerResult[] results) + { + picker.DismissViewController(true, null); + tcs?.TrySetResult(results == null ? null : ConvertPickerResults(results)); + } } - static UIViewController GetCurrentUIViewController() - => Platform.GetCurrentUIViewController() - ?? throw new InvalidOperationException("Could not find current view controller."); + class PresentatControllerDelegate : UIAdaptivePresentationControllerDelegate + { + TaskCompletionSource> tcs; + + internal PresentatControllerDelegate(TaskCompletionSource> tcs) + => this.tcs = tcs; + + public override void DidDismiss(UIPresentationController presentationController) => + tcs?.TrySetResult(null); + } } } diff --git a/README.md b/README.md index 513676c..76016e6 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ if (results?.Files == null) foreach(var res in results.Files) { - var fileName = file.FileName; + var fileName = file.FileNameWithoutExtension; //Can return an null or empty value var extension = file.Extension; var contentType = file.ContentType; using var stream = await file.OpenReadAsync(); @@ -105,6 +105,13 @@ await MediaGallery.SaveAsync(MediaFileType.Video, filePath); - Multi picking is supported from iOS version 14.0+ On older versions, the plugin will prompt the user to select a single file +# Minimum Available Operating Systems + +| OS | Version | +| --- | --- | +| Android | 5.0| +| iOS | 11.0 | + # Screenshots |______________| iOS | Android |______________| diff --git a/Sample/Sample/Sample.csproj b/Sample/Sample/Sample.csproj index 7a75a97..44a8ac7 100644 --- a/Sample/Sample/Sample.csproj +++ b/Sample/Sample/Sample.csproj @@ -26,14 +26,6 @@ - - PickPage %28copy%29.xaml - Code - - - MediaFileInfoPage.xaml - Code - diff --git a/Sample/Sample/ViewModels/MediaFileInfoVM.cs b/Sample/Sample/ViewModels/MediaFileInfoVM.cs index ef98e1c..8733142 100644 --- a/Sample/Sample/ViewModels/MediaFileInfoVM.cs +++ b/Sample/Sample/ViewModels/MediaFileInfoVM.cs @@ -34,7 +34,13 @@ public async override void OnAppearing() try { using var stream = await File.OpenReadAsync(); - Path = await FilesHelper.SaveToCacheAsync(stream, File.FileName); + + var name = (string.IsNullOrWhiteSpace(File.FileNameWithoutExtension) + ? Guid.NewGuid().ToString() + : File.FileNameWithoutExtension) + + $".{File.Extension}"; + + Path = await FilesHelper.SaveToCacheAsync(stream, name); stream.Position = 0; await ReadMeta(stream); } diff --git a/Sample/Sample/ViewModels/PickVM.cs b/Sample/Sample/ViewModels/PickVM.cs index bcad3af..646311a 100644 --- a/Sample/Sample/ViewModels/PickVM.cs +++ b/Sample/Sample/ViewModels/PickVM.cs @@ -38,6 +38,10 @@ async Task Pick(params MediaFileType[] types) { try { + if (SelectedItems?.Count() > 0) + foreach (var item in SelectedItems) + item.Dispose(); + var result = await MediaGallery.PickAsync(SelectionLimit, types); SelectedItems = result?.Files?.ToArray(); } diff --git a/Sample/Sample/Views/MediaFileInfoPage.xaml b/Sample/Sample/Views/MediaFileInfoPage.xaml index 9a0222a..87be2d0 100644 --- a/Sample/Sample/Views/MediaFileInfoPage.xaml +++ b/Sample/Sample/Views/MediaFileInfoPage.xaml @@ -6,19 +6,19 @@ xmlns:vm="clr-namespace:Sample.ViewModels" x:Class="Sample.Views.MediaFileInfoPage" x:DataType="vm:MediaFileInfoVM" - Title="{Binding File.FileName}"> + Title="{Binding File.FileNameWithoutExtension}"> -