Browse Source

File activation (#15317)

* Avoid AndroidX extensions in storage provider code

* Add new ActivationKind.File

* ActivationKind.File implementation for Android

* ActivationKind.File implementation for macOS

* ActivationKind.File implementation for iOS

* Properly handle "file:" scheme on macOS backend

* Remove unused PackageReference
pull/15367/head
Max Katz 2 years ago
committed by GitHub
parent
commit
4c22660c3f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  2. 18
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  3. 19
      src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs
  4. 68
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  5. 28
      src/Avalonia.Controls/ApplicationLifetimes/ActivatableLifetimeBase.cs
  6. 3
      src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs
  7. 11
      src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs
  8. 14
      src/Avalonia.Controls/ApplicationLifetimes/FileActivatedEventArgs.cs
  9. 6
      src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs
  10. 2
      src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs
  11. 60
      src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
  12. 29
      src/Avalonia.Native/MacOSActivatableLifetime.cs
  13. 11
      src/iOS/Avalonia.iOS/ActivatableLifetime.cs
  14. 12
      src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
  15. 13
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
  16. 25
      src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs

2
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -8,8 +8,6 @@
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.6.1.5" />
<PackageReference Include="Xamarin.AndroidX.DocumentFile" Version="1.0.1.21" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.6.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />

18
src/Android/Avalonia.Android/AvaloniaActivity.cs

@ -8,6 +8,7 @@ using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.AppCompat.App;
using Avalonia.Android.Platform.Storage;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Android;
@ -80,12 +81,23 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity
_listener = new GlobalLayoutListener(_view);
_view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener);
// TODO: we probably don't need to create AvaloniaView, if it's just a protocol activation, and main activity is already created.
if (Intent?.Data is {} androidUri
&& androidUri.IsAbsolute
&& Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var protocolUri))
&& Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var uri))
{
_onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, protocolUri));
if (uri.Scheme == Uri.UriSchemeFile)
{
if (AndroidStorageItem.CreateItem(this, androidUri) is { } item)
{
_onActivated?.Invoke(this, new FileActivatedEventArgs(new [] { item }));
}
}
else
{
_onActivated?.Invoke(this, new ProtocolActivatedEventArgs(uri));
}
}
}

19
src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs

@ -1,10 +1,9 @@
using System;
using Android.App;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Android.Platform;
internal class AndroidActivatableLifetime : IActivatableLifetime
internal class AndroidActivatableLifetime : ActivatableLifetimeBase
{
private IAvaloniaActivity? _activity;
@ -28,20 +27,10 @@ internal class AndroidActivatableLifetime : IActivatableLifetime
}
}
}
public event EventHandler<ActivatedEventArgs>? Activated;
public event EventHandler<ActivatedEventArgs>? Deactivated;
public bool TryLeaveBackground() => false;
public bool TryEnterBackground() => (_activity as Activity)?.MoveTaskToBack(true) == true;
public override bool TryEnterBackground() => (_activity as Activity)?.MoveTaskToBack(true) == true;
private void ActivityOnDeactivated(object? sender, ActivatedEventArgs e)
{
Deactivated?.Invoke(this, e);
}
private void ActivityOnDeactivated(object? sender, ActivatedEventArgs e) => OnDeactivated(e);
private void ActivityOnActivated(object? sender, ActivatedEventArgs e)
{
Activated?.Invoke(this, e);
}
private void ActivityOnActivated(object? sender, ActivatedEventArgs e) => OnActivated(e);
}

