@ -2,10 +2,11 @@
using System ;
using System.Collections.Generic ;
using System.Diagnostics.CodeAnalysis ;
using System.IO ;
using System.Linq ;
using System.Threading.Tasks ;
using Android ;
using Android.App ;
using Android.Content ;
using Android.Provider ;
using Avalonia.Logging ;
@ -19,41 +20,48 @@ namespace Avalonia.Android.Platform.Storage;
internal abstract class AndroidStorageItem : IStorageBookmarkItem
{
private Context ? _ context ;
private Activity ? _ activity ;
private readonly bool _ needsExternalFilesPermission ;
protected AndroidStorageItem ( Context context , AndroidUri uri )
protected AndroidStorageItem ( Activity activity , AndroidUri uri , bool needsExternalFilesPermission )
{
_ context = context ;
_ activity = activity ;
_ needsExternalFilesPermission = needsExternalFilesPermission ;
Uri = uri ;
}
internal AndroidUri Uri { get ; }
protected Activity Activity = > _ activity ? ? throw new ObjectDisposedException ( nameof ( AndroidStorageItem ) ) ;
protected Context Context = > _ context ? ? throw new ObjectDisposedException ( nameof ( AndroidStorageItem ) ) ;
public string Name = > GetColumnValue ( Context , Uri , MediaStore . IMediaColumns . DisplayName )
public virtual string Name = > GetColumnValue ( Activity , Uri , MediaStore . IMediaColumns . DisplayName )
? ? Uri . PathSegments ? . LastOrDefault ( ) ? ? string . Empty ;
public Uri Path = > new ( Uri . ToString ( ) ! ) ;
public bool CanBookmark = > true ;
public Task < string? > SaveBookmarkAsync ( )
public async Task < string? > SaveBookmarkAsync ( )
{
Context . ContentResolver ? . TakePersistableUriPermission ( Uri , ActivityFlags . GrantWriteUriPermission | ActivityFlags . GrantReadUriPermission ) ;
return Task . FromResult ( Uri . ToString ( ) ) ;
}
if ( ! await EnsureExternalFilesPermission ( false ) )
{
return null ;
}
public Task ReleaseBookmarkAsync ( )
{
Context . ContentResolver ? . ReleasePersistableUriPermission ( Uri , ActivityFlags . GrantWriteUriPermission | ActivityFlags . GrantReadUriPermission ) ;
return Task . CompletedTask ;
Activity . ContentResolver ? . TakePersistableUriPermission ( Uri , ActivityFlags . GrantWriteUriPermission | ActivityFlags . GrantReadUriPermission ) ;
return Uri . ToString ( ) ;
}
public bool TryGetUri ( [ NotNullWhen ( true ) ] out Uri ? uri )
public async Task ReleaseBookmarkAsync ( )
{
uri = new Uri ( Uri . ToString ( ) ! ) ;
return true ;
}
if ( ! await EnsureExternalFilesPermission ( false ) )
{
return ;
}
Activity . ContentResolver ? . ReleasePersistableUriPermission ( Uri , ActivityFlags . GrantWriteUriPermission | ActivityFlags . GrantReadUriPermission ) ;
}
public abstract Task < StorageItemProperties > GetBasicPropertiesAsync ( ) ;
protected string? GetColumnValue ( Context context , AndroidUri contentUri , string column , string? selection = null , string [ ] ? selectionArgs = null )
@ -77,29 +85,44 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
return null ;
}
public Task < IStorageFolder ? > GetParentAsync ( )
public async Task < IStorageFolder ? > GetParentAsync ( )
{
if ( ! await EnsureExternalFilesPermission ( false ) )
{
return null ;
}
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 new AndroidStorageFolder ( Activity , androidUri , false ) ;
}
return Task . FromResult < IStorageFolder ? > ( null ) ;
return null ;
}
protected async Task < bool > EnsureExternalFilesPermission ( bool write )
{
if ( ! _ needsExternalFilesPermission )
{
return true ;
}
return await _ activity . CheckPermission ( Manifest . Permission . ReadExternalStorage ) ;
}
public void Dispose ( )
{
_ context = null ;
_ activity = null ;
}
}
internal sealed class AndroidStorageFolder : AndroidStorageItem , IStorageBookmarkFolder
internal class AndroidStorageFolder : AndroidStorageItem , IStorageBookmarkFolder
{
public AndroidStorageFolder ( Context context , AndroidUri uri ) : base ( context , uri )
public AndroidStorageFolder ( Activity activity , AndroidUri uri , bool needsExternalFilesPermission ) : base ( activity , uri , needsExternalFilesPermission )
{
}
@ -110,6 +133,11 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
public async Task < IReadOnlyList < IStorageItem > > GetItemsAsync ( )
{
if ( ! await EnsureExternalFilesPermission ( false ) )
{
return Array . Empty < IStorageItem > ( ) ;
}
using var javaFile = new JavaFile ( Uri . Path ! ) ;
// Java file represents files AND directories. Don't be confused.
@ -124,8 +152,8 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
. Where ( t = > t . uri is not null )
. Select ( t = > t . file switch
{
{ IsFile : true } = > ( IStorageItem ) new AndroidStorageFile ( Context , t . uri ! ) ,
{ IsDirectory : true } = > new AndroidStorageFolder ( Context , t . uri ! ) ,
{ IsFile : true } = > ( IStorageItem ) new AndroidStorageFile ( Activity , t . uri ! ) ,
{ IsDirectory : true } = > new AndroidStorageFolder ( Activity , t . uri ! , false ) ,
_ = > null
} )
. Where ( i = > i is not null )
@ -133,9 +161,20 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
}
}
internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder
{
public WellKnownAndroidStorageFolder ( Activity activity , string identifier , AndroidUri uri , bool needsExternalFilesPermission )
: base ( activity , uri , needsExternalFilesPermission )
{
Name = identifier ;
}
public override string Name { get ; }
}
internal sealed class AndroidStorageFile : AndroidStorageItem , IStorageBookmarkFile
{
public AndroidStorageFile ( Context context , AndroidUri uri ) : base ( context , uri )
public AndroidStorageFile ( Activity activity , AndroidUri uri ) : base ( activity , uri , false )
{
}
@ -143,10 +182,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
public bool CanOpenWrite = > true ;
public Task < Stream > OpenReadAsync ( ) = > Task . FromResult ( OpenContentStream ( Context , Uri , false )
public Task < Stream > OpenReadAsync ( ) = > Task . FromResult ( OpenContentStream ( Activity , Uri , false )
? ? throw new InvalidOperationException ( "Failed to open content stream" ) ) ;
public Task < Stream > OpenWriteAsync ( ) = > Task . FromResult ( OpenContentStream ( Context , Uri , true )
public Task < Stream > OpenWriteAsync ( ) = > Task . FromResult ( OpenContentStream ( Activity , Uri , true )
? ? throw new InvalidOperationException ( "Failed to open content stream" ) ) ;
private Stream ? OpenContentStream ( Context context , AndroidUri uri , bool isOutput )
@ -210,7 +249,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
MediaStore . IMediaColumns . Size , MediaStore . IMediaColumns . DateAdded ,
MediaStore . IMediaColumns . DateModified
} ;
using var cursor = Context . ContentResolver ! . Query ( Uri , projection , null , null , null ) ;
using var cursor = Activity . ContentResolver ! . Query ( Uri , projection , null , null , null ) ;
if ( cursor ? . MoveToFirst ( ) = = true )
{