From 65866fc5261cefa2d9380c01b970c638f635fa27 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 25 Jul 2022 10:40:54 +0200 Subject: [PATCH 1/3] Improve csv export - Wrap cell content in " [...] " in order to keep line breaks and other special chars - Replace any " with "" as " needs to be escaped in csv --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index d42468f47e..42350de4c2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5995,8 +5995,9 @@ namespace Avalonia.Controls var numberOfItem = clipboardRowContent.Count; for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++) { - var cellContent = clipboardRowContent[cellIndex]; - text.Append(cellContent.Content); + var cellContent = clipboardRowContent[cellIndex].Content?.ToString(); + cellContent = cellContent?.Replace("\"", "\"\""); + text.Append($"\"{cellContent}\""); if (cellIndex < numberOfItem - 1) { text.Append('\t'); From 16cb38f60d8296f4eb6f664a9b5eefe42a468f7e Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 8 Mar 2023 19:43:17 +0000 Subject: [PATCH 2/3] add new IStorage api --- .../Properties/AndroidManifest.xml | 2 + .../Avalonia.Android/Avalonia.Android.csproj | 1 + .../Platform/Storage/AndroidStorageItem.cs | 232 +++++++++++++++++- .../Platform/Storage/FileIO/BclStorageFile.cs | 18 ++ .../Storage/FileIO/BclStorageFolder.cs | 35 +++ .../Platform/Storage/IStorageFolder.cs | 14 ++ .../Platform/Storage/IStorageItem.cs | 13 + 7 files changed, 302 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml index 6f551d2b01..ec07a94b22 100644 --- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml +++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml @@ -2,4 +2,6 @@ + + diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 2533016e9f..d8b0c3d534 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 8052b3911a..2dc0fa774b 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -8,7 +8,10 @@ using System.Threading.Tasks; using Android; using Android.App; using Android.Content; +using Android.OS; using Android.Provider; +using Android.Webkit; +using AndroidX.DocumentFile.Provider; using Avalonia.Logging; using Avalonia.Platform.Storage; using Java.Lang; @@ -22,20 +25,25 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem { private Activity? _activity; private readonly bool _needsExternalFilesPermission; + private readonly AndroidStorageFolder? _parent; + private readonly AndroidUri? _permissionRoot; - protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExternalFilesPermission) + protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExternalFilesPermission, AndroidStorageFolder? parent = null, AndroidUri? permissionRoot = null) { _activity = activity; _needsExternalFilesPermission = needsExternalFilesPermission; + _parent = parent; + _permissionRoot = permissionRoot ?? parent?.Uri ?? Uri; Uri = uri; } - internal AndroidUri Uri { get; } + internal AndroidUri Uri { get; set; } protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem)); public virtual string Name => GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName) - ?? Uri.PathSegments?.LastOrDefault() ?? string.Empty; + ?? Document?.Name + ?? Uri.PathSegments?.LastOrDefault()?.Split("/", StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? string.Empty; public Uri Path => new(Uri.ToString()!); @@ -92,6 +100,23 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem return null; } + if(_parent != null) + { + 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. @@ -118,12 +143,88 @@ 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 MoveAsync(IStorageFolder destination); } internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder { - public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExternalFilesPermission) : base(activity, uri, needsExternalFilesPermission) + public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExternalFilesPermission, AndroidStorageFolder? parent = null, AndroidUri? permissionRoot = null) : base(activity, uri, needsExternalFilesPermission, parent, permissionRoot) + { + } + + public async Task CreateFile(string name) + { + var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream"; + var newFile = Document.CreateFile(mimeType, name); + + if(newFile == null) + { + return null; + } + + return new AndroidStorageFile(Activity, newFile.Uri, this); + } + + public async Task CreateFolder(string name) { + var newFolder = Document?.CreateDirectory(name); + + if (newFolder == null) + { + return null; + } + + return new AndroidStorageFolder(Activity, newFolder.Uri, false, this, PermissionRoot); + } + + public override async Task DeleteAsync() + { + if (!await EnsureExternalFilesPermission(false)) + { + return; + } + + if (Activity != null) + { + await DeleteContents(this); + } + + async Task DeleteContents(AndroidStorageFolder storageFolder) + { + foreach (var file in storageFolder.GetItemsAsync()) + { + if(file is AndroidStorageFolder folder) + { + await DeleteContents(folder); + } + else if(file is AndroidStorageFile storageFile) + { + await storageFile.DeleteAsync(); + } + } + + DocumentFile.FromTreeUri(Activity, storageFolder.Uri)?.Delete(); + } } public override Task GetBasicPropertiesAsync() @@ -137,14 +238,16 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder { yield break; } - + var contentResolver = Activity.ContentResolver; if (contentResolver == null) { yield break; } - var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(Uri!, DocumentsContract.GetTreeDocumentId(Uri)); + var root = PermissionRoot ?? Uri; + var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri); + var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId); var projection = new[] { @@ -160,17 +263,55 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder { var mime = cursor.GetString(1); var id = cursor.GetString(0); - var uri = DocumentsContract.BuildDocumentUriUsingTree(Uri!, id); + + bool isDirectory = mime == DocumentsContract.Document.MimeTypeDir; + var uri = DocumentsContract.BuildDocumentUriUsingTree(root, id); + if (uri == null) { continue; } + yield return isDirectory ? new AndroidStorageFolder(Activity, uri, false, this, root) : + new AndroidStorageFile(Activity, uri, this, root); + } + } + } - yield return mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) : - new AndroidStorageFile(Activity, uri); + public override async Task MoveAsync(IStorageFolder destination) + { + if (Activity != null) + { + return await MoveRecursively(this, (AndroidStorageFolder)destination); + } + + return null; + + async Task MoveRecursively(AndroidStorageFolder storageFolder, AndroidStorageFolder destination) + { + destination = await destination.CreateFolder(storageFolder.Name) as AndroidStorageFolder; + + if (destination == null) + { + return null; + } + + foreach (var file in storageFolder.GetItemsAsync()) + { + if (file is AndroidStorageFolder folder) + { + await MoveRecursively(folder, destination); + } + else if (file is AndroidStorageFile) + { + await file.MoveAsync(destination); } + } + + await storageFolder.DeleteAsync(); + + return destination; } - } + } } internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder @@ -182,14 +323,14 @@ internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder } public override string Name { get; } -} +} internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile { - public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false) + public AndroidStorageFile(Activity activity, AndroidUri uri, AndroidStorageFolder? parent = null, AndroidUri? permissionRoot = null) : base(activity, uri, false, parent, permissionRoot) { } - + public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false) ?? throw new InvalidOperationException("Failed to open content stream")); @@ -313,4 +454,69 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF return Task.FromResult(new StorageItemProperties(size, itemDate, dateModified)); } + + public override async Task DeleteAsync() + { + if (!await EnsureExternalFilesPermission(false)) + { + return; + } + + if (Activity != null) + { + DocumentsContract.DeleteDocument(Activity.ContentResolver!, Uri); + } + } + + public override async Task MoveAsync(IStorageFolder destination) + { + if (!await EnsureExternalFilesPermission(false)) + { + return null; + } + + if (Activity != null && destination is AndroidStorageFolder storageFolder) + { + if (Build.VERSION.SdkInt >= BuildVersionCodes.N) + { + try + { + var uri = DocumentsContract.MoveDocument(Activity.ContentResolver!, Uri, ((await GetParentAsync()) as AndroidStorageFolder)!.Uri, storageFolder.Document!.Uri); + + return new AndroidStorageFile(Activity, uri, storageFolder); + } + catch (Exception ex) + { + // There are many reason why DocumentContract will fail to move a file. We fallback to copying. + return await MoveFileByCopy(); + + } + } + else + { + return await MoveFileByCopy(); + } + } + + async Task MoveFileByCopy() + { + var newFile = await storageFolder.CreateFile(Name) as AndroidStorageFile; + + if (newFile != null) + { + using var input = await OpenReadAsync(); + using var output = await newFile.OpenWriteAsync(); + + await input.CopyToAsync(output); + + await DeleteAsync(); + + return new AndroidStorageFile(Activity, newFile.Uri, storageFolder); + } + + return null; + } + + return null; + } } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 543fb0ab74..f9f02ec339 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -92,4 +92,22 @@ internal class BclStorageFile : IStorageBookmarkFile Dispose(disposing: true); GC.SuppressFinalize(this); } + + public async Task DeleteAsync() + { + FileInfo.Delete(); + } + + public async Task MoveAsync(IStorageFolder destination) + { + if (destination is BclStorageFolder storageFolder) + { + var newPath = System.IO.Path.Combine(storageFolder.DirectoryInfo.FullName, FileInfo.Name); + FileInfo.MoveTo(newPath); + + return new BclStorageFile(new FileInfo(newPath)); + } + + return null; + } } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index e6551390d6..b9e74104c8 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -94,4 +94,39 @@ internal class BclStorageFolder : IStorageBookmarkFolder Dispose(disposing: true); GC.SuppressFinalize(this); } + + public async Task DeleteAsync() + { + DirectoryInfo.Delete(true); + } + + public async Task MoveAsync(IStorageFolder destination) + { + if (destination is BclStorageFolder storageFolder) + { + var newPath = System.IO.Path.Combine(storageFolder.DirectoryInfo.FullName, DirectoryInfo.Name); + DirectoryInfo.MoveTo(newPath); + + return new BclStorageFolder(new DirectoryInfo(newPath)); + } + + return null; + } + + public async Task CreateFile(string name) + { + var fileName = System.IO.Path.Combine(DirectoryInfo.FullName, name); + var newFile = new FileInfo(fileName); + + using var stream = newFile.Create(); + + return new BclStorageFile(newFile); + } + + public async Task CreateFolder(string name) + { + var newFolder = DirectoryInfo.CreateSubdirectory(name); + + return new BclStorageFolder(newFolder); + } } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 52b3256387..61feeba79a 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -17,4 +17,18 @@ public interface IStorageFolder : IStorageItem /// When this method completes successfully, it returns a list of the files and folders in the current folder. Each item in the list is represented by an implementation object. /// IAsyncEnumerable GetItemsAsync(); + + /// + /// Creates a file with specified name as a child of the current storage folder + /// + /// The display name + /// A new pointing to the moved file. If not null, the current storage item becomes invalid + Task CreateFile(string name); + + /// + /// Creates a folder with specified name as a child of the current storage folder + /// + /// The display name + /// A new pointing to the moved file. If not null, the current storage item becomes invalid + Task CreateFolder(string name); } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs index c2c93400b0..b5873fdb27 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -50,4 +50,17 @@ public interface IStorageItem : IDisposable /// Gets the parent folder of the current storage item. /// Task GetParentAsync(); + + /// + /// Deletes the current storage item and it's contents + /// + /// + Task DeleteAsync(); + + /// + /// Moves the current storage item and it's contents to a + /// + /// The to move the item into + /// + Task MoveAsync(IStorageFolder destination); } From 2d2fcf94acb23678d5d087e4e2d647e22c6f5f78 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 9 Mar 2023 13:06:38 +0000 Subject: [PATCH 3/3] add async suffix --- .../Platform/Storage/AndroidStorageItem.cs | 33 +++++++++++-------- .../Storage/FileIO/BclStorageFolder.cs | 4 +-- .../Platform/Storage/IStorageFolder.cs | 4 +-- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 2dc0fa774b..f0c22d7f38 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -172,7 +172,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder { } - public async Task CreateFile(string name) + public async Task CreateFileAsync(string name) { var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream"; var newFile = Document.CreateFile(mimeType, name); @@ -185,7 +185,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder return new AndroidStorageFile(Activity, newFile.Uri, this); } - public async Task CreateFolder(string name) + public async Task CreateFolderAsync(string name) { var newFolder = Document?.CreateDirectory(name); @@ -211,7 +211,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder async Task DeleteContents(AndroidStorageFolder storageFolder) { - foreach (var file in storageFolder.GetItemsAsync()) + await foreach (var file in storageFolder.GetItemsAsync()) { if(file is AndroidStorageFolder folder) { @@ -288,14 +288,14 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder async Task MoveRecursively(AndroidStorageFolder storageFolder, AndroidStorageFolder destination) { - destination = await destination.CreateFolder(storageFolder.Name) as AndroidStorageFolder; + destination = await destination.CreateFolderAsync(storageFolder.Name) as AndroidStorageFolder; if (destination == null) { return null; } - foreach (var file in storageFolder.GetItemsAsync()) + await foreach (var file in storageFolder.GetItemsAsync()) { if (file is AndroidStorageFolder folder) { @@ -498,20 +498,27 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF } } - async Task MoveFileByCopy() + async Task MoveFileByCopy() { - var newFile = await storageFolder.CreateFile(Name) as AndroidStorageFile; + var newFile = await storageFolder.CreateFileAsync(Name) as AndroidStorageFile; - if (newFile != null) + try { - using var input = await OpenReadAsync(); - using var output = await newFile.OpenWriteAsync(); + if (newFile != null) + { + using var input = await OpenReadAsync(); + using var output = await newFile.OpenWriteAsync(); - await input.CopyToAsync(output); + await input.CopyToAsync(output); - await DeleteAsync(); + await DeleteAsync(); - return new AndroidStorageFile(Activity, newFile.Uri, storageFolder); + return new AndroidStorageFile(Activity, newFile.Uri, storageFolder); + } + } + catch + { + newFile?.DeleteAsync(); } return null; diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index b9e74104c8..8c657fb787 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -113,7 +113,7 @@ internal class BclStorageFolder : IStorageBookmarkFolder return null; } - public async Task CreateFile(string name) + public async Task CreateFileAsync(string name) { var fileName = System.IO.Path.Combine(DirectoryInfo.FullName, name); var newFile = new FileInfo(fileName); @@ -123,7 +123,7 @@ internal class BclStorageFolder : IStorageBookmarkFolder return new BclStorageFile(newFile); } - public async Task CreateFolder(string name) + public async Task CreateFolderAsync(string name) { var newFolder = DirectoryInfo.CreateSubdirectory(name); diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 61feeba79a..a9d1ff3669 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -23,12 +23,12 @@ public interface IStorageFolder : IStorageItem /// /// The display name /// A new pointing to the moved file. If not null, the current storage item becomes invalid - Task CreateFile(string name); + Task CreateFileAsync(string name); /// /// Creates a folder with specified name as a child of the current storage folder /// /// The display name /// A new pointing to the moved file. If not null, the current storage item becomes invalid - Task CreateFolder(string name); + Task CreateFolderAsync(string name); }