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);
}