Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 195 additions & 63 deletions src/Essentials/src/MediaPicker/MediaPicker.ios.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Foundation;
Expand All @@ -7,14 +8,14 @@
using Microsoft.Maui.Storage;
using MobileCoreServices;
using Photos;
using PhotosUI;
using UIKit;

namespace Microsoft.Maui.Media
{
partial class MediaPickerImplementation : IMediaPicker
{
static UIImagePickerController picker;

static UIViewController pickerRef;
public bool IsCaptureSupported
=> UIImagePickerController.IsSourceTypeAvailable(UIImagePickerControllerSourceType.Camera);

Expand All @@ -24,7 +25,9 @@ public Task<FileResult> PickPhotoAsync(MediaPickerOptions options)
public Task<FileResult> CapturePhotoAsync(MediaPickerOptions options)
{
if (!IsCaptureSupported)
{
throw new FeatureNotSupportedException();
}

return PhotoAsync(options, true, false);
}
Expand All @@ -35,78 +38,140 @@ public Task<FileResult> PickVideoAsync(MediaPickerOptions options)
public Task<FileResult> CaptureVideoAsync(MediaPickerOptions options)
{
if (!IsCaptureSupported)
{
throw new FeatureNotSupportedException();
}

return PhotoAsync(options, false, false);
}

public async Task<FileResult> PhotoAsync(MediaPickerOptions options, bool photo, bool pickExisting)
{
#pragma warning disable CA1416 // TODO: UIImagePickerControllerSourceType.PhotoLibrary, UTType.Image, UTType.Movie is supported on ios version 14 and above
#pragma warning disable CA1422 // Validate platform compatibility
var sourceType = pickExisting ? UIImagePickerControllerSourceType.PhotoLibrary : UIImagePickerControllerSourceType.Camera;
var mediaType = photo ? UTType.Image : UTType.Movie;
#pragma warning restore CA1422 // Validate platform compatibility
#pragma warning restore CA1416

if (!UIImagePickerController.IsSourceTypeAvailable(sourceType))
throw new FeatureNotSupportedException();
if (!UIImagePickerController.AvailableMediaTypes(sourceType).Contains(mediaType))
throw new FeatureNotSupportedException();

if (!photo && !pickExisting)
{
await Permissions.EnsureGrantedAsync<Permissions.Microphone>();
}

// Check if picking existing or not and ensure permission accordingly as they can be set independently from each other
if (pickExisting && !OperatingSystem.IsIOSVersionAtLeast(11, 0))
#pragma warning disable CA1416 // TODO: Permissions.Photos is supported on ios version 14 and above
{
await Permissions.EnsureGrantedAsync<Permissions.Photos>();
#pragma warning restore CA1416
}

if (!pickExisting)
{
await Permissions.EnsureGrantedAsync<Permissions.Camera>();
}

var vc = WindowStateManager.Default.GetCurrentUIViewController(true);
var tcs = new TaskCompletionSource<FileResult>();

picker = new UIImagePickerController();
picker.SourceType = sourceType;
picker.MediaTypes = new string[] { mediaType };
picker.AllowsEditing = false;
if (!photo && !pickExisting)
picker.CameraCaptureMode = UIImagePickerControllerCameraCaptureMode.Video;
if (pickExisting && OperatingSystem.IsIOSVersionAtLeast(14, 0))
{
var config = new PHPickerConfiguration
{
Filter = photo
? PHPickerFilter.ImagesFilter
: PHPickerFilter.VideosFilter
};

if (!string.IsNullOrWhiteSpace(options?.Title))
picker.Title = options.Title;
var picker = new PHPickerViewController(config)
{
Delegate = new Media.PhotoPickerDelegate
{
CompletedHandler = res =>
tcs.TrySetResult(PickerResultsToMediaFile(res))
}
};

pickerRef = picker;
}
else
{
if (!pickExisting)
{
await Permissions.EnsureGrantedAsync<Permissions.PhotosAddOnly>();
}

if (DeviceInfo.Current.Idiom == DeviceIdiom.Tablet && picker.PopoverPresentationController != null && vc.View != null)
picker.PopoverPresentationController.SourceRect = vc.View.Bounds;
var sourceType = pickExisting
? UIImagePickerControllerSourceType.PhotoLibrary
: UIImagePickerControllerSourceType.Camera;

var tcs = new TaskCompletionSource<FileResult>(picker);
picker.Delegate = new PhotoPickerDelegate
{
CompletedHandler = async info =>
var mediaType = photo ? UTType.Image : UTType.Movie;

if (!UIImagePickerController.IsSourceTypeAvailable(sourceType))
{
tcs.TrySetCanceled();
throw new FeatureNotSupportedException();
}

if (!UIImagePickerController.AvailableMediaTypes(sourceType).Contains(mediaType))
{
tcs.TrySetCanceled();
throw new FeatureNotSupportedException();
}

var picker = new UIImagePickerController
{
SourceType = sourceType,
MediaTypes = [mediaType],
AllowsEditing = false
};

if (!photo && !pickExisting)
{
await vc.DismissViewControllerAsync(true);
GetFileResult(info, tcs);
picker.CameraCaptureMode = UIImagePickerControllerCameraCaptureMode.Video;
}
};

if (picker.PresentationController != null)
pickerRef = picker;

picker.Delegate = new PhotoPickerDelegate
{
CompletedHandler = async info =>
{
GetFileResult(info, tcs);
await vc.DismissViewControllerAsync(true);
}
};
}

if (!string.IsNullOrWhiteSpace(options?.Title))
{
picker.PresentationController.Delegate =
new UIPresentationControllerDelegate(() => GetFileResult(null, tcs));
pickerRef.Title = options.Title;
}

await vc.PresentViewControllerAsync(picker, true);
if (DeviceInfo.Idiom == DeviceIdiom.Tablet)
{
pickerRef.ModalPresentationStyle = UIModalPresentationStyle.PageSheet;
}

if (pickerRef.PresentationController is not null)
{
pickerRef.PresentationController.Delegate = new PhotoPickerPresentationControllerDelegate
{
Handler = () => tcs.TrySetResult(null)
};
}

await vc.PresentViewControllerAsync(pickerRef, true);

var result = await tcs.Task;

picker?.Dispose();
picker = null;
pickerRef?.Dispose();
pickerRef = null;

return result;
}

static FileResult PickerResultsToMediaFile(PHPickerResult[] results)
{
var file = results?.FirstOrDefault();

return file == null
? null
: new PHPickerFileResult(file.ItemProvider);
}

static void GetFileResult(NSDictionary info, TaskCompletionSource<FileResult> tcs)
{
try
Expand All @@ -121,8 +186,16 @@ static void GetFileResult(NSDictionary info, TaskCompletionSource<FileResult> tc

static FileResult DictionaryToMediaFile(NSDictionary info)
{
if (info == null)
// This method should only be called for iOS < 14
if (!OperatingSystem.IsIOSVersionAtLeast(14))
{
return null;
}

if (info is null)
{
return null;
}

PHAsset phAsset = null;
NSUrl assetUrl = null;
Expand All @@ -132,62 +205,121 @@ static FileResult DictionaryToMediaFile(NSDictionary info)
assetUrl = info[UIImagePickerController.ImageUrl] as NSUrl;

// Try the MediaURL sometimes used for videos
if (assetUrl == null)
assetUrl = info[UIImagePickerController.MediaURL] as NSUrl;
assetUrl ??= info[UIImagePickerController.MediaURL] as NSUrl;

if (assetUrl != null)
if (assetUrl is not null)
{
if (!assetUrl.Scheme.Equals("assets-library", StringComparison.OrdinalIgnoreCase))
{
return new UIDocumentFileResult(assetUrl);
#pragma warning disable CA1416 // TODO: 'UIImagePickerController.PHAsset' is only supported on: 'ios' from version 11.0 to 14.0
#pragma warning disable CA1422 // Validate platform compatibility
}

phAsset = info.ValueForKey(UIImagePickerController.PHAsset) as PHAsset;
#pragma warning restore CA1422 // Validate platform compatibility
#pragma warning restore CA1416
}
}

#if !MACCATALYST
#pragma warning disable CA1416 // TODO: 'UIImagePickerController.ReferenceUrl' is unsupported on 'ios' 11.0 and later
#pragma warning disable CA1422 // Validate platform compatibility
if (phAsset == null)
if (phAsset is null)
{
assetUrl = info[UIImagePickerController.ReferenceUrl] as NSUrl;

if (assetUrl != null)
phAsset = PHAsset.FetchAssets(new NSUrl[] { assetUrl }, null)?.LastObject as PHAsset;
if (assetUrl is not null)
{
phAsset = PHAsset.FetchAssets([assetUrl], null)?.LastObject as PHAsset;
}
}
#pragma warning restore CA1422 // Validate platform compatibility
#pragma warning restore CA1416 // 'PHAsset.FetchAssets(NSUrl[], PHFetchOptions?)' is unsupported on 'ios' 11.0 and later
#endif

if (phAsset == null || assetUrl == null)
if (phAsset is null || assetUrl is null)
{
var img = info.ValueForKey(UIImagePickerController.OriginalImage) as UIImage;

if (img != null)
if (img is not null)
{
return new UIImageFileResult(img);
}
}

if (phAsset == null || assetUrl == null)
if (phAsset is null || assetUrl is null)
{
return null;
#pragma warning disable CA1416 // https://github.com/xamarin/xamarin-macios/issues/14619
#pragma warning disable CA1422 // Validate platform compatibility
}

string originalFilename = PHAssetResource.GetAssetResources(phAsset).FirstOrDefault()?.OriginalFilename;
#pragma warning restore CA1422 // Validate platform compatibility
#pragma warning restore CA1416
return new PHAssetFileResult(assetUrl, phAsset, originalFilename);
}

class PhotoPickerDelegate : UIImagePickerControllerDelegate
{
public Action<NSDictionary> CompletedHandler { get; set; }

public override void FinishedPickingMedia(UIImagePickerController picker, NSDictionary info) =>
public override void FinishedPickingMedia(UIImagePickerController picker, NSDictionary info)
{
picker.DismissViewController(true, null);
CompletedHandler?.Invoke(info);
}

public override void Canceled(UIImagePickerController picker) =>
public override void Canceled(UIImagePickerController picker)
{
picker.DismissViewController(true, null);
CompletedHandler?.Invoke(null);
}
}
}

class PhotoPickerDelegate : PHPickerViewControllerDelegate
{
public Action<PHPickerResult[]> CompletedHandler { get; set; }

public override void DidFinishPicking(PHPickerViewController picker, PHPickerResult[] results)
{
picker.DismissViewController(true, null);
CompletedHandler?.Invoke(results?.Length > 0 ? results : null);
}
}

class PhotoPickerPresentationControllerDelegate : UIAdaptivePresentationControllerDelegate
{
public Action Handler { get; set; }

public override void DidDismiss(UIPresentationController presentationController) =>
Handler?.Invoke();

protected override void Dispose(bool disposing)
{
Handler?.Invoke();
base.Dispose(disposing);
}
}

class PHPickerFileResult : FileResult
{
readonly string _identifier;
readonly NSItemProvider _provider;

internal PHPickerFileResult(NSItemProvider provider)
{
_provider = provider;
var identifiers = provider?.RegisteredTypeIdentifiers;

_identifier = (identifiers?.Any(i => i.StartsWith(UTType.LivePhoto)) ?? false)
&& (identifiers?.Contains(UTType.JPEG) ?? false)
? identifiers?.FirstOrDefault(i => i == UTType.JPEG)
: identifiers?.FirstOrDefault();

if (string.IsNullOrWhiteSpace(_identifier))
{
return;
}

FileName = FullPath
= $"{provider?.SuggestedName}.{GetTag(_identifier, UTType.TagClassFilenameExtension)}";
}

internal override async Task<Stream> PlatformOpenReadAsync()
=> (await _provider?.LoadDataRepresentationAsync(_identifier))?.AsStream();

protected internal static string GetTag(string identifier, string tagClass)
=> UTType.CopyAllTags(identifier, tagClass)?.FirstOrDefault();
}
}
Loading