68
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@ -8,7 +8,6 @@ using Android.App;
using Android.Content;
using Android.Provider;
using Android.Webkit;
using AndroidX.DocumentFile.Provider;
using Avalonia.Logging;
using Avalonia.Platform.Storage;
using Java.Lang;
@ -38,8 +37,8 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
public virtual string Name => GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName)
?? Document?.Name
public virtual string Name => GetColumnValue(Activity, Uri, DocumentsContract.Document.ColumnDisplayName)
?? GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName)
?? Uri.PathSegments?.LastOrDefault()?.Split("/", StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? string.Empty;
public Uri Path => new(Uri.ToString()!);
@ -69,7 +68,7 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
public abstract Task<StorageItemProperties> GetBasicPropertiesAsync();
protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null)
protected static string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null)
{
try
{
@ -84,7 +83,7 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(this, "File metadata reader failed: '{Exception}'", ex);
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(null, "File metadata reader failed: '{Exception}'", ex);
}
return null;
@ -102,18 +101,6 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
return _parent;
}
var document = Document;
if (document == null)
{
return null;
}
if(document.ParentFile != null)
{
return new AndroidStorageFolder(Activity, document.ParentFile.Uri, true);
}
using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused.
@ -140,27 +127,25 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
{
_activity = null;
}
internal DocumentFile? Document
{
get
{
if (this is AndroidStorageFile)
{
return DocumentFile.FromSingleUri(Activity, Uri);
}
else
{
return DocumentFile.FromTreeUri(Activity, Uri);
}
}
}
internal AndroidUri? PermissionRoot => _permissionRoot;
public abstract Task DeleteAsync();
public abstract Task<IStorageItem?> MoveAsync(IStorageFolder destination);
public static IStorageItem CreateItem(Activity activity, AndroidUri uri)
{
var mimeType = GetColumnValue(activity, uri, DocumentsContract.Document.ColumnMimeType);
if (mimeType == DocumentsContract.Document.MimeTypeDir)
{
return new AndroidStorageFolder(activity, uri, false);
}
else
{
return new AndroidStorageFile(activity, uri);
}
}
}
internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
@ -172,26 +157,24 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
public Task<IStorageFile?> CreateFileAsync(string name)
{
var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream";
var newFile = Document?.CreateFile(mimeType, name);
var newFile = DocumentsContract.CreateDocument(Activity.ContentResolver!, Uri, mimeType, name);
if(newFile == null)
{
return Task.FromResult<IStorageFile?>(null);
}
return Task.FromResult<IStorageFile?>(new AndroidStorageFile(Activity, newFile.Uri, this));
return Task.FromResult<IStorageFile?>(new AndroidStorageFile(Activity, newFile, this));
}
public Task<IStorageFolder?> CreateFolderAsync(string name)
{
var newFolder = Document?.CreateDirectory(name);
var newFolder = DocumentsContract.CreateDocument(Activity.ContentResolver!, Uri, DocumentsContract.Document.MimeTypeDir, name);
if (newFolder == null)
{
return Task.FromResult<IStorageFolder?>(null);
}
return Task.FromResult<IStorageFolder?>(new AndroidStorageFolder(Activity, newFolder.Uri, false, this, PermissionRoot));
return Task.FromResult<IStorageFolder?>(new AndroidStorageFolder(Activity, newFolder, false, this, PermissionRoot));
}
public override async Task DeleteAsync()
@ -220,7 +203,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
}
}
DocumentFile.FromTreeUri(Activity, storageFolder.Uri)?.Delete();
DocumentsContract.DeleteDocument(Activity.ContentResolver!, storageFolder.Uri);
}
}
@ -484,7 +467,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
try
{
if (Activity.ContentResolver is { } contentResolver &&
storageFolder.Document?.Uri is { } targetParentUri &&
storageFolder.Uri is { } targetParentUri &&
await GetParentAsync() is AndroidStorageFolder parentFolder)
{
movedUri = DocumentsContract.MoveDocument(contentResolver, Uri, parentFolder.Uri, targetParentUri);
@ -492,8 +475,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
}
catch (Exception)
{
// There are many reason why DocumentContract will fail to move a file. We fallback to copying.
return await MoveFileByCopy();
// There are many reason why DocumentContract will fail to move a file. We fallback to copying below.
}
}

28
src/Avalonia.Controls/ApplicationLifetimes/ActivatableLifetimeBase.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Platform;
using Avalonia.Metadata;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
namespace Avalonia.Controls.ApplicationLifetimes;
[PrivateApi]
public abstract class ActivatableLifetimeBase : IActivatableLifetime
{
public event EventHandler<ActivatedEventArgs>? Activated;
public event EventHandler<ActivatedEventArgs>? Deactivated;
public virtual bool TryLeaveBackground() => false;
public virtual bool TryEnterBackground() => false;
protected internal void OnActivated(ActivationKind kind) => OnActivated(new ActivatedEventArgs(kind));
protected internal void OnActivated(ActivatedEventArgs eventArgs) =>
Dispatcher.UIThread.Send(_ => Activated?.Invoke(this, eventArgs));
protected internal void OnDeactivated(ActivationKind kind) => OnDeactivated(new ActivatedEventArgs(kind));
protected internal void OnDeactivated(ActivatedEventArgs eventArgs) =>
Dispatcher.UIThread.Send(_ => Deactivated?.Invoke(this, eventArgs));
}

3
src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Controls.ApplicationLifetimes;
@ -15,7 +16,7 @@ public class ActivatedEventArgs : EventArgs
{
Kind = kind;
}
/// <summary>
/// The <see cref="ActivationKind"/> that this event represents.
/// </summary>

11
src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs

