AndroidStorageProvider.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Threading.Tasks;
  6. using Android.App;
  7. using Android.Content;
  8. using Android.Provider;
  9. using Avalonia.Platform.Storage;
  10. using AndroidUri = Android.Net.Uri;
  11. namespace Avalonia.Android.Platform.Storage;
  12. internal class AndroidStorageProvider : IStorageProvider
  13. {
  14. private readonly AvaloniaActivity _activity;
  15. private int _lastRequestCode = 20000;
  16. public AndroidStorageProvider(AvaloniaActivity activity)
  17. {
  18. _activity = activity;
  19. }
  20. public bool CanOpen => OperatingSystem.IsAndroidVersionAtLeast(19);
  21. public bool CanSave => OperatingSystem.IsAndroidVersionAtLeast(19);
  22. public bool CanPickFolder => OperatingSystem.IsAndroidVersionAtLeast(21);
  23. public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
  24. {
  25. var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
  26. return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri));
  27. }
  28. public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
  29. {
  30. var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
  31. return Task.FromResult<IStorageBookmarkFile?>(new AndroidStorageFile(_activity, uri));
  32. }
  33. public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
  34. {
  35. var mimeTypes = options.FileTypeFilter?.Where(t => t != FilePickerFileTypes.All)
  36. .SelectMany(f => f.MimeTypes ?? Array.Empty<string>()).Distinct().ToArray() ?? Array.Empty<string>();
  37. var intent = new Intent(Intent.ActionOpenDocument)
  38. .AddCategory(Intent.CategoryOpenable)
  39. .PutExtra(Intent.ExtraAllowMultiple, options.AllowMultiple)
  40. .SetType(FilePickerFileTypes.All.MimeTypes![0]);
  41. if (mimeTypes.Length > 0)
  42. {
  43. intent = intent.PutExtra(Intent.ExtraMimeTypes, mimeTypes);
  44. }
  45. if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri)
  46. {
  47. intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri);
  48. }
  49. var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select file");
  50. var uris = await StartActivity(pickerIntent, false);
  51. return uris.Select(u => new AndroidStorageFile(_activity, u)).ToArray();
  52. }
  53. public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
  54. {
  55. var mimeTypes = options.FileTypeChoices?.Where(t => t != FilePickerFileTypes.All)
  56. .SelectMany(f => f.MimeTypes ?? Array.Empty<string>()).Distinct().ToArray() ?? Array.Empty<string>();
  57. var intent = new Intent(Intent.ActionCreateDocument)
  58. .AddCategory(Intent.CategoryOpenable)
  59. .SetType(FilePickerFileTypes.All.MimeTypes![0]);
  60. if (mimeTypes.Length > 0)
  61. {
  62. intent = intent.PutExtra(Intent.ExtraMimeTypes, mimeTypes);
  63. }
  64. if (options.SuggestedFileName is { } fileName)
  65. {
  66. if (options.DefaultExtension is { } ext)
  67. {
  68. fileName += ext.StartsWith('.') ? ext : "." + ext;
  69. }
  70. intent = intent.PutExtra(Intent.ExtraTitle, fileName);
  71. }
  72. if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri)
  73. {
  74. intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri);
  75. }
  76. var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Save file");
  77. var uris = await StartActivity(pickerIntent, true);
  78. return uris.Select(u => new AndroidStorageFile(_activity, u)).FirstOrDefault();
  79. }
  80. public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
  81. {
  82. var intent = new Intent(Intent.ActionOpenDocumentTree)
  83. .PutExtra(Intent.ExtraAllowMultiple, options.AllowMultiple);
  84. if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri)
  85. {
  86. intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri);
  87. }
  88. var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder");
  89. var uris = await StartActivity(pickerIntent, false);
  90. return uris.Select(u => new AndroidStorageFolder(_activity, u)).ToArray();
  91. }
  92. private async Task<List<AndroidUri>> StartActivity(Intent? pickerIntent, bool singleResult)
  93. {
  94. var resultList = new List<AndroidUri>(1);
  95. var tcs = new TaskCompletionSource<Intent?>();
  96. var currentRequestCode = _lastRequestCode++;
  97. _activity.ActivityResult += OnActivityResult;
  98. _activity.StartActivityForResult(pickerIntent, currentRequestCode);
  99. var result = await tcs.Task;
  100. if (result != null)
  101. {
  102. // ClipData first to avoid issue with multiple files selection.
  103. if (!singleResult && result.ClipData is { } clipData)
  104. {
  105. for (var i = 0; i < clipData.ItemCount; i++)
  106. {
  107. var uri = clipData.GetItemAt(i)?.Uri;
  108. if (uri != null)
  109. {
  110. resultList.Add(uri);
  111. }
  112. }
  113. }
  114. else if (result.Data is { } uri)
  115. {
  116. resultList.Add(uri);
  117. }
  118. }
  119. if (result?.HasExtra("error") == true)
  120. {
  121. throw new Exception(result.GetStringExtra("error"));
  122. }
  123. return resultList;
  124. void OnActivityResult(int requestCode, Result resultCode, Intent data)
  125. {
  126. if (currentRequestCode != requestCode)
  127. {
  128. return;
  129. }
  130. _activity.ActivityResult -= OnActivityResult;
  131. _ = tcs.TrySetResult(resultCode == Result.Ok ? data : null);
  132. }
  133. }
  134. private static AndroidUri? TryGetInitialUri(IStorageFolder? folder)
  135. {
  136. if (OperatingSystem.IsAndroidVersionAtLeast(26)
  137. && (folder as AndroidStorageItem)?.Uri is { } uri)
  138. {
  139. return uri;
  140. }
  141. return null;
  142. }
  143. }