-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
455 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
244 changes: 244 additions & 0 deletions
244
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
#nullable enable | ||
|
||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Android.Content; | ||
using Android.Provider; | ||
using Avalonia.Logging; | ||
using Avalonia.Platform.Storage; | ||
using Java.Lang; | ||
using AndroidUri = Android.Net.Uri; | ||
using Exception = System.Exception; | ||
using JavaFile = Java.IO.File; | ||
|
||
namespace Avalonia.Android.Platform.Storage; | ||
|
||
internal abstract class AndroidStorageItem : IStorageBookmarkItem | ||
{ | ||
private Context? _context; | ||
|
||
protected AndroidStorageItem(Context context, AndroidUri uri) | ||
{ | ||
_context = context; | ||
Uri = uri; | ||
} | ||
|
||
internal AndroidUri Uri { get; } | ||
|
||
protected Context Context => _context ?? throw new ObjectDisposedException(nameof(AndroidStorageItem)); | ||
|
||
public string Name => GetColumnValue(Context, Uri, MediaStore.IMediaColumns.DisplayName) | ||
?? Uri.PathSegments?.LastOrDefault() ?? string.Empty; | ||
|
||
public bool CanBookmark => true; | ||
|
||
public Task<string?> SaveBookmark() | ||
{ | ||
Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); | ||
return Task.FromResult(Uri.ToString()); | ||
} | ||
|
||
public Task ReleaseBookmark() | ||
{ | ||
Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); | ||
return Task.CompletedTask; | ||
} | ||
|
||
public bool TryGetUri([NotNullWhen(true)] out Uri? uri) | ||
{ | ||
uri = new Uri(Uri.ToString()!); | ||
return true; | ||
} | ||
|
||
public abstract Task<StorageItemProperties> GetBasicPropertiesAsync(); | ||
|
||
protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null) | ||
{ | ||
try | ||
{ | ||
var projection = new[] { column }; | ||
using var cursor = context.ContentResolver!.Query(contentUri, projection, selection, selectionArgs, null); | ||
if (cursor?.MoveToFirst() == true) | ||
{ | ||
var columnIndex = cursor.GetColumnIndex(column); | ||
if (columnIndex != -1) | ||
return cursor.GetString(columnIndex); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(this, "File metadata reader failed: '{Exception}'", ex); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public Task<IStorageFolder?> GetParentAsync() | ||
{ | ||
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) | ||
{ | ||
return Task.FromResult<IStorageFolder?>(new AndroidStorageFolder(Context, androidUri)); | ||
} | ||
|
||
return Task.FromResult<IStorageFolder?>(null); | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
_context = null; | ||
} | ||
} | ||
|
||
internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder | ||
{ | ||
public AndroidStorageFolder(Context context, AndroidUri uri) : base(context, uri) | ||
{ | ||
} | ||
|
||
public override Task<StorageItemProperties> GetBasicPropertiesAsync() | ||
{ | ||
return Task.FromResult(new StorageItemProperties()); | ||
} | ||
} | ||
|
||
internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile | ||
{ | ||
public AndroidStorageFile(Context context, AndroidUri uri) : base(context, uri) | ||
{ | ||
} | ||
|
||
public bool CanOpenRead => true; | ||
|
||
public bool CanOpenWrite => true; | ||
|
||
public Task<Stream> OpenRead() => Task.FromResult(OpenContentStream(Context, Uri, false) | ||
?? throw new InvalidOperationException("Failed to open content stream")); | ||
|
||
public Task<Stream> OpenWrite() => Task.FromResult(OpenContentStream(Context, Uri, true) | ||
?? throw new InvalidOperationException("Failed to open content stream")); | ||
|
||
private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) | ||
{ | ||
var isVirtual = IsVirtualFile(context, uri); | ||
if (isVirtual) | ||
{ | ||
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(this, "Content URI was virtual: '{Uri}'", uri); | ||
return GetVirtualFileStream(context, uri, isOutput); | ||
} | ||
|
||
return isOutput | ||
? context.ContentResolver?.OpenOutputStream(uri) | ||
: context.ContentResolver?.OpenInputStream(uri); | ||
} | ||
|
||
private bool IsVirtualFile(Context context, AndroidUri uri) | ||
{ | ||
if (!DocumentsContract.IsDocumentUri(context, uri)) | ||
return false; | ||
|
||
var value = GetColumnValue(context, uri, DocumentsContract.Document.ColumnFlags); | ||
if (!string.IsNullOrEmpty(value) && int.TryParse(value, out var flagsInt)) | ||
{ | ||
var flags = (DocumentContractFlags)flagsInt; | ||
return flags.HasFlag(DocumentContractFlags.VirtualDocument); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private Stream? GetVirtualFileStream(Context context, AndroidUri uri, bool isOutput) | ||
{ | ||
var mimeTypes = context.ContentResolver?.GetStreamTypes(uri, FilePickerFileTypes.All.MimeTypes![0]); | ||
if (mimeTypes?.Length >= 1) | ||
{ | ||
var mimeType = mimeTypes[0]; | ||
var asset = context.ContentResolver! | ||
.OpenTypedAssetFileDescriptor(uri, mimeType, null); | ||
|
||
var stream = isOutput | ||
? asset?.CreateOutputStream() | ||
: asset?.CreateInputStream(); | ||
|
||
return stream; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public override Task<StorageItemProperties> GetBasicPropertiesAsync() | ||
{ | ||
ulong? size = null; | ||
DateTimeOffset? itemDate = null; | ||
DateTimeOffset? dateModified = null; | ||
|
||
try | ||
{ | ||
var projection = new[] | ||
{ | ||
MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded, | ||
MediaStore.IMediaColumns.DateModified | ||
}; | ||
using var cursor = Context.ContentResolver!.Query(Uri, projection, null, null, null); | ||
|
||
if (cursor?.MoveToFirst() == true) | ||
{ | ||
try | ||
{ | ||
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.Size); | ||
if (columnIndex != -1) | ||
{ | ||
size = (ulong)cursor.GetLong(columnIndex); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? | ||
.Log(this, "File Size metadata reader failed: '{Exception}'", ex); | ||
} | ||
|
||
try | ||
{ | ||
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.DateAdded); | ||
if (columnIndex != -1) | ||
{ | ||
var longValue = cursor.GetLong(columnIndex); | ||
itemDate = longValue > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(longValue) : null; | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? | ||
.Log(this, "File DateAdded metadata reader failed: '{Exception}'", ex); | ||
} | ||
|
||
try | ||
{ | ||
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.DateModified); | ||
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, "File DateAdded metadata reader failed: '{Exception}'", ex); | ||
} | ||
} | ||
} | ||
catch (UnsupportedOperationException) | ||
{ | ||
// It's not possible to get parameters of some files/folders. | ||
} | ||
|
||
return Task.FromResult(new StorageItemProperties(size, itemDate, dateModified)); | ||
} | ||
} |
Oops, something went wrong.