diff --git a/src/Uno.UWP/Devices/Sensors/Helpers/SensorHelpers.iOS.cs b/src/Uno.UWP/Devices/Sensors/Helpers/SensorHelpers.iOS.cs index a4579a75761c..33b1aaffe2b9 100644 --- a/src/Uno.UWP/Devices/Sensors/Helpers/SensorHelpers.iOS.cs +++ b/src/Uno.UWP/Devices/Sensors/Helpers/SensorHelpers.iOS.cs @@ -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); } } diff --git a/src/Uno.UWP/Extensions/NSDateExtensions.cs b/src/Uno.UWP/Extensions/NSDateExtensions.cs new file mode 100644 index 000000000000..f3b093deb2c0 --- /dev/null +++ b/src/Uno.UWP/Extensions/NSDateExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Foundation; + +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) + { + 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) + { + 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); + } +} diff --git a/src/Uno.UWP/Storage/Pickers/FileOpenPicker.iOS.cs b/src/Uno.UWP/Storage/Pickers/FileOpenPicker.iOS.cs index f9b83b16d088..a786d37b018c 100644 --- a/src/Uno.UWP/Storage/Pickers/FileOpenPicker.iOS.cs +++ b/src/Uno.UWP/Storage/Pickers/FileOpenPicker.iOS.cs @@ -5,16 +5,15 @@ 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 { @@ -73,7 +72,7 @@ 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 @@ -83,7 +82,7 @@ private UIViewController GetViewController(bool multiple, int limit, TaskComplet Delegate = new PhotoPickerDelegate(completionSource) }; 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 @@ -167,9 +166,13 @@ public override void FinishedPickingMedia(UIImagePickerController picker, NSDict private class PhotoPickerDelegate : PHPickerViewControllerDelegate { private readonly TaskCompletionSource _taskCompletionSource; + private readonly bool _readOnly; - public PhotoPickerDelegate(TaskCompletionSource taskCompletionSource) => + public PhotoPickerDelegate(TaskCompletionSource taskCompletionSource, bool readOnly) + { _taskCompletionSource = taskCompletionSource; + _readOnly = readOnly; + } public override async void DidFinishPicking(PHPickerViewController picker, PHPickerResult[] results) { @@ -186,33 +189,51 @@ public override async void DidFinishPicking(PHPickerViewController picker, PHPic private async Task> ConvertPickerResults(PHPickerResult[] results) { List storageFiles = new List(); - 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); } + } + else + { + var providers = results + .Select(res => res.ItemProvider) + .Where(provider => provider != null && provider.RegisteredTypeIdentifiers?.Length > 0) + .ToArray(); - var extension = GetExtension(identifier); + foreach (NSItemProvider provider in providers) + { + var identifier = GetIdentifier(provider.RegisteredTypeIdentifiers ?? []) ?? "public.data"; + var data = await provider.LoadDataRepresentationAsync(identifier); - var destinationUrl = NSFileManager.DefaultManager - .GetTemporaryDirectory() - .Append($"{NSProcessInfo.ProcessInfo.GloballyUniqueString}.{extension}", false); - data.Save(destinationUrl, false); + if (data is null) + { + continue; + } - storageFiles.Add(StorageFile.GetFromSecurityScopedUrl(destinationUrl, null)); - } + 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)) diff --git a/src/Uno.UWP/Storage/StorageFile.iOS.cs b/src/Uno.UWP/Storage/StorageFile.iOS.cs index 97182e946667..636e2aeb48ec 100644 --- a/src/Uno.UWP/Storage/StorageFile.iOS.cs +++ b/src/Uno.UWP/Storage/StorageFile.iOS.cs @@ -1,19 +1,19 @@ #nullable enable using System; +using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Foundation; +using Photos; +using PhotosUI; using UIKit; -using Windows.Storage.FileProperties; -using Windows.Storage.Streams; using Uno.Storage.Internal; using Uno.Storage.Streams.Internal; -using System.IO; -using System.Linq; -using MobileCoreServices; -using SystemPath = System.IO.Path; -using PhotosUI; +using Windows.Extensions; +using Windows.Storage.FileProperties; +using Windows.Storage.Streams; namespace Windows.Storage { @@ -22,8 +22,8 @@ public partial class StorageFile internal static StorageFile GetFromSecurityScopedUrl(NSUrl nsUrl, StorageFolder? parent) => new StorageFile(new SecurityScopedFile(nsUrl, parent)); - internal static StorageFile GetFromPHPickerResult(PHPickerResult result, StorageFolder? parent) => - new StorageFile(new PHPickerResultFile(result, parent)); + internal static StorageFile GetFromPHPickerResult(PHPickerResult result, PHAsset phAsset, StorageFolder? parent) => + new StorageFile(new PHPickerResultFile(result, phAsset, parent)); internal class SecurityScopedFile : ImplementationBase { @@ -100,61 +100,64 @@ implementation is SecurityScopedFile file && file._nsUrl.FilePathUrl?.Path == _nsUrl.FilePathUrl?.Path; } - internal class PHPickerResultFile : ImplementationBase + internal class PHAssetFile : ImplementationBase { - private PHPickerResult _phPickerResult; + private readonly PHAsset _phAsset; private StorageFolder? _parent; - public PHPickerResultFile(PHPickerResult phPickerResult, StorageFolder? parent) : base(string.Empty) + public PHAssetFile(PHAsset phAsset, StorageFolder? parent) : base(string.Empty) { - if (phPickerResult is null) - { - throw new ArgumentNullException(nameof(phPickerResult)); - } - - _phPickerResult = phPickerResult; + _phAsset = phAsset; _parent = parent; + Path = phAsset. } public override StorageProvider Provider => StorageProviders.IosPHPicker; - public override DateTimeOffset DateCreated - { - get - { - var itemProvider = _phPickerResult.ItemProvider; + public override DateTimeOffset DateCreated => _phAsset.CreationDate.ToDateTimeOffset(); - if (itemProvider.HasItemConformingTo(UTType.Image)) + public override Task DeleteAsync(CancellationToken ct, StorageDeleteOption options) + { + TaskCompletionSource resultCompletionSource = new TaskCompletionSource(); + PHPhotoLibrary.SharedPhotoLibrary.PerformChanges( + () => { - var fileUrl = await GetFileUrlAsync(itemProvider); - - if (fileUrl != null) + PHAssetChangeRequest.DeleteAssets(new PHAsset[] { _phAsset }); + }, + (success, error) => + { + if (success) + { + resultCompletionSource.SetResult(); + } + else { - var attributes = NSFileManager.DefaultManager.GetAttributes(fileUrl.Path, out NSError error); - if (error != null) - { - throw new IOException($"Error retrieving file attributes: {error.LocalizedDescription}"); - } - - var creationDate = attributes.CreationDate; - return creationDate?.ToDateTimeOffset() - ?? throw new InvalidOperationException("Creation date not found."); + resultCompletionSource.SetException(new Exception(error.LocalizedDescription)); } } + ); - throw new InvalidOperationException("Item does not conform to a supported type."); - } + return resultCompletionSource.Task; } - public override Task DeleteAsync(CancellationToken ct, StorageDeleteOption options) => throw new NotImplementedException(); - public override Task GetBasicPropertiesAsync(CancellationToken ct) => throw new NotImplementedException(); - public override Task GetParentAsync(CancellationToken ct) => throw new NotImplementedException(); - public override Task OpenAsync(CancellationToken ct, FileAccessMode accessMode, StorageOpenOptions options) => throw new NotImplementedException(); - public override Task OpenTransactedWriteAsync(CancellationToken ct, StorageOpenOptions option) => throw new NotImplementedException(); - protected override bool IsEqual(ImplementationBase implementation) + public override Task GetBasicPropertiesAsync(CancellationToken ct) { - implementation is PHPickerResultFile file && file._phPickerResult.ItemProvider.Load + var resources = PHAssetResource.GetAssetResources(_phAsset); + var imageSizeBytes = (resources.FirstOrDefault()?.ValueForKey(new NSString("fileSize")) as NSNumber) ?? 0; + var dateModified = _phAsset.ModificationDate.ToDateTimeOffset(); + return Task.FromResult(new BasicProperties((ulong)imageSizeBytes, dateModified)); } + + public override Task GetParentAsync(CancellationToken ct) => Task.FromResult(_parent); + + public override Task OpenAsync(CancellationToken ct, FileAccessMode accessMode, StorageOpenOptions options) + => Task.FromResult(new RandomAccessStreamWithContentType(FileRandomAccessStream.CreateSecurityScoped(_phAsset.Rea, ToFileAccess(accessMode), ToFileShare(options)), ContentType)); + public override Task OpenTransactedWriteAsync(CancellationToken ct, StorageOpenOptions option) + { + + } + + protected override bool IsEqual(ImplementationBase implementation) => implementation is PHAssetFile file && file._phAsset.LocalIdentifier == _phAsset.LocalIdentifier; } } }