Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster photo picking on iOS #18886

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
5 changes: 4 additions & 1 deletion src/Uno.UI/Resources/Strings/cs-CZ/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@
<data name="StorageProviderIosSecurityScopedDisplayName" xml:space="preserve">
<value>iOS Security Scoped</value>
</data>
<data name="StorageProviderIosPHPickerDisplayName" xml:space="preserve">
<value>iOS PHPicker</value>
</data>
<data name="StorageProviderLocalDisplayName" xml:space="preserve">
<value>Tento počítač</value>
</data>
Expand All @@ -135,4 +138,4 @@
<data name="StorageProviderWasmNativeDisplayName" xml:space="preserve">
<value>JS File Access API</value>
</data>
</root>
</root>
3 changes: 3 additions & 0 deletions src/Uno.UI/Resources/Strings/en/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@
<data name="StorageProviderIosSecurityScopedDisplayName" xml:space="preserve">
<value>iOS Security Scoped</value>
</data>
<data name="StorageProviderIosPHPickerDisplayName" xml:space="preserve">
<value>iOS PHPicker</value>
</data>
<data name="StorageProviderLocalDisplayName" xml:space="preserve">
<value>This PC</value>
</data>
Expand Down
52 changes: 7 additions & 45 deletions src/Uno.UWP/Devices/Sensors/Helpers/SensorHelpers.iOS.cs
Original file line number Diff line number Diff line change
@@ -1,52 +1,14 @@
using Foundation;
using System;

namespace Uno.Devices.Sensors.Helpers
namespace Uno.Devices.Sensors.Helpers;

internal static class SensorHelpers
{
internal static class SensorHelpers
public static DateTimeOffset TimestampToDateTimeOffset(double timestamp)
{
private static readonly DateTimeOffset NSDateConversionStart =
new DateTimeOffset(2001, 1, 1, 0, 0, 0, TimeSpan.Zero);

public static DateTimeOffset TimestampToDateTimeOffset(double timestamp)
{
var bootTime = NSDate.FromTimeIntervalSinceNow(-NSProcessInfo.ProcessInfo.SystemUptime);
var date = (DateTime)bootTime.AddSeconds(timestamp);
return new DateTimeOffset(date);
}

public static DateTimeOffset NSDateToDateTimeOffset(NSDate nsDate)
{
if (nsDate == NSDate.DistantPast)
{
return DateTimeOffset.MinValue;
}
else if (nsDate == NSDate.DistantFuture)
{
return DateTimeOffset.MaxValue;
}

return NSDateConversionStart.AddSeconds(
nsDate.SecondsSinceReferenceDate);
}

public static NSDate DateTimeOffsetToNSDate(DateTimeOffset dateTimeOffset)
{
if (dateTimeOffset == DateTimeOffset.MinValue)
{
return NSDate.DistantPast;
}
else if (dateTimeOffset == DateTimeOffset.MaxValue)
{
return NSDate.DistantFuture;
}

var dateInSecondsFromStart = dateTimeOffset
.ToUniversalTime()
.Subtract(NSDateConversionStart.UtcDateTime);

return NSDate.FromTimeIntervalSinceReferenceDate(
dateInSecondsFromStart.TotalSeconds);
}
var bootTime = NSDate.FromTimeIntervalSinceNow(-NSProcessInfo.ProcessInfo.SystemUptime);
var date = (DateTime)bootTime.AddSeconds(timestamp);
return new DateTimeOffset(date);
}
}
48 changes: 48 additions & 0 deletions src/Uno.UWP/Extensions/NSDateExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Foundation;

Check failure on line 6 in src/Uno.UWP/Extensions/NSDateExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The type or namespace name 'Foundation' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 6 in src/Uno.UWP/Extensions/NSDateExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The type or namespace name 'Foundation' could not be found (are you missing a using directive or an assembly reference?)

namespace Windows.Extensions;

internal static class NSDateExtensions
{
private static readonly DateTimeOffset NSDateConversionStart =
new DateTimeOffset(2001, 1, 1, 0, 0, 0, TimeSpan.Zero);

public static DateTimeOffset ToDateTimeOffset(this NSDate nsDate)

Check failure on line 15 in src/Uno.UWP/Extensions/NSDateExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The type or namespace name 'NSDate' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 15 in src/Uno.UWP/Extensions/NSDateExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The type or namespace name 'NSDate' could not be found (are you missing a using directive or an assembly reference?)
{
if (nsDate == NSDate.DistantPast)
{
return DateTimeOffset.MinValue;
}
else if (nsDate == NSDate.DistantFuture)
{
return DateTimeOffset.MaxValue;
}

return NSDateConversionStart.AddSeconds(
nsDate.SecondsSinceReferenceDate);
}

public static NSDate ToNSDate(this DateTimeOffset dateTimeOffset)

Check failure on line 30 in src/Uno.UWP/Extensions/NSDateExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The type or namespace name 'NSDate' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 30 in src/Uno.UWP/Extensions/NSDateExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The type or namespace name 'NSDate' could not be found (are you missing a using directive or an assembly reference?)
{
if (dateTimeOffset == DateTimeOffset.MinValue)
{
return NSDate.DistantPast;
}
else if (dateTimeOffset == DateTimeOffset.MaxValue)
{
return NSDate.DistantFuture;
}

var dateInSecondsFromStart = dateTimeOffset
.ToUniversalTime()
.Subtract(NSDateConversionStart.UtcDateTime);

return NSDate.FromTimeIntervalSinceReferenceDate(
dateInSecondsFromStart.TotalSeconds);
}
}
2 changes: 2 additions & 0 deletions src/Uno.UWP/Storage/Internal/StorageProviders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ internal static class StorageProviders