@ -3,10 +3,15 @@ namespace Avalonia.Controls.ApplicationLifetimes;
public enum ActivationKind
{
/// <summary>
/// When the application is passed a URI to open.
/// When the application is passed a file to open.
/// </summary>
OpenUri = 20,
File = 10,
/// <summary>
/// When the application is passed a URI to open, protocol activation.
/// </summary>
OpenUri = 20,
/// <summary>
/// When the application is asked to reopen.
/// An example of this is on MacOS when all the windows are closed,

14
src/Avalonia.Controls/ApplicationLifetimes/FileActivatedEventArgs.cs

@ -0,0 +1,14 @@
using System.Collections.Generic;
using Avalonia.Platform.Storage;
namespace Avalonia.Controls.ApplicationLifetimes;
public sealed class FileActivatedEventArgs : ActivatedEventArgs
{
public FileActivatedEventArgs(IReadOnlyList<IStorageItem> files) : base(ActivationKind.File)
{
Files = files;
}
public IReadOnlyList<IStorageItem> Files { get; }
}

6
src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs

@ -1,10 +1,12 @@
using System;
using System.Linq;
using Avalonia.Metadata;
namespace Avalonia.Controls.ApplicationLifetimes;
public class ProtocolActivatedEventArgs : ActivatedEventArgs
public sealed class ProtocolActivatedEventArgs : ActivatedEventArgs
{
public ProtocolActivatedEventArgs(ActivationKind kind, Uri uri) : base(kind)
public ProtocolActivatedEventArgs(Uri uri) : base(ActivationKind.OpenUri)
{
Uri = uri;
}

2
src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs

@ -2,7 +2,7 @@ using Avalonia.Metadata;
namespace Avalonia.Platform
{
[Unstable]
[Unstable("This interface will be removed in 12.0.")]
public interface IApplicationPlatformEvents
{
void RaiseUrlsOpened(string[] urls);

60
src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs

@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Native.Interop;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Native
{
@ -13,46 +16,85 @@ namespace Avalonia.Native
void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls)
{
((IApplicationPlatformEvents)Application.Current)?.RaiseUrlsOpened(urls.ToStringArray());
if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is ActivatableLifetimeBase lifetime)
{
var filePaths = urls.ToStringArray();
var files = new List<IStorageItem>(filePaths.Length);
foreach (var filePath in filePaths)
{
if (StorageProviderHelpers.TryCreateBclStorageItem(filePath) is { } file)
{
files.Add(file);
}
}
if (files.Count > 0)
{
lifetime.OnActivated(new FileActivatedEventArgs(files));
}
}
}
void IAvnApplicationEvents.UrlsOpened(IAvnStringArray urls)
{
// Raise the urls opened event to be compatible with legacy behavior.
((IApplicationPlatformEvents)Application.Current)?.RaiseUrlsOpened(urls.ToStringArray());
if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is MacOSActivatableLifetime lifetime)
if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is ActivatableLifetimeBase lifetime)
{
var files = new List<IStorageItem>();
var uris = new List<Uri>();
foreach (var url in urls.ToStringArray())
{
if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri))
{
lifetime.RaiseUrl(uri);
if (uri.Scheme == Uri.UriSchemeFile)
{
if (StorageProviderHelpers.TryCreateBclStorageItem(uri.LocalPath) is { } file)
{
files.Add(file);
}
}
else
{
uris.Add(uri);
}
}
}
foreach (var uri in uris)
{
lifetime.OnActivated(new ProtocolActivatedEventArgs(uri));
}
if (files.Count > 0)
{
lifetime.OnActivated(new FileActivatedEventArgs(files));
}
}
}
void IAvnApplicationEvents.OnReopen()
{
if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is MacOSActivatableLifetime lifetime)
if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is ActivatableLifetimeBase lifetime)
{
lifetime.RaiseActivated(ActivationKind.Reopen);
lifetime.OnActivated(ActivationKind.Reopen);
}
}
void IAvnApplicationEvents.OnHide()
{
if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is MacOSActivatableLifetime lifetime)
if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is ActivatableLifetimeBase lifetime)
{
lifetime.RaiseDeactivated(ActivationKind.Background);
lifetime.OnActivated(ActivationKind.Background);
}
}
void IAvnApplicationEvents.OnUnhide()
{
if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is MacOSActivatableLifetime lifetime)
if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is ActivatableLifetimeBase lifetime)
{
lifetime.RaiseActivated(ActivationKind.Background);
lifetime.OnActivated(ActivationKind.Background);
}
}

29
src/Avalonia.Native/MacOSActivatableLifetime.cs

