Skip to content

Commit

Permalink
feat: Android SAF StorageFolder/File operations
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinZikmund committed Mar 16, 2021
1 parent 4493c51 commit 1eb6b53
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
#pragma warning disable 114 // new keyword hiding
namespace Windows.Storage.Streams
{
#if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
#if false
[global::Uno.NotImplemented]
#endif
public partial interface IInputStreamReference
{
#if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
#if false
global::Windows.Foundation.IAsyncOperation<global::Windows.Storage.Streams.IInputStream> OpenSequentialReadAsync();
#endif
}
Expand Down
62 changes: 62 additions & 0 deletions src/Uno.UWP/Storage/Internal/SafHelpers.Android.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Android.App;
using Android.Provider;
using Windows.Storage.FileProperties;

namespace Uno.Storage.Internal
{
/// <summary>
/// Shared functionality handling for Android's Storage
/// Access Framework-based StorageFiles and StorageFolders.
/// </summary>
internal static class SafHelpers
{
/// <summary>
/// Retrieves basic properties for a given SAF Uri.
/// </summary>
/// <param name="safUri">SAF Uri.</param>
/// <param name="includeSize">A value indicating whether the size should be included (not useful for folders).</param>
/// <param name="token">Cancellation token.</param>
/// <returns>Basic properties.</returns>
public static async Task<BasicProperties> GetBasicPropertiesAsync(Android.Net.Uri safUri, bool includeSize, CancellationToken token)
{
if (Application.Context.ContentResolver == null)
{
throw new InvalidOperationException("Content resolver for app is not available.");
}

return await Task.Run(() =>
{
var cursor = Application.Context.ContentResolver.Query(safUri, null, null, null, null, null);
if (cursor == null)
{
throw new UnauthorizedAccessException("Cannot access the folder.");
}
using (cursor)
{
if (cursor.MoveToFirst())
{
int size = 0;
var sizeColumnIndex = cursor.GetColumnIndex(DocumentsContract.Document.ColumnSize);
if (includeSize && sizeColumnIndex >= 0)
{
size = cursor.GetInt(sizeColumnIndex);
}
var lastModified = DateTimeOffset.MinValue;
var lastModifiedIndex = cursor.GetColumnIndex(DocumentsContract.Document.ColumnLastModified);
if (lastModifiedIndex >= 0)
{
var lastModifiedTimestamp = cursor.GetLong(lastModifiedIndex);
lastModified = DateTimeOffset.FromUnixTimeMilliseconds(lastModifiedTimestamp);
}
return new BasicProperties((ulong)size, lastModified);
}
}
return new BasicProperties(0, DateTimeOffset.MinValue);
}, token);
}
}
}
2 changes: 1 addition & 1 deletion src/Uno.UWP/Storage/StorageFile.Base.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void InitOwner(StorageFile owner)

public virtual string Path { get; protected set; } = string.Empty;

public virtual string FileType => global::System.IO.Path.GetExtension(Path);
public virtual string FileType => global::System.IO.Path.GetExtension(Name);

public virtual string Name => global::System.IO.Path.GetFileName(Path);

Expand Down
55 changes: 44 additions & 11 deletions src/Uno.UWP/Storage/StorageFile.saf.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
using System.Threading.Tasks;
using Android.App;
using AndroidX.DocumentFile.Provider;
using Uno.Storage.Internal;
using Windows.Storage.FileProperties;
using Windows.Storage.Streams;

namespace Windows.Storage
{
public partial class StorageFile
{
public partial class StorageFile
{
public static StorageFile GetFromSafDocument(DocumentFile document) =>
new StorageFile(new SafFile(document));

Expand All @@ -25,19 +26,19 @@ internal class SafFile : ImplementationBase
{
private static readonly StorageProvider _provider = new StorageProvider("Android.StorageAccessFramework", "Android Storage Access Framework");

private readonly Android.Net.Uri _folderUri;
private readonly Android.Net.Uri _fileUri;
private readonly DocumentFile _directoryDocument;

internal SafFile(Android.Net.Uri uri)
{
_folderUri = uri ?? throw new ArgumentNullException(nameof(uri));
_directoryDocument = DocumentFile.FromTreeUri(Application.Context, uri);
_fileUri = uri ?? throw new ArgumentNullException(nameof(uri));
_directoryDocument = DocumentFile.FromSingleUri(Application.Context, uri);
}

internal SafFile(DocumentFile directoryDocument)
{
_directoryDocument = directoryDocument ?? throw new ArgumentNullException(nameof(directoryDocument));
_folderUri = _directoryDocument.Uri;
_fileUri = _directoryDocument.Uri;
}

public override StorageProvider Provider => _provider;
Expand All @@ -47,12 +48,44 @@ internal SafFile(DocumentFile directoryDocument)

public override DateTimeOffset DateCreated => throw new NotImplementedException();

public override Task DeleteAsync(CancellationToken ct, StorageDeleteOption options) => throw new NotImplementedException();
public override Task<BasicProperties> GetBasicPropertiesAsync(CancellationToken ct) => throw new NotImplementedException();
public override Task<StorageFolder?> GetParentAsync(CancellationToken ct) => throw new NotImplementedException();
public override Task<IRandomAccessStreamWithContentType> OpenAsync(CancellationToken ct, FileAccessMode accessMode, StorageOpenOptions options) => throw new NotImplementedException();
public override async Task DeleteAsync(CancellationToken ct, StorageDeleteOption options)
{
await Task.Run(() =>
{
_directoryDocument.Delete();
}, ct);
}

public override Task<BasicProperties> GetBasicPropertiesAsync(CancellationToken ct) =>
SafHelpers.GetBasicPropertiesAsync(_fileUri, true, ct);

public override Task<StorageFolder?> GetParentAsync(CancellationToken ct)
{
if (_directoryDocument.ParentFile == null)
{
return Task.FromResult<StorageFolder?>(null);
}

var parentFolder = StorageFolder.GetFromSafDocument(_directoryDocument.ParentFile);
return Task.FromResult<StorageFolder?>(parentFolder);
}

public override Task<IRandomAccessStreamWithContentType> OpenAsync(CancellationToken ct, FileAccessMode accessMode, StorageOpenOptions options) =>
Task.FromResult<IRandomAccessStreamWithContentType>(new RandomAccessStreamWithContentType(FileRandomAccessStream.CreateFromSafUri(_fileUri, accessMode), ContentType));

public override Task<StorageStreamTransaction> OpenTransactedWriteAsync(CancellationToken ct, StorageOpenOptions option) => throw new NotImplementedException();
protected override bool IsEqual(ImplementationBase implementation) => throw new NotImplementedException();

protected override bool IsEqual(ImplementationBase implementation)
{
if (implementation is SafFile otherFile)
{
var path = _directoryDocument.Uri?.ToString() ?? string.Empty;
var otherPath = otherFile._directoryDocument.Uri?.ToString() ?? string.Empty;
return path.Equals(otherPath, StringComparison.InvariantCulture);
}

return false;
}
}
}
}
Loading

0 comments on commit 1eb6b53

Please sign in to comment.