#if __IOS__
public static StorageProvider IosSecurityScoped { get; } = new StorageProvider("iossecurityscoped", "StorageProviderIosSecurityScopedDisplayName");

public static StorageProvider IosPHPicker { get; } = new("iosphpicker", "StorageProviderIosPHPickerDisplayName");
#endif
}
}
17 changes: 0 additions & 17 deletions src/Uno.UWP/Storage/Pickers/FileOpenPicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,4 @@ private void ValidateConfiguration()
}
#endif
}

public static class FileOpenPickerExtensions
{
/// <summary>
/// Sets the file limit a user can select when picking multiple files.
/// </summary>
/// <param name="limit">The maximum number of files that the user can pick.</param>
#if !__IOS__
[global::Uno.NotImplemented]
#endif
public static void SetMultipleFilesLimit(this FileOpenPicker picker, int limit)
{
#if __IOS__
picker.SetMultipleFileLimit(limit);
#endif
}
}
}
109 changes: 53 additions & 56 deletions src/Uno.UWP/Storage/Pickers/FileOpenPicker.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Uno.Storage.Pickers.Internal;
using UIKit;
using Foundation;
using Windows.ApplicationModel.Core;
using Uno.Helpers.Theming;
using MobileCoreServices;
using Photos;
using PhotosUI;
using UIKit;
using Uno.Helpers.Theming;
using Uno.Storage.Pickers.Internal;
using Uno.UI.Dispatching;
using MobileCoreServices;
using Windows.Foundation.Metadata;
using Uno.Foundation.Logging;
using Windows.ApplicationModel.Core;

