From 14b591b488ff9f6a873ac85146c03f38cbf61fc6 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 19 Mar 2026 11:02:17 +0000 Subject: [PATCH] Android - Query date modified for android storage folder (#20860) * query date modified for android storage folder * fix comments * Update src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Platform/Storage/AndroidStorageItem.cs | 119 +++++++++++++----- 1 file changed, 86 insertions(+), 33 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 47d95d8da1..59ec332b2d 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -12,6 +12,7 @@ using Avalonia.Logging; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; using Java.Lang; +using static Android.Provider.DocumentsContract; using AndroidUri = Android.Net.Uri; using Exception = System.Exception; using JavaFile = Java.IO.File; @@ -35,10 +36,10 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem } internal AndroidUri Uri { get; set; } - + protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem)); - public virtual string Name => GetColumnValue(Activity, Uri, DocumentsContract.Document.ColumnDisplayName) + public virtual string Name => GetColumnValue(Activity, Uri, Document.ColumnDisplayName) ?? GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName) ?? Uri.PathSegments?.LastOrDefault()?.Split("/", StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? string.Empty; @@ -67,7 +68,7 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem Activity.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); } - + public abstract Task GetBasicPropertiesAsync(); protected static string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null) @@ -98,7 +99,7 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem return null; } - if(_parent != null) + if (_parent != null) { return _parent; } @@ -106,8 +107,8 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem using var javaFile = new JavaFile(Uri.Path!); // Java file represents files AND directories. Don't be confused. - if (javaFile.ParentFile is {} parentFile - && AndroidUri.FromFile(parentFile) is {} androidUri) + if (javaFile.ParentFile is { } parentFile + && AndroidUri.FromFile(parentFile) is { } androidUri) { return new AndroidStorageFolder(Activity, androidUri, false); } @@ -124,12 +125,12 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem return await _activity!.CheckPermission(Manifest.Permission.ReadExternalStorage); } - + public void Dispose() { _activity = null; } - + internal AndroidUri? PermissionRoot => _permissionRoot; public abstract Task DeleteAsync(); @@ -138,8 +139,8 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem public static IStorageItem CreateItem(Activity activity, AndroidUri uri) { - var mimeType = GetColumnValue(activity, uri, DocumentsContract.Document.ColumnMimeType); - if (mimeType == DocumentsContract.Document.MimeTypeDir) + var mimeType = GetColumnValue(activity, uri, Document.ColumnMimeType); + if (mimeType == Document.MimeTypeDir) { return new AndroidStorageFolder(activity, uri, false); } @@ -160,8 +161,8 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder { var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream"; var treeUri = GetTreeUri().treeUri; - var newFile = DocumentsContract.CreateDocument(Activity.ContentResolver!, treeUri!, mimeType, name); - if(newFile == null) + var newFile = CreateDocument(Activity.ContentResolver!, treeUri!, mimeType, name); + if (newFile == null) { return Task.FromResult(null); } @@ -172,7 +173,7 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder public Task CreateFolderAsync(string name) { var treeUri = GetTreeUri().treeUri; - var newFolder = DocumentsContract.CreateDocument(Activity.ContentResolver!, treeUri!, DocumentsContract.Document.MimeTypeDir, name); + var newFolder = CreateDocument(Activity.ContentResolver!, treeUri!, Document.MimeTypeDir, name); if (newFolder == null) { return Task.FromResult(null); @@ -197,24 +198,76 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder { await foreach (var file in storageFolder.GetItemsAsync()) { - if(file is AndroidStorageFolder folder) + if (file is AndroidStorageFolder folder) { await DeleteContents(folder); } - else if(file is AndroidStorageFile storageFile) + else if (file is AndroidStorageFile storageFile) { await storageFile.DeleteAsync(); } } var treeUri = GetTreeUri().treeUri; - DocumentsContract.DeleteDocument(Activity.ContentResolver!, treeUri!); + DeleteDocument(Activity.ContentResolver!, treeUri!); } } public override Task GetBasicPropertiesAsync() { - return Task.FromResult(new StorageItemProperties()); + DateTimeOffset? dateModified = null; + + AndroidUri? queryUri = null; + + try + { + try + { + // When Uri is a tree URI, use its document id to build a document URI. + var folderId = GetTreeDocumentId(Uri); + queryUri = BuildDocumentUriUsingTree(Uri, folderId); + } + catch (UnsupportedOperationException) + { + // For non-root items, Uri may already be a document URI; use it directly. + queryUri = Uri; + } + + if (queryUri != null) + { + var projection = new[] + { + Document.ColumnLastModified + }; + using var cursor = Activity.ContentResolver!.Query(queryUri, projection, null, null, null); + + if (cursor?.MoveToFirst() == true) + { + try + { + var columnIndex = cursor.GetColumnIndex(Document.ColumnLastModified); + if (columnIndex != -1) + { + var longValue = cursor.GetLong(columnIndex); + dateModified = longValue > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(longValue) : null; + } + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "Directory LastModified metadata reader failed: '{Exception}'", ex); + } + } + } + } + catch (Exception ex) + { + // Data may not be available for this item or the URI may not be in the expected shape. + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "Directory basic properties metadata unavailable: '{Exception}'", ex); + } + + return Task.FromResult(new StorageItemProperties(null, null, dateModified)); } public async IAsyncEnumerable GetItemsAsync() @@ -234,8 +287,8 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder var projection = new[] { - DocumentsContract.Document.ColumnDocumentId, - DocumentsContract.Document.ColumnMimeType + Document.ColumnDocumentId, + Document.ColumnMimeType }; if (childrenUri != null) { @@ -247,8 +300,8 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder var mime = cursor.GetString(1); var id = cursor.GetString(0); - bool isDirectory = mime == DocumentsContract.Document.MimeTypeDir; - var uri = DocumentsContract.BuildDocumentUriUsingTree(root, id); + bool isDirectory = mime == Document.MimeTypeDir; + var uri = BuildDocumentUriUsingTree(root, id); if (uri == null) { @@ -313,9 +366,9 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder var projection = new[] { - DocumentsContract.Document.ColumnDocumentId, - DocumentsContract.Document.ColumnMimeType, - DocumentsContract.Document.ColumnDisplayName + Document.ColumnDocumentId, + Document.ColumnMimeType, + Document.ColumnDisplayName }; if (childrenUri != null) @@ -332,15 +385,15 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder if (fileName != name) { continue; - } + } - bool mineDirectory = mime == DocumentsContract.Document.MimeTypeDir; + bool mineDirectory = mime == Document.MimeTypeDir; if (isDirectory != mineDirectory) { return null; } - var uri = DocumentsContract.BuildDocumentUriUsingTree(root, id); + var uri = BuildDocumentUriUsingTree(root, id); if (uri == null) { return null; @@ -370,8 +423,8 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder private (AndroidUri root, AndroidUri? treeUri) GetTreeUri() { var root = PermissionRoot ?? Uri; - var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri); - return (root, DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId)); + var folderId = root != Uri ? GetDocumentId(Uri) : GetTreeDocumentId(Uri); + return (root, BuildChildDocumentsUriUsingTree(root, folderId)); } } @@ -419,10 +472,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF if (!OperatingSystem.IsAndroidVersionAtLeast(24)) return false; - if (!DocumentsContract.IsDocumentUri(context, uri)) + if (!IsDocumentUri(context, uri)) return false; - var value = GetColumnValue(context, uri, DocumentsContract.Document.ColumnFlags); + var value = GetColumnValue(context, uri, Document.ColumnFlags); if (!string.IsNullOrEmpty(value) && int.TryParse(value, out var flagsInt)) { var flags = (DocumentContractFlags)flagsInt; @@ -530,7 +583,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF if (Activity != null) { - DocumentsContract.DeleteDocument(Activity.ContentResolver!, Uri); + DeleteDocument(Activity.ContentResolver!, Uri); } } @@ -553,7 +606,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF storageFolder.Uri is { } targetParentUri && await GetParentAsync() is AndroidStorageFolder parentFolder) { - movedUri = DocumentsContract.MoveDocument(contentResolver, Uri, parentFolder.Uri, targetParentUri); + movedUri = MoveDocument(contentResolver, Uri, parentFolder.Uri, targetParentUri); } } catch (Exception)