diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FileSaverViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FileSaverViewModel.cs index 451c2cd48d..4618052548 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FileSaverViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FileSaverViewModel.cs @@ -20,8 +20,10 @@ async Task SaveFile(CancellationToken cancellationToken) using var stream = new MemoryStream(Encoding.Default.GetBytes("Hello from the Community Toolkit!")); try { - var fileLocation = await fileSaver.SaveAsync("test.txt", stream, cancellationToken); - await Toast.Make($"File is saved: {fileLocation}").Show(cancellationToken); + var fileLocationResult = await fileSaver.SaveAsync("test.txt", stream, cancellationToken); + fileLocationResult.EnsureSuccess(); + + await Toast.Make($"File is saved: {fileLocationResult.FilePath}").Show(cancellationToken); } catch (Exception ex) { @@ -33,14 +35,14 @@ async Task SaveFile(CancellationToken cancellationToken) async Task SaveFileStatic(CancellationToken cancellationToken) { using var stream = new MemoryStream(Encoding.Default.GetBytes("Hello from the Community Toolkit!")); - try + var fileSaveResult = await FileSaver.SaveAsync("DCIM", "test.txt", stream, cancellationToken); + if (fileSaveResult.IsSuccessful) { - var fileLocation = await FileSaver.SaveAsync("test.txt", stream, cancellationToken); - await Toast.Make($"File is saved: {fileLocation}").Show(cancellationToken); + await Toast.Make($"File is saved: {fileSaveResult.FilePath}").Show(cancellationToken); } - catch (Exception ex) + else { - await Toast.Make($"File is not saved, {ex.Message}").Show(cancellationToken); + await Toast.Make($"File is not saved, {fileSaveResult.Exception.Message}").Show(cancellationToken); } } @@ -51,8 +53,10 @@ async Task SaveFileInstance(CancellationToken cancellationToken) try { var fileSaverInstance = new FileSaverImplementation(); - var fileLocation = await fileSaverInstance.SaveAsync("test.txt", stream, cancellationToken); - await Toast.Make($"File is saved: {fileLocation}").Show(cancellationToken); + var fileSaverResult = await fileSaverInstance.SaveAsync("test.txt", stream, cancellationToken); + fileSaverResult.EnsureSuccess(); + + await Toast.Make($"File is saved: {fileSaverResult.FilePath}").Show(cancellationToken); #if IOS || MACCATALYST fileSaverInstance.Dispose(); #endif diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FolderPickerViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FolderPickerViewModel.cs index e9f53985dd..90d3ff5939 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FolderPickerViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FolderPickerViewModel.cs @@ -1,4 +1,3 @@ -using System.Text; using CommunityToolkit.Maui.Alerts; using CommunityToolkit.Maui.Core; using CommunityToolkit.Maui.Storage; @@ -18,47 +17,48 @@ public FolderPickerViewModel(IFolderPicker folderPicker) [RelayCommand] async Task PickFolder(CancellationToken cancellationToken) { - try + var folderPickerResult = await folderPicker.PickAsync(cancellationToken); + if (folderPickerResult.IsSuccessful) { - var folder = await folderPicker.PickAsync(cancellationToken); - await Toast.Make($"Folder picked: Name - {folder.Name}, Path - {folder.Path}", ToastDuration.Long).Show(cancellationToken); + await Toast.Make($"Folder picked: Name - {folderPickerResult.Folder.Name}, Path - {folderPickerResult.Folder.Path}", ToastDuration.Long).Show(cancellationToken); } - catch (Exception ex) + else { - await Toast.Make($"Folder is not picked, {ex.Message}").Show(cancellationToken); + await Toast.Make($"Folder is not picked, {folderPickerResult.Exception.Message}").Show(cancellationToken); } } [RelayCommand] async Task PickFolderStatic(CancellationToken cancellationToken) { - try + var folderResult = await FolderPicker.PickAsync("DCIM", cancellationToken); + if (folderResult.IsSuccessful) { - var folder = await FolderPicker.PickAsync(cancellationToken); - await Toast.Make($"Folder picked: Name - {folder.Name}, Path - {folder.Path}", ToastDuration.Long).Show(cancellationToken); + await Toast.Make($"Folder picked: Name - {folderResult.Folder.Name}, Path - {folderResult.Folder.Path}", ToastDuration.Long).Show(cancellationToken); } - catch (Exception ex) + else { - await Toast.Make($"Folder is not picked, {ex.Message}").Show(cancellationToken); + await Toast.Make($"Folder is not picked, {folderResult.Exception.Message}").Show(cancellationToken); } } [RelayCommand] async Task PickFolderInstance(CancellationToken cancellationToken) { - using var stream = new MemoryStream(Encoding.Default.GetBytes("Hello from the Community Toolkit!")); + var folderPickerInstance = new FolderPickerImplementation(); try { - var folderPickerInstance = new FolderPickerImplementation(); - var folder = await folderPickerInstance.PickAsync(cancellationToken); - await Toast.Make($"Folder picked: Name - {folder.Name}, Path - {folder.Path}", ToastDuration.Long).Show(cancellationToken); + var folderPickerResult = await folderPickerInstance.PickAsync(cancellationToken); + folderPickerResult.EnsureSuccess(); + + await Toast.Make($"Folder picked: Name - {folderPickerResult.Folder.Name}, Path - {folderPickerResult.Folder.Path}", ToastDuration.Long).Show(cancellationToken); #if IOS || MACCATALYST folderPickerInstance.Dispose(); #endif } - catch (Exception ex) + catch (Exception e) { - await Toast.Make($"Folder is not picked, {ex.Message}").Show(cancellationToken); + await Toast.Make($"Folder is not picked, {e.Message}").Show(cancellationToken); } } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaver.shared.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaver.shared.cs index d0be25d5ad..e519b889b2 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaver.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaver.shared.cs @@ -11,11 +11,11 @@ public static class FileSaver public static IFileSaver Default => defaultImplementation.Value; /// - public static Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) => + public static Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) => Default.SaveAsync(initialPath, fileName, stream, cancellationToken); /// - public static Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) => + public static Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) => Default.SaveAsync(fileName, stream, cancellationToken); internal static void SetDefault(IFileSaver implementation) => diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.android.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.android.cs index 6ccf0e0cde..529c37e67a 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.android.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.android.cs @@ -1,4 +1,6 @@ +using System.Web; using Android.Content; +using Android.Provider; using Android.Webkit; using Java.IO; using Microsoft.Maui.ApplicationModel; @@ -10,8 +12,7 @@ namespace CommunityToolkit.Maui.Storage; /// public sealed partial class FileSaverImplementation : IFileSaver { - /// - public async Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) + static async Task InternalSaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) { var status = await Permissions.RequestAsync().WaitAsync(cancellationToken).ConfigureAwait(false); if (status is not PermissionStatus.Granted) @@ -19,10 +20,19 @@ public async Task SaveAsync(string initialPath, string fileName, Stream throw new PermissionException("Storage permission is not granted."); } + const string baseUrl = "content://com.android.externalstorage.documents/document/primary%3A"; + if (Android.OS.Environment.ExternalStorageDirectory is not null) + { + initialPath = initialPath.Replace(Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, string.Empty, StringComparison.InvariantCulture); + } + + var initialFolderUri = AndroidUri.Parse(baseUrl + HttpUtility.UrlEncode(initialPath)); var intent = new Intent(Intent.ActionCreateDocument); + intent.AddCategory(Intent.CategoryOpenable); - intent.SetType(MimeTypeMap.Singleton?.GetMimeTypeFromExtension(GetExtension(fileName)) ?? "*/*"); + intent.SetType(MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(fileName)) ?? "*/*"); intent.PutExtra(Intent.ExtraTitle, fileName); + intent.PutExtra(DocumentsContract.ExtraInitialUri, initialFolderUri); var pickerIntent = Intent.CreateChooser(intent, string.Empty) ?? throw new InvalidOperationException("Unable to create intent."); AndroidUri? filePath = null; @@ -42,10 +52,9 @@ void OnResult(Intent resultIntent) } } - /// - public Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) + static Task InternalSaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) { - return SaveAsync(GetExternalDirectory(), fileName, stream, cancellationToken); + return InternalSaveAsync(GetExternalDirectory(), fileName, stream, cancellationToken); } static string GetExternalDirectory() @@ -81,8 +90,8 @@ static async Task SaveDocument(AndroidUri uri, Stream stream, Cancellati fileOutputStream.Close(); parcelFileDescriptor?.Close(); - var split = uri.Path?.Split(":") ?? throw new FolderPickerException("Unable to resolve path."); + var split = uri.Path?.Split(':') ?? throw new FolderPickerException("Unable to resolve path."); - return Android.OS.Environment.ExternalStorageDirectory + "/" + split[1]; + return $"{Android.OS.Environment.ExternalStorageDirectory}/{split[^1]}"; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs index 7dc191a681..316a34a8a7 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.macios.cs @@ -10,8 +10,13 @@ public sealed partial class FileSaverImplementation : IFileSaver, IDisposable UIDocumentPickerViewController? documentPickerViewController; TaskCompletionSource? taskCompetedSource; - /// - public async Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) + /// + public void Dispose() + { + InternalDispose(); + } + + async Task InternalSaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var fileManager = NSFileManager.DefaultManager; @@ -22,6 +27,7 @@ public async Task SaveAsync(string initialPath, string fileName, Stream taskCompetedSource = new TaskCompletionSource(); documentPickerViewController = new UIDocumentPickerViewController(new[] { fileUrl }); + documentPickerViewController.DirectoryUrl = NSUrl.FromString(initialPath); documentPickerViewController.DidPickDocumentAtUrls += DocumentPickerViewControllerOnDidPickDocumentAtUrls; documentPickerViewController.WasCancelled += DocumentPickerViewControllerOnWasCancelled; @@ -30,17 +36,10 @@ public async Task SaveAsync(string initialPath, string fileName, Stream return await taskCompetedSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); } - - /// - public Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) + + Task InternalSaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) { - return SaveAsync("/", fileName, stream, cancellationToken); - } - - /// - public void Dispose() - { - InternalDispose(); + return InternalSaveAsync("/", fileName, stream, cancellationToken); } void DocumentPickerViewControllerOnWasCancelled(object? sender, EventArgs e) diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.net.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.net.cs index 0f5a559999..c05280f491 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.net.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.net.cs @@ -3,14 +3,12 @@ namespace CommunityToolkit.Maui.Storage; /// public sealed partial class FileSaverImplementation : IFileSaver { - /// - public Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) + Task InternalSaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) { throw new NotImplementedException(); } - /// - public Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) + Task InternalSaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.shared.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.shared.cs index 5587c374af..fb0882325a 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.shared.cs @@ -2,6 +2,36 @@ namespace CommunityToolkit.Maui.Storage; public sealed partial class FileSaverImplementation { + /// + public async Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + var path = await InternalSaveAsync(initialPath, fileName, stream, cancellationToken); + return new FileSaverResult(path, null); + } + catch (Exception e) + { + return new FileSaverResult(null, e); + } + } + + /// + public async Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + var path = await InternalSaveAsync(fileName, stream, cancellationToken); + return new FileSaverResult(path, null); + } + catch (Exception e) + { + return new FileSaverResult(null, e); + } + } + static async Task WriteStream(Stream stream, string filePath, CancellationToken cancellationToken) { await using var fileStream = new FileStream(filePath, FileMode.OpenOrCreate); diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.tizen.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.tizen.cs index 3b358b24ad..5acec1f326 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.tizen.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.tizen.cs @@ -5,8 +5,7 @@ namespace CommunityToolkit.Maui.Storage; /// public sealed partial class FileSaverImplementation : IFileSaver { - /// - public async Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) + async Task InternalSaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) { var status = await Permissions.RequestAsync().WaitAsync(cancellationToken); if (status is not PermissionStatus.Granted) @@ -26,9 +25,8 @@ public async Task SaveAsync(string initialPath, string fileName, Stream return path; } - /// - public Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) + Task InternalSaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) { - return SaveAsync(FileFolderDialog.GetExternalDirectory(), fileName, stream, cancellationToken); + return InternalSaveAsync(FileFolderDialog.GetExternalDirectory(), fileName, stream, cancellationToken); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.windows.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.windows.cs index 3b64bc2840..e9a7fced44 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.windows.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.windows.cs @@ -8,8 +8,7 @@ public sealed partial class FileSaverImplementation : IFileSaver { readonly List allFilesExtension = new() { "." }; - /// - public async Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) + async Task InternalSaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) { var savePicker = new FileSavePicker { @@ -24,11 +23,6 @@ public async Task SaveAsync(string initialPath, string fileName, Stream var filePickerOperation = savePicker.PickSaveFileAsync(); - void CancelFilePickerOperation() - { - filePickerOperation.Cancel(); - } - await using var _ = cancellationToken.Register(CancelFilePickerOperation); var file = await filePickerOperation; if (string.IsNullOrEmpty(file?.Path)) @@ -38,11 +32,15 @@ void CancelFilePickerOperation() await WriteStream(stream, file.Path, cancellationToken).ConfigureAwait(false); return file.Path; - } - /// - public Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) + void CancelFilePickerOperation() + { + filePickerOperation.Cancel(); + } + } + + Task InternalSaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) { - return SaveAsync(string.Empty, fileName, stream, cancellationToken); + return InternalSaveAsync(string.Empty, fileName, stream, cancellationToken); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverResult.shared.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverResult.shared.cs new file mode 100644 index 0000000000..2ae0d0d8fe --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverResult.shared.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; + +namespace CommunityToolkit.Maui.Storage; + +/// +/// Result of the +/// +/// Saved file path +/// Exception if operation failed +public record FileSaverResult(string? FilePath, Exception? Exception) +{ + /// + /// Check if operation was successful. + /// + [MemberNotNullWhen(true, nameof(FilePath))] + [MemberNotNullWhen(false, nameof(Exception))] + public bool IsSuccessful => Exception is null; + + /// + /// Check if operation was successful. + /// + [MemberNotNull(nameof(FilePath))] + public void EnsureSuccess() + { + if (!IsSuccessful) + { + throw Exception; + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/IFileSaver.shared.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/IFileSaver.shared.cs index 4f456735d2..b3252468ca 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/IFileSaver.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/IFileSaver.shared.cs @@ -12,7 +12,7 @@ public interface IFileSaver /// File name with extension /// /// - Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken); + Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken); /// /// Saves a file to the default folder on the file system @@ -20,5 +20,5 @@ public interface IFileSaver /// File name with extension /// /// - Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken); + Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPicker.shared.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPicker.shared.cs index 68c4badb32..0d6a9f1ea0 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPicker.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPicker.shared.cs @@ -1,5 +1,3 @@ -using CommunityToolkit.Maui.Core.Primitives; - namespace CommunityToolkit.Maui.Storage; /// @@ -13,11 +11,11 @@ public static class FolderPicker public static IFolderPicker Default => defaultImplementation.Value; /// - public static Task PickAsync(string initialPath, CancellationToken cancellationToken) => + public static Task PickAsync(string initialPath, CancellationToken cancellationToken) => Default.PickAsync(initialPath, cancellationToken); /// - public static Task PickAsync(CancellationToken cancellationToken) => + public static Task PickAsync(CancellationToken cancellationToken) => Default.PickAsync(cancellationToken); internal static void SetDefault(IFolderPicker implementation) => diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.android.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.android.cs index af16a092fa..3288ccda63 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.android.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.android.cs @@ -1,4 +1,6 @@ +using System.Web; using Android.Content; +using Android.Provider; using CommunityToolkit.Maui.Core.Primitives; using Microsoft.Maui.ApplicationModel; using AndroidUri = Android.Net.Uri; @@ -6,10 +8,9 @@ namespace CommunityToolkit.Maui.Storage; /// -public sealed class FolderPickerImplementation : IFolderPicker +public sealed partial class FolderPickerImplementation : IFolderPicker { - /// - public async Task PickAsync(string initialPath, CancellationToken cancellationToken) + async Task InternalPickAsync(string initialPath, CancellationToken cancellationToken) { var status = await Permissions.RequestAsync().WaitAsync(cancellationToken).ConfigureAwait(false); if (status is not PermissionStatus.Granted) @@ -18,8 +19,16 @@ public async Task PickAsync(string initialPath, CancellationToken cancel } Folder? folder = null; + const string baseUrl = "content://com.android.externalstorage.documents/document/primary%3A"; + if (Android.OS.Environment.ExternalStorageDirectory is not null) + { + initialPath = initialPath.Replace(Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, string.Empty, StringComparison.InvariantCulture); + } + + var initialFolderUri = AndroidUri.Parse(baseUrl + HttpUtility.UrlEncode(initialPath)); var intent = new Intent(Intent.ActionOpenDocumentTree); + intent.PutExtra(DocumentsContract.ExtraInitialUri, initialFolderUri); var pickerIntent = Intent.CreateChooser(intent, string.Empty) ?? throw new InvalidOperationException("Unable to create intent."); await IntermediateActivity.StartAsync(pickerIntent, (int)AndroidRequestCode.RequestCodeFolderPicker, onResult: OnResult).WaitAsync(cancellationToken); @@ -33,10 +42,9 @@ void OnResult(Intent resultIntent) } } - /// - public Task PickAsync(CancellationToken cancellationToken) + Task InternalPickAsync(CancellationToken cancellationToken) { - return PickAsync(GetExternalDirectory(), cancellationToken); + return InternalPickAsync(GetExternalDirectory(), cancellationToken); } static string GetExternalDirectory() @@ -54,8 +62,8 @@ static string EnsurePhysicalPath(AndroidUri? uri) const string uriSchemeFolder = "content"; if (uri.Scheme is not null && uri.Scheme.Equals(uriSchemeFolder, StringComparison.OrdinalIgnoreCase)) { - var split = uri.Path?.Split(":") ?? throw new FolderPickerException("Unable to resolve path."); - return Android.OS.Environment.ExternalStorageDirectory + "/" + split[1]; + var split = uri.Path?.Split(':') ?? throw new FolderPickerException("Unable to resolve path."); + return $"{Android.OS.Environment.ExternalStorageDirectory}/{split[^1]}"; } throw new FolderPickerException($"Unable to resolve absolute path or retrieve contents of URI '{uri}'."); diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.macios.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.macios.cs index c2920cfca7..6c700f87ab 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.macios.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.macios.cs @@ -7,7 +7,7 @@ namespace CommunityToolkit.Maui.Storage; /// [SupportedOSPlatform("iOS14.0")] [SupportedOSPlatform("MacCatalyst14.0")] -public sealed class FolderPickerImplementation : IFolderPicker, IDisposable +public sealed partial class FolderPickerImplementation : IFolderPicker, IDisposable { readonly UIDocumentPickerViewController documentPickerViewController = new(new[] { UTTypes.Folder }) { @@ -26,7 +26,14 @@ public FolderPickerImplementation() } /// - public async Task PickAsync(string initialPath, CancellationToken cancellationToken) + public void Dispose() + { + documentPickerViewController.DidPickDocumentAtUrls -= DocumentPickerViewControllerOnDidPickDocumentAtUrls; + documentPickerViewController.WasCancelled -= DocumentPickerViewControllerOnWasCancelled; + documentPickerViewController.Dispose(); + } + + async Task InternalPickAsync(string initialPath, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); documentPickerViewController.DirectoryUrl = NSUrl.FromString(initialPath); @@ -38,18 +45,9 @@ public async Task PickAsync(string initialPath, CancellationToken cancel return await taskCompetedSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false); } - /// - public Task PickAsync(CancellationToken cancellationToken) - { - return PickAsync("/", cancellationToken); - } - - /// - public void Dispose() + Task InternalPickAsync(CancellationToken cancellationToken) { - documentPickerViewController.DidPickDocumentAtUrls -= DocumentPickerViewControllerOnDidPickDocumentAtUrls; - documentPickerViewController.WasCancelled -= DocumentPickerViewControllerOnWasCancelled; - documentPickerViewController.Dispose(); + return InternalPickAsync("/", cancellationToken); } void DocumentPickerViewControllerOnWasCancelled(object? sender, EventArgs e) diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.net.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.net.cs index 691d9ad1ac..691c9761f3 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.net.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.net.cs @@ -3,11 +3,9 @@ namespace CommunityToolkit.Maui.Storage; /// -public sealed class FolderPickerImplementation : IFolderPicker +public sealed partial class FolderPickerImplementation : IFolderPicker { - /// - public Task PickAsync(string initialPath, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task InternalPickAsync(string initialPath, CancellationToken cancellationToken) => throw new NotImplementedException(); - /// - public Task PickAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + Task InternalPickAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.shared.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.shared.cs new file mode 100644 index 0000000000..855e47e63e --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.shared.cs @@ -0,0 +1,34 @@ +namespace CommunityToolkit.Maui.Storage; + +public sealed partial class FolderPickerImplementation +{ + /// + public async Task PickAsync(string initialPath, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + var folder = await InternalPickAsync(initialPath, cancellationToken); + return new FolderPickerResult(folder, null); + } + catch (Exception e) + { + return new FolderPickerResult(null, e); + } + } + + /// + public async Task PickAsync(CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + var folder = await InternalPickAsync(cancellationToken); + return new FolderPickerResult(folder, null); + } + catch (Exception e) + { + return new FolderPickerResult(null, e); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.tizen.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.tizen.cs index 3083b16903..24a72f613f 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.tizen.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.tizen.cs @@ -4,10 +4,9 @@ namespace CommunityToolkit.Maui.Storage; /// -public sealed class FolderPickerImplementation : IFolderPicker +public sealed partial class FolderPickerImplementation : IFolderPicker { - /// - public async Task PickAsync(string initialPath, CancellationToken cancellationToken) + async Task InternalPickAsync(string initialPath, CancellationToken cancellationToken) { var status = await Permissions.RequestAsync().WaitAsync(cancellationToken); if (status is not PermissionStatus.Granted) @@ -21,9 +20,8 @@ public async Task PickAsync(string initialPath, CancellationToken cancel return new Folder(path, Path.GetFileName(path)); } - /// - public Task PickAsync(CancellationToken cancellationToken) + Task InternalPickAsync(CancellationToken cancellationToken) { - return PickAsync(FileFolderDialog.GetExternalDirectory(), cancellationToken); + return InternalPickAsync(FileFolderDialog.GetExternalDirectory(), cancellationToken); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.windows.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.windows.cs index a4a9d91a79..bfed9a902b 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.windows.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.windows.cs @@ -5,10 +5,9 @@ namespace CommunityToolkit.Maui.Storage; /// -public sealed class FolderPickerImplementation : IFolderPicker +public sealed partial class FolderPickerImplementation : IFolderPicker { - /// - public async Task PickAsync(string initialPath, CancellationToken cancellationToken) + async Task InternalPickAsync(string initialPath, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var folderPicker = new Windows.Storage.Pickers.FolderPicker() @@ -34,9 +33,8 @@ void CancelFolderPickerOperation() return new Folder(folder.Path, folder.Name); } - /// - public Task PickAsync(CancellationToken cancellationToken) + Task InternalPickAsync(CancellationToken cancellationToken) { - return PickAsync(string.Empty, cancellationToken); + return InternalPickAsync(string.Empty, cancellationToken); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerResult.shared.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerResult.shared.cs new file mode 100644 index 0000000000..f13f4f735b --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerResult.shared.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using CommunityToolkit.Maui.Core.Primitives; + +namespace CommunityToolkit.Maui.Storage; + +/// +/// Result of the +/// +/// +/// Exception if operation failed +public record FolderPickerResult(Folder? Folder, Exception? Exception) +{ + /// + /// Check if operation was successful. + /// + [MemberNotNullWhen(true, nameof(Folder))] + [MemberNotNullWhen(false, nameof(Exception))] + public bool IsSuccessful => Exception is null; + + /// + /// Check if operation was successful. + /// + [MemberNotNull(nameof(Folder))] + public void EnsureSuccess() + { + if (!IsSuccessful) + { + throw Exception; + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/IFolderPicker.shared.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/IFolderPicker.shared.cs index d55f443df6..fa2eb4ca30 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/IFolderPicker.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/IFolderPicker.shared.cs @@ -1,5 +1,3 @@ -using CommunityToolkit.Maui.Core.Primitives; - namespace CommunityToolkit.Maui.Storage; /// @@ -12,13 +10,13 @@ public interface IFolderPicker /// /// Initial path /// - /// - Task PickAsync(string initialPath, CancellationToken cancellationToken); + /// + Task PickAsync(string initialPath, CancellationToken cancellationToken); /// /// Allows the user to pick a folder from the file system /// /// - /// - Task PickAsync(CancellationToken cancellationToken); + /// + Task PickAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Essentials/FileSaverTests.cs b/src/CommunityToolkit.Maui.UnitTests/Essentials/FileSaverTests.cs index 2b49b6e906..8b274abb83 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Essentials/FileSaverTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Essentials/FileSaverTests.cs @@ -4,6 +4,7 @@ using Xunit; namespace CommunityToolkit.Maui.UnitTests.Essentials; + public class FileSaverTests { [Fact] @@ -19,7 +20,23 @@ public void FileSaverTestsSetDefaultUpdatesInstance() public async Task SaveAsyncFailsOnNet() { FileSaver.SetDefault(new FileSaverImplementation()); - await Assert.ThrowsAsync(() => FileSaver.SaveAsync("file name", Stream.Null, CancellationToken.None)); - await Assert.ThrowsAsync(() => FileSaver.SaveAsync("initial path", "file name", Stream.Null, CancellationToken.None)); + var result = await FileSaver.SaveAsync("fileName", Stream.Null, CancellationToken.None); + result.Should().NotBeNull(); + result.Exception.Should().BeOfType(); + result.FilePath.Should().BeNull(); + result.IsSuccessful.Should().BeFalse(); + Assert.Throws(result.EnsureSuccess); + } + + [Fact] + public async Task SaveAsyncWithInitialPathFailsOnNet() + { + FileSaver.SetDefault(new FileSaverImplementation()); + var result = await FileSaver.SaveAsync("initial path","fileName", Stream.Null, CancellationToken.None); + result.Should().NotBeNull(); + result.Exception.Should().BeOfType(); + result.FilePath.Should().BeNull(); + result.IsSuccessful.Should().BeFalse(); + Assert.Throws(result.EnsureSuccess); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Essentials/FolderPickerTests.cs b/src/CommunityToolkit.Maui.UnitTests/Essentials/FolderPickerTests.cs index d7d578daba..b3648e1210 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Essentials/FolderPickerTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Essentials/FolderPickerTests.cs @@ -1,10 +1,10 @@ -using CommunityToolkit.Maui.Core; -using CommunityToolkit.Maui.Storage; +using CommunityToolkit.Maui.Storage; using CommunityToolkit.Maui.UnitTests.Mocks; using FluentAssertions; using Xunit; namespace CommunityToolkit.Maui.UnitTests.Essentials; + public class FolderPickerTests { [Fact] @@ -20,7 +20,23 @@ public void FolderPickerSetDefaultUpdatesInstance() public async Task PickAsyncFailsOnNet() { FolderPicker.SetDefault(new FolderPickerImplementation()); - await Assert.ThrowsAsync(() => FolderPicker.PickAsync(CancellationToken.None)); - await Assert.ThrowsAsync(() => FolderPicker.PickAsync("initial path", CancellationToken.None)); + var result = await FolderPicker.PickAsync(CancellationToken.None); + result.Should().NotBeNull(); + result.Exception.Should().BeOfType(); + result.Folder.Should().BeNull(); + result.IsSuccessful.Should().BeFalse(); + Assert.Throws(result.EnsureSuccess); + } + + [Fact] + public async Task PickAsyncWithInitialPathFailsOnNet() + { + FolderPicker.SetDefault(new FolderPickerImplementation()); + var result = await FolderPicker.PickAsync("initial path", CancellationToken.None); + result.Should().NotBeNull(); + result.Exception.Should().BeOfType(); + result.Folder.Should().BeNull(); + result.IsSuccessful.Should().BeFalse(); + Assert.Throws(result.EnsureSuccess); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/FileSaverImplementationMock.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/FileSaverImplementationMock.cs index 87a5c4bcda..f643cb7899 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Mocks/FileSaverImplementationMock.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/FileSaverImplementationMock.cs @@ -4,14 +4,14 @@ namespace CommunityToolkit.Maui.UnitTests.Mocks; class FileSaverImplementationMock : IFileSaver { - public Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) + public Task SaveAsync(string initialPath, string fileName, Stream stream, CancellationToken cancellationToken) { return string.IsNullOrWhiteSpace(initialPath) ? - Task.FromException(new FileSaveException("Error")) : - Task.FromResult("path"); + Task.FromResult(new FileSaverResult(null, new FileSaveException("Error"))) : + Task.FromResult(new FileSaverResult("path", null)); } - public Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) + public Task SaveAsync(string fileName, Stream stream, CancellationToken cancellationToken) { return SaveAsync(string.Empty, fileName, stream, cancellationToken); } diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/FolderPickerImplementationMock.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/FolderPickerImplementationMock.cs index 47b2debcea..06ed7438a3 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Mocks/FolderPickerImplementationMock.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/FolderPickerImplementationMock.cs @@ -5,17 +5,17 @@ namespace CommunityToolkit.Maui.UnitTests.Mocks; class FolderPickerImplementationMock : IFolderPicker { - public Task PickAsync(string initialPath, CancellationToken cancellationToken) + public Task PickAsync(string initialPath, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(initialPath)) { - return Task.FromException(new FolderPickerException("error")); + return Task.FromResult(new FolderPickerResult(null, new FolderPickerException("error"))); } - return Task.FromResult(new Folder(initialPath, "name")); + return Task.FromResult(new FolderPickerResult(new Folder(initialPath, "name"), null)); } - public Task PickAsync(CancellationToken cancellationToken) + public Task PickAsync(CancellationToken cancellationToken) { return PickAsync(string.Empty, cancellationToken); }