namespace Windows.Storage.Pickers
{
public partial class FileOpenPicker
{
private int _multipleFileLimit;
private bool _isReadOnly;

private Task<StorageFile?> PickSingleFileTaskAsync(CancellationToken token)
{
Expand Down Expand Up @@ -53,10 +53,9 @@ private Task<IReadOnlyList<StorageFile>> PickMultipleFilesTaskAsync(Cancellation
return tcs.Task;
}

internal void SetMultipleFileLimit(int limit)
{
_multipleFileLimit = limit;
}
internal void SetMultipleFileLimit(int limit) => _multipleFileLimit = limit;

internal void SetReadOnlyMode(bool readOnly) => _isReadOnly = readOnly;

private UIViewController GetViewController(bool multiple, int limit, TaskCompletionSource<StorageFile?[]> completionSource)
{
Expand All @@ -73,24 +72,24 @@ private UIViewController GetViewController(bool multiple, int limit, TaskComplet
};

case PickerLocationId.PicturesLibrary when multiple is true && iOS14AndAbove is true:
var imageConfiguration = new PHPickerConfiguration
var imageConfiguration = new PHPickerConfiguration(PHPhotoLibrary.SharedPhotoLibrary)
{
Filter = PHPickerFilter.ImagesFilter,
SelectionLimit = limit
};
return new PHPickerViewController(imageConfiguration)
{
Delegate = new PhotoPickerDelegate(completionSource)
Delegate = new PhotoPickerDelegate(completionSource, _isReadOnly)
};
case PickerLocationId.VideosLibrary when multiple is true && iOS14AndAbove is true:
var videoConfiguration = new PHPickerConfiguration
var videoConfiguration = new PHPickerConfiguration(PHPhotoLibrary.SharedPhotoLibrary)
{
Filter = PHPickerFilter.VideosFilter,
SelectionLimit = limit
};
return new PHPickerViewController(videoConfiguration)
{
Delegate = new PhotoPickerDelegate(completionSource)
Delegate = new PhotoPickerDelegate(completionSource, _isReadOnly)
};

default:
Expand Down Expand Up @@ -167,9 +166,13 @@ public override void FinishedPickingMedia(UIImagePickerController picker, NSDict
private class PhotoPickerDelegate : PHPickerViewControllerDelegate
{
private readonly TaskCompletionSource<StorageFile?[]> _taskCompletionSource;
private readonly bool _readOnly;

public PhotoPickerDelegate(TaskCompletionSource<StorageFile?[]> taskCompletionSource) =>
public PhotoPickerDelegate(TaskCompletionSource<StorageFile?[]> taskCompletionSource, bool readOnly)
{
_taskCompletionSource = taskCompletionSource;
_readOnly = readOnly;
}

public override async void DidFinishPicking(PHPickerViewController picker, PHPickerResult[] results)
{
Expand All @@ -186,56 +189,50 @@ public override async void DidFinishPicking(PHPickerViewController picker, PHPic
private async Task<IEnumerable<StorageFile>> ConvertPickerResults(PHPickerResult[] results)
{
List<StorageFile> storageFiles = new List<StorageFile>();
var providers = results
.Select(res => res.ItemProvider)
.Where(provider => provider != null && provider.RegisteredTypeIdentifiers?.Length > 0)
.ToArray();

foreach (NSItemProvider provider in providers)
if (_readOnly)
{
var identifier = GetIdentifier(provider.RegisteredTypeIdentifiers ?? []) ?? "public.data";
var data = await provider.LoadDataRepresentationAsync(identifier);

if (data is null)
var assetIdentifiers = results
.Select(res => res.AssetIdentifier!)
.Where(id => id != null)
.ToArray();

var resultsByIdentifier = results.ToDictionary(res => res.AssetIdentifier!);
var assets = PHAsset.FetchAssetsUsingLocalIdentifiers(assetIdentifiers, null);
foreach (PHAsset asset in assets)
{
continue;
var file = StorageFile.GetFromPHPickerResult(resultsByIdentifier[asset.LocalIdentifier], asset, null);
storageFiles.Add(file);
}

var extension = GetExtension(identifier);

var destinationUrl = NSFileManager.DefaultManager
.GetTemporaryDirectory()
.Append($"{NSProcessInfo.ProcessInfo.GloballyUniqueString}.{extension}", false);
data.Save(destinationUrl, false);

storageFiles.Add(StorageFile.GetFromSecurityScopedUrl(destinationUrl, null));
}

return storageFiles;
}
private static string? GetIdentifier(string[] identifiers)
{
if (!(identifiers?.Length > 0))
else
{
return null;
}
var providers = results
.Select(res => res.ItemProvider)
.Where(provider => provider != null && provider.RegisteredTypeIdentifiers?.Length > 0)
.ToArray();

if (identifiers.Any(i => i.StartsWith(UTType.LivePhoto, StringComparison.InvariantCultureIgnoreCase)) && identifiers.Contains(UTType.JPEG))
{
return identifiers.FirstOrDefault(i => i == UTType.JPEG);
}
foreach (NSItemProvider provider in providers)
{
var identifier = StorageFile.GetUTIdentifier(provider.RegisteredTypeIdentifiers ?? []) ?? "public.data";
var data = await provider.LoadDataRepresentationAsync(identifier);

if (identifiers.Contains(UTType.QuickTimeMovie))
{
return identifiers.FirstOrDefault(i => i == UTType.QuickTimeMovie);
}
if (data is null)
{
continue;
}

return identifiers.FirstOrDefault();
}
var extension = StorageFile.GetUTFileExtension(identifier);

private string? GetExtension(string identifier)
=> UTType.CopyAllTags(identifier, UTType.TagClassFilenameExtension)?.FirstOrDefault();
var destinationUrl = NSFileManager.DefaultManager
.GetTemporaryDirectory()
.Append($"{NSProcessInfo.ProcessInfo.GloballyUniqueString}.{extension}", false);
data.Save(destinationUrl, false);

storageFiles.Add(StorageFile.GetFromSecurityScopedUrl(destinationUrl, null));
}
}
return storageFiles;
}
}

private class FileOpenPickerDelegate : UIDocumentPickerDelegate
Expand Down
38 changes: 38 additions & 0 deletions src/Uno.UWP/Storage/Pickers/FileOpenPickerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#nullable enable

namespace Windows.Storage.Pickers;

/// <summary>
/// Contains Uno Platform-specific extesions for the <see cref="FileOpenPicker"/> class.
/// </summary>
public static class FileOpenPickerExtensions
{
/// <summary>
/// Sets the file limit a user can select when picking multiple files.
/// </summary>
/// <param name="limit">The maximum number of files that the user can pick.</param>
#if !__IOS__
[global::Uno.NotImplemented]
#endif
public static void SetMultipleFilesLimit(this FileOpenPicker picker, int limit)
{
#if __IOS__
picker.SetMultipleFileLimit(limit);
#endif
}

/// <summary>
///
/// </summary>
/// <param name="picker"></param>
/// <param name="readOnly"></param>
#if !__IOS__
[global::Uno.NotImplemented]
#endif
public static void SetReadOnlyMode(this FileOpenPicker picker, bool readOnly)
{
#if __IOS__
picker.SetReadOnlyMode(readOnly);
#endif
}
}
Loading
Loading