@ -6,16 +6,9 @@ namespace Avalonia.Native;
#nullable enable
internal class MacOSActivatableLifetime : IActivatableLifetime
internal class MacOSActivatableLifetime : ActivatableLifetimeBase
{
/// <inheritdoc />
public event EventHandler<ActivatedEventArgs>? Activated;
/// <inheritdoc />
public event EventHandler<ActivatedEventArgs>? Deactivated;
/// <inheritdoc />
public bool TryLeaveBackground()
public override bool TryLeaveBackground()
{
var nativeApplicationCommands = AvaloniaLocator.Current.GetService<INativeApplicationCommands>();
nativeApplicationCommands?.ShowApp();
@ -23,27 +16,11 @@ internal class MacOSActivatableLifetime : IActivatableLifetime
return true;
}
/// <inheritdoc />
public bool TryEnterBackground()
public override bool TryEnterBackground()
{
var nativeApplicationCommands = AvaloniaLocator.Current.GetService<INativeApplicationCommands>();
nativeApplicationCommands?.HideApp();
return true;
}
internal void RaiseUrl(Uri uri)
{
Activated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, uri));
}
internal void RaiseActivated(ActivationKind kind)
{
Activated?.Invoke(this, new ActivatedEventArgs(kind));
}
internal void RaiseDeactivated(ActivationKind kind)
{
Deactivated?.Invoke(this, new ActivatedEventArgs(kind));
}
}

11
src/iOS/Avalonia.iOS/ActivatableLifetime.cs

@ -3,16 +3,11 @@ using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.iOS;
internal class ActivatableLifetime : IActivatableLifetime
internal class ActivatableLifetime : ActivatableLifetimeBase
{
public ActivatableLifetime(IAvaloniaAppDelegate avaloniaAppDelegate)
{
avaloniaAppDelegate.Activated += (_, args) => Activated?.Invoke(this, args);
avaloniaAppDelegate.Deactivated += (_, args) => Deactivated?.Invoke(this, args);
avaloniaAppDelegate.Activated += (_, args) => OnActivated(args);
avaloniaAppDelegate.Deactivated += (_, args) => OnDeactivated(args);
}
public event EventHandler<ActivatedEventArgs>? Activated;
public event EventHandler<ActivatedEventArgs>? Deactivated;
public bool TryLeaveBackground() => false;
public bool TryEnterBackground() => false;
}

12
src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs

@ -1,7 +1,6 @@
using System;
using Foundation;
using Avalonia.Controls.ApplicationLifetimes;
using UIKit;
namespace Avalonia.iOS
@ -74,7 +73,16 @@ namespace Avalonia.iOS
{
if (Uri.TryCreate(url.ToString(), UriKind.Absolute, out var uri))
{
_onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, uri));
#if !TVOS
if (uri.Scheme == Uri.UriSchemeFile)
{
_onActivated?.Invoke(this, new FileActivatedEventArgs(new[] { Storage.IOSStorageItem.CreateItem(url) }));
}
else
#endif
{
_onActivated?.Invoke(this, new ProtocolActivatedEventArgs(uri));
}
return true;
}

13
src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

@ -29,6 +29,13 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
?? string.Empty;
}
}
public static IStorageItem CreateItem(NSUrl url, NSUrl? securityScopedAncestorUrl = null)
{
return url.HasDirectoryPath ?
new IOSStorageFolder(url, securityScopedAncestorUrl) :
new IOSStorageFile(url, securityScopedAncestorUrl);
}
internal NSUrl Url { get; }
// Calling StartAccessingSecurityScopedResource on items retrieved from, or created in a folder
@ -161,7 +168,7 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile
public IOSStorageFile(NSUrl url, NSUrl? securityScopedAncestorUrl = null) : base(url, securityScopedAncestorUrl)
{
}
public Task<Stream> OpenReadAsync()
{
return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, SecurityScopedAncestorUrl, FileAccess.Read));
@ -208,9 +215,7 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
else
{
var items = content
.Select(u => u.HasDirectoryPath ?
(IStorageItem)new IOSStorageFolder(u, SecurityScopedAncestorUrl) :
new IOSStorageFile(u, SecurityScopedAncestorUrl))
.Select(u => CreateItem(u, SecurityScopedAncestorUrl))
.ToArray();
tcs.TrySetResult(items);
}

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

@ -104,14 +104,31 @@ internal class IOSStorageProvider : IStorageProvider
public Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{
// TODO: research if it's possible, maybe with additional permissions.
return Task.FromResult<IStorageFile?>(null);
var fileUrl = (NSUrl?)filePath;
var isDirectory = false;
var file = fileUrl is not null
&& fileUrl.Path is { } path
&& NSFileManager.DefaultManager.FileExists(path, ref isDirectory)
&& !isDirectory
&& NSFileManager.DefaultManager.IsReadableFile(path) ?
new IOSStorageFile(fileUrl) :
null;
return Task.FromResult<IStorageFile?>(file);
}
public Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{
// TODO: research if it's possible, maybe with additional permissions.
return Task.FromResult<IStorageFolder?>(null);
var folderUrl = (NSUrl?)folderPath;
var isDirectory = false;
var folder = folderUrl is not null
&& folderUrl.Path is { } path
&& NSFileManager.DefaultManager.FileExists(path, ref isDirectory)
&& isDirectory ?
new IOSStorageFolder(folderUrl) :
null;
return Task.FromResult<IStorageFolder?>(folder);
}
public Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)

Loading…
Cancel
Save