From add928f57b47c1b59beab2447a5cf224add75b50 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 2 Dec 2020 15:58:07 +0200 Subject: [PATCH] Rework the "picker" results to correctly manage files (#1555) * Rework the "picker" results to correctly manage lifetime of files and URIs * Extract magic strings into constants - Mime types - Extensions - Changed property type of ShareMultipleFilesRequest.Files to be a List for consistency --- .../Properties/AndroidManifest.xml | 9 + Samples/Samples/ViewModel/ShareViewModel.cs | 5 +- Tests/FileSystem_Tests.cs | 21 ++ Xamarin.Essentials/Email/Email.android.cs | 2 +- .../FilePicker/FilePicker.android.cs | 48 +-- .../FilePicker/FilePicker.ios.cs | 12 +- .../FilePicker/FilePicker.tizen.cs | 12 +- .../FilePicker/FilePicker.uwp.cs | 15 +- .../FileSystem/FileSystem.android.cs | 339 +++++++++++++++--- .../FileSystem/FileSystem.ios.cs | 2 +- .../FileSystem/FileSystem.shared.cs | 71 +++- Xamarin.Essentials/Launcher/Launcher.tizen.cs | 2 +- .../MediaPicker/MediaPicker.android.cs | 77 ++-- .../PhoneDialer/PhoneDialer.android.cs | 3 +- .../Platform/Platform.android.cs | 166 ++++----- Xamarin.Essentials/Share/Share.android.cs | 7 +- Xamarin.Essentials/Share/Share.shared.cs | 5 +- Xamarin.Essentials/Sms/Sms.android.cs | 2 +- .../Types/FileProvider.android.cs | 13 +- .../WebAuthenticator.android.cs | 4 +- 20 files changed, 572 insertions(+), 243 deletions(-) diff --git a/Samples/Samples.Android/Properties/AndroidManifest.xml b/Samples/Samples.Android/Properties/AndroidManifest.xml index 90ab11dfc..2e52e0fbb 100644 --- a/Samples/Samples.Android/Properties/AndroidManifest.xml +++ b/Samples/Samples.Android/Properties/AndroidManifest.xml @@ -13,26 +13,35 @@ + + + + + + + + + diff --git a/Samples/Samples/ViewModel/ShareViewModel.cs b/Samples/Samples/ViewModel/ShareViewModel.cs index a04601323..edddcd12f 100644 --- a/Samples/Samples/ViewModel/ShareViewModel.cs +++ b/Samples/Samples/ViewModel/ShareViewModel.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Windows.Input; using Samples.Helpers; using Xamarin.Essentials; @@ -157,7 +158,7 @@ async void OnFilesRequest(Xamarin.Forms.View element) await Share.RequestAsync(new ShareMultipleFilesRequest { Title = ShareFilesTitle, - Files = new ShareFile[] { new ShareFile(file1), new ShareFile(file2) }, + Files = new List { new ShareFile(file1), new ShareFile(file2) }, PresentationSourceBounds = GetRectangle(element) }); } diff --git a/Tests/FileSystem_Tests.cs b/Tests/FileSystem_Tests.cs index 5851f66b7..f88e3460e 100644 --- a/Tests/FileSystem_Tests.cs +++ b/Tests/FileSystem_Tests.cs @@ -17,5 +17,26 @@ public async Task OpenAppPackageFileAsync_Fail_On_NetStandard() { await Assert.ThrowsAsync(() => FileSystem.OpenAppPackageFileAsync("filename.txt")); } + + [Theory] + [InlineData(null, "")] + [InlineData("", "")] + [InlineData(".", ".")] + [InlineData(".txt", ".txt")] + [InlineData("*.txt", ".txt")] + [InlineData("*.*", ".*")] + [InlineData("txt", ".txt")] + [InlineData("test.txt", ".test.txt")] + [InlineData("test.", ".test.")] + [InlineData("....txt", ".txt")] + [InlineData("******txt", ".txt")] + [InlineData("******.txt", ".txt")] + [InlineData("******.......txt", ".txt")] + public void Extensions_Clean_Correctly_Cleans_Extensions(string input, string output) + { + var cleaned = FileSystem.Extensions.Clean(input); + + Assert.Equal(output, cleaned); + } } } diff --git a/Xamarin.Essentials/Email/Email.android.cs b/Xamarin.Essentials/Email/Email.android.cs index 23cb0f792..be96d1e48 100644 --- a/Xamarin.Essentials/Email/Email.android.cs +++ b/Xamarin.Essentials/Email/Email.android.cs @@ -45,7 +45,7 @@ static Intent CreateIntent(EmailMessage message) if (action == Intent.ActionSendto) intent.SetData(Uri.Parse("mailto:")); else - intent.SetType("message/rfc822"); + intent.SetType(FileSystem.MimeTypes.EmailMessage); if (!string.IsNullOrEmpty(message?.Body)) { diff --git a/Xamarin.Essentials/FilePicker/FilePicker.android.cs b/Xamarin.Essentials/FilePicker/FilePicker.android.cs index 30e65843e..dc83fc0ca 100644 --- a/Xamarin.Essentials/FilePicker/FilePicker.android.cs +++ b/Xamarin.Essentials/FilePicker/FilePicker.android.cs @@ -2,11 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Threading.Tasks; -using Android.App; using Android.Content; -using Android.Provider; namespace Xamarin.Essentials { @@ -22,7 +19,7 @@ static async Task> PlatformPickAsync(PickOptions options var action = Intent.ActionOpenDocument; var intent = new Intent(action); - intent.SetType("*/*"); + intent.SetType(FileSystem.MimeTypes.All); intent.PutExtra(Intent.ExtraAllowMultiple, allowMultiple); var allowedTypes = options?.FileTypes?.Value?.ToArray(); @@ -33,23 +30,30 @@ static async Task> PlatformPickAsync(PickOptions options try { - var result = await IntermediateActivity.StartAsync(pickerIntent, Platform.requestCodeFilePicker); var resultList = new List(); - - var clipData = new List(); - - if (result.ClipData == null) + void OnResult(Intent intent) { - clipData.Add(result.Data); - } - else - { - for (var i = 0; i < result.ClipData.ItemCount; i++) - clipData.Add(result.ClipData.GetItemAt(i).Uri); + // The uri returned is only temporary and only lives as long as the Activity that requested it, + // so this means that it will always be cleaned up by the time we need it because we are using + // an intermediate activity. + + if (intent.ClipData == null) + { + var path = FileSystem.EnsurePhysicalPath(intent.Data); + resultList.Add(new FileResult(path)); + } + else + { + for (var i = 0; i < intent.ClipData.ItemCount; i++) + { + var uri = intent.ClipData.GetItemAt(i).Uri; + var path = FileSystem.EnsurePhysicalPath(uri); + resultList.Add(new FileResult(path)); + } + } } - foreach (var contentUri in clipData) - resultList.Add(new FileResult(contentUri)); + await IntermediateActivity.StartAsync(pickerIntent, Platform.requestCodeFilePicker, onResult: OnResult); return resultList; } @@ -65,31 +69,31 @@ public partial class FilePickerFileType static FilePickerFileType PlatformImageFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.Android, new[] { "image/png", "image/jpeg" } } + { DevicePlatform.Android, new[] { FileSystem.MimeTypes.ImagePng, FileSystem.MimeTypes.ImageJpg } } }); static FilePickerFileType PlatformPngFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.Android, new[] { "image/png" } } + { DevicePlatform.Android, new[] { FileSystem.MimeTypes.ImagePng } } }); static FilePickerFileType PlatformJpegFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.Android, new[] { "image/jpeg" } } + { DevicePlatform.Android, new[] { FileSystem.MimeTypes.ImageJpg } } }); static FilePickerFileType PlatformVideoFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.Android, new[] { "video/*" } } + { DevicePlatform.Android, new[] { FileSystem.MimeTypes.VideoAll } } }); static FilePickerFileType PlatformPdfFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.Android, new[] { "application/pdf" } } + { DevicePlatform.Android, new[] { FileSystem.MimeTypes.Pdf } } }); } } diff --git a/Xamarin.Essentials/FilePicker/FilePicker.ios.cs b/Xamarin.Essentials/FilePicker/FilePicker.ios.cs index 65463a3ab..2a43df790 100644 --- a/Xamarin.Essentials/FilePicker/FilePicker.ios.cs +++ b/Xamarin.Essentials/FilePicker/FilePicker.ios.cs @@ -34,7 +34,6 @@ static Task> PlatformPickAsync(PickOptions options, bool { try { - // there was a cancellation tcs.TrySetResult(GetFileResults(urls)); } catch (Exception ex) @@ -72,13 +71,10 @@ static Task> PlatformPickAsync(PickOptions options, bool return tcs.Task; } - static IEnumerable GetFileResults(NSUrl[] urls) - { - if (urls?.Length > 0) - return urls.Select(url => new UIDocumentFileResult(url)); - else - return Enumerable.Empty(); - } + static IEnumerable GetFileResults(NSUrl[] urls) => + urls?.Length > 0 + ? urls.Select(url => new UIDocumentFileResult(url)) + : Enumerable.Empty(); class PickerDelegate : UIDocumentPickerDelegate { diff --git a/Xamarin.Essentials/FilePicker/FilePicker.tizen.cs b/Xamarin.Essentials/FilePicker/FilePicker.tizen.cs index 0b444805b..e68439f92 100644 --- a/Xamarin.Essentials/FilePicker/FilePicker.tizen.cs +++ b/Xamarin.Essentials/FilePicker/FilePicker.tizen.cs @@ -24,7 +24,7 @@ static async Task> PlatformPickAsync(PickOptions options appControl.LaunchMode = AppControlLaunchMode.Single; var fileType = options?.FileTypes?.Value?.FirstOrDefault(); - appControl.Mime = fileType ?? "*/*"; + appControl.Mime = fileType ?? FileSystem.MimeTypes.All; var fileResults = new List(); @@ -51,31 +51,31 @@ public partial class FilePickerFileType static FilePickerFileType PlatformImageFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.Tizen, new[] { "image/*" } }, + { DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.ImageAll } }, }); static FilePickerFileType PlatformPngFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.Tizen, new[] { "image/png" } } + { DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.ImagePng } } }); static FilePickerFileType PlatformJpegFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.Tizen, new[] { "image/jpeg" } } + { DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.ImageJpg } } }); static FilePickerFileType PlatformVideoFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.Tizen, new[] { "video/*" } } + { DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.VideoAll } } }); static FilePickerFileType PlatformPdfFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.Tizen, new[] { "application/pdf" } } + { DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.Pdf } } }); } } diff --git a/Xamarin.Essentials/FilePicker/FilePicker.uwp.cs b/Xamarin.Essentials/FilePicker/FilePicker.uwp.cs index 3e8653f30..ae440472e 100644 --- a/Xamarin.Essentials/FilePicker/FilePicker.uwp.cs +++ b/Xamarin.Essentials/FilePicker/FilePicker.uwp.cs @@ -49,9 +49,10 @@ static void SetFileTypes(PickOptions options, FileOpenPicker picker) { foreach (var type in options.FileTypes.Value) { - if (type.StartsWith(".") || type.StartsWith("*.")) + var ext = FileSystem.Extensions.Clean(type); + if (!string.IsNullOrWhiteSpace(ext)) { - picker.FileTypeFilter.Add(type.TrimStart('*')); + picker.FileTypeFilter.Add(ext); hasAtLeastOneType = true; } } @@ -67,31 +68,31 @@ public partial class FilePickerFileType static FilePickerFileType PlatformImageFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.UWP, new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp" } } + { DevicePlatform.UWP, FileSystem.Extensions.AllImage } }); static FilePickerFileType PlatformPngFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.UWP, new[] { "*.png" } } + { DevicePlatform.UWP, new[] { FileSystem.Extensions.Png } } }); static FilePickerFileType PlatformJpegFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.UWP, new[] { "*.jpg", "*.jpeg" } } + { DevicePlatform.UWP, FileSystem.Extensions.AllJpeg } }); static FilePickerFileType PlatformVideoFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.UWP, new[] { "*.mp4", "*.mov", "*.avi", "*.wmv", "*.m4v", "*.mpg", "*.mpeg", "*.mp2", "*.mkv", "*.flv", "*.gifv", "*.qt" } } + { DevicePlatform.UWP, FileSystem.Extensions.AllVideo } }); static FilePickerFileType PlatformPdfFileType() => new FilePickerFileType(new Dictionary> { - { DevicePlatform.UWP, new[] { "*.pdf" } } + { DevicePlatform.UWP, new[] { FileSystem.Extensions.Pdf } } }); } } diff --git a/Xamarin.Essentials/FileSystem/FileSystem.android.cs b/Xamarin.Essentials/FileSystem/FileSystem.android.cs index 3aafc4552..e771a2ac7 100644 --- a/Xamarin.Essentials/FileSystem/FileSystem.android.cs +++ b/Xamarin.Essentials/FileSystem/FileSystem.android.cs @@ -1,15 +1,36 @@ using System; +using System.Diagnostics; using System.IO; -using System.Net; using System.Threading.Tasks; -using Android.App; using Android.Provider; using Android.Webkit; +using AndroidUri = Android.Net.Uri; namespace Xamarin.Essentials { public partial class FileSystem { + internal const string EssentialsFolderHash = "2203693cc04e0be7f4f024d5f9499e13"; + + const string storageTypePrimary = "primary"; + const string storageTypeRaw = "raw"; + const string storageTypeImage = "image"; + const string storageTypeVideo = "video"; + const string storageTypeAudio = "audio"; + static readonly string[] contentUriPrefixes = + { + "content://downloads/public_downloads", + "content://downloads/my_downloads", + "content://downloads/all_downloads", + }; + + internal const string UriSchemeFile = "file"; + internal const string UriSchemeContent = "content"; + + internal const string UriAuthorityExternalStorage = "com.android.externalstorage.documents"; + internal const string UriAuthorityDownloads = "com.android.providers.downloads.documents"; + internal const string UriAuthorityMedia = "com.android.providers.media.documents"; + static string PlatformCacheDirectory => Platform.AppContext.CacheDir.AbsolutePath; @@ -31,67 +52,292 @@ static Task PlatformOpenAppPackageFileAsync(string filename) throw new FileNotFoundException(ex.Message, filename, ex); } } - } - public partial class FileBase - { - internal FileBase(Java.IO.File file) - : this(file?.Path) + internal static Java.IO.File GetEssentialsTemporaryFile(Java.IO.File root, string fileName) { + // create the directory for all Essentials files + var rootDir = new Java.IO.File(root, EssentialsFolderHash); + rootDir.Mkdirs(); + rootDir.DeleteOnExit(); + + // create a unique directory just in case there are multiple file with the same name + var tmpDir = new Java.IO.File(rootDir, Guid.NewGuid().ToString("N")); + tmpDir.Mkdirs(); + tmpDir.DeleteOnExit(); + + // create the new temporary file + var tmpFile = new Java.IO.File(tmpDir, fileName); + tmpFile.DeleteOnExit(); + + return tmpFile; } - internal FileBase(global::Android.Net.Uri contentUri) - : this(GetFullPath(contentUri)) + internal static string EnsurePhysicalPath(AndroidUri uri) { - this.contentUri = contentUri; - FileName = GetFileName(contentUri); + // if this is a file, use that + if (uri.Scheme.Equals(UriSchemeFile, StringComparison.OrdinalIgnoreCase)) + return uri.Path; + + // try resolve using the content provider + var absolute = ResolvePhysicalPath(uri); + if (!string.IsNullOrWhiteSpace(absolute) && Path.IsPathRooted(absolute)) + return absolute; + + // fall back to just copying it + absolute = CacheContentFile(uri); + if (!string.IsNullOrWhiteSpace(absolute) && Path.IsPathRooted(absolute)) + return absolute; + + throw new FileNotFoundException($"Unable to resolve absolute path or retrieve contents of URI '{uri}'."); } - readonly global::Android.Net.Uri contentUri; + static string ResolvePhysicalPath(AndroidUri uri) + { + if (Platform.HasApiLevelKitKat && DocumentsContract.IsDocumentUri(Platform.AppContext, uri)) + { + var resolved = ResolveDocumentPath(uri); + if (File.Exists(resolved)) + return resolved; + } - internal static string PlatformGetContentType(string extension) => - MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension.TrimStart('.')); + if (uri.Scheme.Equals(UriSchemeContent, StringComparison.OrdinalIgnoreCase)) + { + var resolved = ResolveContentPath(uri); + if (File.Exists(resolved)) + return resolved; + } + else if (uri.Scheme.Equals(UriSchemeFile, StringComparison.OrdinalIgnoreCase)) + { + var resolved = uri.Path; + if (File.Exists(resolved)) + return resolved; + } + + return null; + } - static string GetFullPath(global::Android.Net.Uri contentUri) + static string ResolveDocumentPath(AndroidUri uri) { - // if this is a file, use that - if (contentUri.Scheme == "file") - return contentUri.Path; + Debug.WriteLine($"Trying to resolve document URI: '{uri}'"); - // ask the content provider for the data column, which may contain the actual file path + var docId = DocumentsContract.GetDocumentId(uri); + + var docIdParts = docId?.Split(':'); + if (docIdParts == null || docIdParts.Length == 0) + return null; + + if (uri.Authority.Equals(UriAuthorityExternalStorage, StringComparison.OrdinalIgnoreCase)) + { + Debug.WriteLine($"Resolving external storage URI: '{uri}'"); + + if (docIdParts.Length == 2) + { + var storageType = docIdParts[0]; + var uriPath = docIdParts[1]; + + // This is the internal "external" memory, NOT the SD Card + if (storageType.Equals(storageTypePrimary, StringComparison.OrdinalIgnoreCase)) + { #pragma warning disable CS0618 // Type or member is obsolete - var path = QueryContentResolverColumn(contentUri, MediaStore.Files.FileColumns.Data); + var root = global::Android.OS.Environment.ExternalStorageDirectory.Path; #pragma warning restore CS0618 // Type or member is obsolete - if (!string.IsNullOrEmpty(path) && Path.IsPathRooted(path)) - return path; + return Path.Combine(root, uriPath); + } + + // TODO: support other types, such as actual SD Cards + } + } + else if (uri.Authority.Equals(UriAuthorityDownloads, StringComparison.OrdinalIgnoreCase)) + { + Debug.WriteLine($"Resolving downloads URI: '{uri}'"); + + // NOTE: This only really applies to older Android vesions since the privacy changes + + if (docIdParts.Length == 2) + { + var storageType = docIdParts[0]; + var uriPath = docIdParts[1]; + + if (storageType.Equals(storageTypeRaw, StringComparison.OrdinalIgnoreCase)) + return uriPath; + } + + // ID could be "###" or "msf:###" + var fileId = docIdParts.Length == 2 + ? docIdParts[1] + : docIdParts[0]; + + foreach (var prefix in contentUriPrefixes) + { + var uriString = prefix + "/" + fileId; + var contentUri = AndroidUri.Parse(uriString); + + if (GetDataFilePath(contentUri) is string filePath) + return filePath; + } + } + else if (uri.Authority.Equals(UriAuthorityMedia, StringComparison.OrdinalIgnoreCase)) + { + Debug.WriteLine($"Resolving media URI: '{uri}'"); + + if (docIdParts.Length == 2) + { + var storageType = docIdParts[0]; + var uriPath = docIdParts[1]; + + AndroidUri contentUri = null; + if (storageType.Equals(storageTypeImage, StringComparison.OrdinalIgnoreCase)) + contentUri = MediaStore.Images.Media.ExternalContentUri; + else if (storageType.Equals(storageTypeVideo, StringComparison.OrdinalIgnoreCase)) + contentUri = MediaStore.Video.Media.ExternalContentUri; + else if (storageType.Equals(storageTypeAudio, StringComparison.OrdinalIgnoreCase)) + contentUri = MediaStore.Audio.Media.ExternalContentUri; + + if (contentUri != null && GetDataFilePath(contentUri, $"{MediaStore.MediaColumns.Id}=?", new[] { uriPath }) is string filePath) + return filePath; + } + } + + Debug.WriteLine($"Unable to resolve document URI: '{uri}'"); + + return null; + } + + static string ResolveContentPath(AndroidUri uri) + { + Debug.WriteLine($"Trying to resolve content URI: '{uri}'"); + + if (GetDataFilePath(uri) is string filePath) + return filePath; + + // TODO: support some additional things, like Google Photos if that is possible + + Debug.WriteLine($"Unable to resolve content URI: '{uri}'"); + + return null; + } + + static string CacheContentFile(AndroidUri uri) + { + if (!uri.Scheme.Equals(UriSchemeContent, StringComparison.OrdinalIgnoreCase)) + return null; + + Debug.WriteLine($"Copying content URI to local cache: '{uri}'"); + + // open the source stream + using var srcStream = OpenContentStream(uri, out var extension); + if (srcStream == null) + return null; + + // resolve or generate a valid destination path + var filename = GetColumnValue(uri, MediaStore.Files.FileColumns.DisplayName) ?? Guid.NewGuid().ToString("N"); + if (!Path.HasExtension(filename) && !string.IsNullOrEmpty(extension)) + filename = Path.ChangeExtension(filename, extension); + + // create a temporary file + var tmpFile = GetEssentialsTemporaryFile(Platform.AppContext.CacheDir, filename); + + // copy to the destination + using var dstStream = File.Create(tmpFile.CanonicalPath); + srcStream.CopyTo(dstStream); - // fallback: use content URI - return contentUri.ToString(); + return tmpFile.CanonicalPath; } - static string GetFileName(global::Android.Net.Uri contentUri) + static Stream OpenContentStream(AndroidUri uri, out string extension) { - // resolve file name by querying content provider for display name - var filename = QueryContentResolverColumn(contentUri, MediaStore.MediaColumns.DisplayName); + var isVirtual = IsVirtualFile(uri); + if (isVirtual) + { + Debug.WriteLine($"Content URI was virtual: '{uri}'"); + return GetVirtualFileStream(uri, out extension); + } + + extension = GetFileExtension(uri); + return Platform.ContentResolver.OpenInputStream(uri); + } - if (string.IsNullOrWhiteSpace(filename)) + static bool IsVirtualFile(AndroidUri uri) + { + if (!DocumentsContract.IsDocumentUri(Platform.AppContext, uri)) + return false; + + var value = GetColumnValue(uri, DocumentsContract.Document.ColumnFlags); + if (!string.IsNullOrEmpty(value) && int.TryParse(value, out var flagsInt)) { - filename = Path.GetFileName(WebUtility.UrlDecode(contentUri.ToString())); + var flags = (DocumentContractFlags)flagsInt; + return flags.HasFlag(DocumentContractFlags.VirtualDocument); } - if (!Path.HasExtension(filename)) - filename = filename.TrimEnd('.') + '.' + GetFileExtensionFromUri(contentUri); + return false; + } + + static Stream GetVirtualFileStream(AndroidUri uri, out string extension) + { + var mimeTypes = Platform.ContentResolver.GetStreamTypes(uri, FileSystem.MimeTypes.All); + if (mimeTypes?.Length >= 1) + { + var mimeType = mimeTypes[0]; + + var stream = Platform.ContentResolver + .OpenTypedAssetFileDescriptor(uri, mimeType, null) + .CreateInputStream(); + + extension = MimeTypeMap.Singleton.GetExtensionFromMimeType(mimeType); + + return stream; + } + + extension = null; + return null; + } + + static string GetColumnValue(AndroidUri contentUri, string column, string selection = null, string[] selectionArgs = null) + { + try + { + var value = QueryContentResolverColumn(contentUri, column, selection, selectionArgs); + if (!string.IsNullOrEmpty(value)) + return value; + } + catch + { + // Ignore all exceptions and use null for the error indicator + } - return filename; + return null; } - static string QueryContentResolverColumn(global::Android.Net.Uri contentUri, string columnName) + static string GetDataFilePath(AndroidUri contentUri, string selection = null, string[] selectionArgs = null) + { +#pragma warning disable CS0618 // Type or member is obsolete + const string column = MediaStore.Files.FileColumns.Data; +#pragma warning restore CS0618 // Type or member is obsolete + + // ask the content provider for the data column, which may contain the actual file path + var path = GetColumnValue(contentUri, column, selection, selectionArgs); + if (!string.IsNullOrEmpty(path) && Path.IsPathRooted(path)) + return path; + + return null; + } + + static string GetFileExtension(AndroidUri uri) + { + var mimeType = Platform.ContentResolver.GetType(uri); + + return mimeType != null + ? MimeTypeMap.Singleton.GetExtensionFromMimeType(mimeType) + : null; + } + + static string QueryContentResolverColumn(AndroidUri contentUri, string columnName, string selection = null, string[] selectionArgs = null) { string text = null; var projection = new[] { columnName }; - using var cursor = Application.Context.ContentResolver.Query(contentUri, projection, null, null, null); + using var cursor = Platform.ContentResolver.Query(contentUri, projection, selection, selectionArgs, null); if (cursor?.MoveToFirst() == true) { var columnIndex = cursor.GetColumnIndex(columnName); @@ -101,35 +347,26 @@ static string QueryContentResolverColumn(global::Android.Net.Uri contentUri, str return text; } + } - static string GetFileExtensionFromUri(global::Android.Net.Uri uri) + public partial class FileBase + { + internal FileBase(Java.IO.File file) + : this(file?.Path) { - var mimeType = Application.Context.ContentResolver.GetType(uri); - return mimeType != null ? global::Android.Webkit.MimeTypeMap.Singleton.GetExtensionFromMimeType(mimeType) : string.Empty; } + internal static string PlatformGetContentType(string extension) => + MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension.TrimStart('.')); + internal void PlatformInit(FileBase file) { } internal virtual Task PlatformOpenReadAsync() { - if (contentUri?.Scheme == "content") - { - var content = Application.Context.ContentResolver.OpenInputStream(contentUri); - return Task.FromResult(content); - } - var stream = File.OpenRead(FullPath); return Task.FromResult(stream); } } - - public partial class FileResult - { - internal FileResult(global::Android.Net.Uri contentUri) - : base(contentUri) - { - } - } } diff --git a/Xamarin.Essentials/FileSystem/FileSystem.ios.cs b/Xamarin.Essentials/FileSystem/FileSystem.ios.cs index f88dcb849..cd53f0d8b 100644 --- a/Xamarin.Essentials/FileSystem/FileSystem.ios.cs +++ b/Xamarin.Essentials/FileSystem/FileSystem.ios.cs @@ -45,7 +45,7 @@ internal UIImageFileResult(UIImage image) { uiImage = image; - FullPath = Guid.NewGuid().ToString() + ".png"; + FullPath = Guid.NewGuid().ToString() + FileSystem.Extensions.Png; FileName = FullPath; } diff --git a/Xamarin.Essentials/FileSystem/FileSystem.shared.cs b/Xamarin.Essentials/FileSystem/FileSystem.shared.cs index 6e6c4c4ab..ad0106dd6 100644 --- a/Xamarin.Essentials/FileSystem/FileSystem.shared.cs +++ b/Xamarin.Essentials/FileSystem/FileSystem.shared.cs @@ -14,11 +14,77 @@ public static string AppDataDirectory public static Task OpenAppPackageFileAsync(string filename) => PlatformOpenAppPackageFileAsync(filename); + + internal static class MimeTypes + { + internal const string All = "*/*"; + + internal const string ImageAll = "image/*"; + internal const string ImagePng = "image/png"; + internal const string ImageJpg = "image/jpeg"; + + internal const string VideoAll = "video/*"; + + internal const string EmailMessage = "message/rfc822"; + + internal const string Pdf = "application/pdf"; + + internal const string TextPlain = "text/plain"; + + internal const string OctetStream = "application/octet-stream"; + } + + internal static class Extensions + { + internal const string Png = ".png"; + internal const string Jpg = ".jpg"; + internal const string Jpeg = ".jpeg"; + internal const string Gif = ".gif"; + internal const string Bmp = ".bmp"; + + internal const string Avi = ".avi"; + internal const string Flv = ".flv"; + internal const string Gifv = ".gifv"; + internal const string Mp4 = ".mp4"; + internal const string M4v = ".m4v"; + internal const string Mpg = ".mpg"; + internal const string Mpeg = ".mpeg"; + internal const string Mp2 = ".mp2"; + internal const string Mkv = ".mkv"; + internal const string Mov = ".mov"; + internal const string Qt = ".qt"; + internal const string Wmv = ".wmv"; + + internal const string Pdf = ".pdf"; + + internal static string[] AllImage => + new[] { Png, Jpg, Jpeg, Gif, Bmp }; + + internal static string[] AllJpeg => + new[] { Jpg, Jpeg }; + + internal static string[] AllVideo => + new[] { Mp4, Mov, Avi, Wmv, M4v, Mpg, Mpeg, Mp2, Mkv, Flv, Gifv, Qt }; + + internal static string Clean(string extension, bool trimLeadingPeriod = false) + { + if (string.IsNullOrWhiteSpace(extension)) + return string.Empty; + + extension = extension.TrimStart('*'); + extension = extension.TrimStart('.'); + + if (!trimLeadingPeriod) + extension = "." + extension; + + return extension; + } + } } public abstract partial class FileBase { - internal const string DefaultContentType = "application/octet-stream"; + internal const string DefaultContentType = FileSystem.MimeTypes.OctetStream; string contentType; @@ -76,7 +142,8 @@ internal string GetContentType() if (!string.IsNullOrWhiteSpace(content)) return content; } - return "application/octet-stream"; + + return DefaultContentType; } string fileName; diff --git a/Xamarin.Essentials/Launcher/Launcher.tizen.cs b/Xamarin.Essentials/Launcher/Launcher.tizen.cs index ccaa0ee79..f66f43a0f 100644 --- a/Xamarin.Essentials/Launcher/Launcher.tizen.cs +++ b/Xamarin.Essentials/Launcher/Launcher.tizen.cs @@ -45,7 +45,7 @@ static Task PlatformOpenAsync(OpenFileRequest request) var appControl = new AppControl { Operation = AppControlOperations.View, - Mime = "*/*", + Mime = FileSystem.MimeTypes.All, Uri = "file://" + request.File.FullPath, }; diff --git a/Xamarin.Essentials/MediaPicker/MediaPicker.android.cs b/Xamarin.Essentials/MediaPicker/MediaPicker.android.cs index 33f70f6be..76e51ab15 100644 --- a/Xamarin.Essentials/MediaPicker/MediaPicker.android.cs +++ b/Xamarin.Essentials/MediaPicker/MediaPicker.android.cs @@ -1,13 +1,9 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; using System.Threading.Tasks; -using Android.App; using Android.Content; using Android.Content.PM; using Android.Provider; +using AndroidUri = Android.Net.Uri; namespace Xamarin.Essentials { @@ -24,20 +20,30 @@ static Task PlatformPickVideoAsync(MediaPickerOptions options) static async Task PlatformPickAsync(MediaPickerOptions options, bool photo) { - // we only need the permission when accessing the file, but it's more natural + // We only need the permission when accessing the file, but it's more natural // to ask the user first, then show the picker. await Permissions.RequestAsync(); var intent = new Intent(Intent.ActionGetContent); - intent.SetType(photo ? "image/*" : "video/*"); + intent.SetType(photo ? FileSystem.MimeTypes.ImageAll : FileSystem.MimeTypes.VideoAll); var pickerIntent = Intent.CreateChooser(intent, options?.Title); try { - var result = await IntermediateActivity.StartAsync(pickerIntent, Platform.requestCodeMediaPicker); + string path = null; + void OnResult(Intent intent) + { + // The uri returned is only temporary and only lives as long as the Activity that requested it, + // so this means that it will always be cleaned up by the time we need it because we are using + // an intermediate activity. + + path = FileSystem.EnsurePhysicalPath(intent.Data); + } - return new FileResult(result.Data); + await IntermediateActivity.StartAsync(pickerIntent, Platform.requestCodeMediaPicker, onResult: OnResult); + + return new FileResult(path); } catch (OperationCanceledException) { @@ -57,32 +63,47 @@ static async Task PlatformCaptureAsync(MediaPickerOptions options, b await Permissions.EnsureGrantedAsync(); var capturePhotoIntent = new Intent(photo ? MediaStore.ActionImageCapture : MediaStore.ActionVideoCapture); - if (capturePhotoIntent.ResolveActivity(Platform.AppContext.PackageManager) != null) - { - try - { - var activity = Platform.GetCurrentActivity(true); - var storageDir = Platform.AppContext.ExternalCacheDir; - var tmpFile = Java.IO.File.CreateTempFile(Guid.NewGuid().ToString(), photo ? ".jpg" : ".mp4", storageDir); - tmpFile.DeleteOnExit(); + if (!Platform.IsIntentSupported(capturePhotoIntent)) + throw new FeatureNotSupportedException($"Either there was no camera on the device or '{capturePhotoIntent.Action}' was not added to the element in the app's manifest file. See more: https://developer.android.com/about/versions/11/privacy/package-visibility"); - capturePhotoIntent.AddFlags(ActivityFlags.GrantReadUriPermission); - capturePhotoIntent.AddFlags(ActivityFlags.GrantWriteUriPermission); + capturePhotoIntent.AddFlags(ActivityFlags.GrantReadUriPermission); + capturePhotoIntent.AddFlags(ActivityFlags.GrantWriteUriPermission); - var result = await IntermediateActivity.StartAsync(capturePhotoIntent, Platform.requestCodeMediaCapture, tmpFile); + try + { + var activity = Platform.GetCurrentActivity(true); + + // Create the temporary file + var ext = photo + ? FileSystem.Extensions.Jpg + : FileSystem.Extensions.Mp4; + var fileName = Guid.NewGuid().ToString("N") + ext; + var tmpFile = FileSystem.GetEssentialsTemporaryFile(Platform.AppContext.CacheDir, fileName); + + // Set up the content:// uri + AndroidUri outputUri = null; + void OnCreate(Intent intent) + { + // Android requires that using a file provider to get a content:// uri for a file to be called + // from within the context of the actual activity which may share that uri with another intent + // it launches. - var outputUri = result.GetParcelableExtra(IntermediateActivity.OutputUriExtra) as global::Android.Net.Uri; + outputUri ??= FileProvider.GetUriForFile(tmpFile); - return new FileResult(outputUri); + intent.PutExtra(MediaStore.ExtraOutput, outputUri); } - catch (OperationCanceledException) - { - return null; - } - } - return null; + // Start the capture process + await IntermediateActivity.StartAsync(capturePhotoIntent, Platform.requestCodeMediaCapture, OnCreate); + + // Return the file that we just captured + return new FileResult(tmpFile.AbsolutePath); + } + catch (OperationCanceledException) + { + return null; + } } } } diff --git a/Xamarin.Essentials/PhoneDialer/PhoneDialer.android.cs b/Xamarin.Essentials/PhoneDialer/PhoneDialer.android.cs index 4a7d0190a..3274d1ca7 100644 --- a/Xamarin.Essentials/PhoneDialer/PhoneDialer.android.cs +++ b/Xamarin.Essentials/PhoneDialer/PhoneDialer.android.cs @@ -16,9 +16,8 @@ internal static bool IsSupported { get { - var packageManager = Platform.AppContext.PackageManager; var dialIntent = ResolveDialIntent(intentCheck); - return dialIntent.ResolveActivity(packageManager) != null; + return Platform.IsIntentSupported(dialIntent); } } diff --git a/Xamarin.Essentials/Platform/Platform.android.cs b/Xamarin.Essentials/Platform/Platform.android.cs index 8ccd378c6..12ea7e397 100644 --- a/Xamarin.Essentials/Platform/Platform.android.cs +++ b/Xamarin.Essentials/Platform/Platform.android.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Android.App; @@ -12,7 +11,6 @@ using Android.Net; using Android.Net.Wifi; using Android.OS; -using Android.Provider; using Android.Views; using AndroidIntent = Android.Content.Intent; using AndroidUri = Android.Net.Uri; @@ -130,12 +128,11 @@ internal static bool HasSystemFeature(string systemFeature) return false; } - internal static bool IsIntentSupported(AndroidIntent intent) - { - var manager = AppContext.PackageManager; - var activities = manager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly); - return activities.Any(); - } + internal static bool IsIntentSupported(AndroidIntent intent) => + intent.ResolveActivity(AppContext.PackageManager) != null; + + internal static bool IsIntentSupported(AndroidIntent intent, string expectedPackageName) => + intent.ResolveActivity(AppContext.PackageManager) is ComponentName c && c.PackageName == expectedPackageName; internal static AndroidUri GetShareableFileUri(FileBase file) { @@ -147,28 +144,11 @@ internal static AndroidUri GetShareableFileUri(FileBase file) } else { - var rootDir = FileProvider.GetTemporaryDirectory(); + var root = FileProvider.GetTemporaryRootDirectory(); - // create a unique directory just in case there are multiple file with the same name - var tmpDir = new Java.IO.File(rootDir, Guid.NewGuid().ToString("N")); - tmpDir.Mkdirs(); - tmpDir.DeleteOnExit(); + var tmpFile = FileSystem.GetEssentialsTemporaryFile(root, file.FileName); - // create the new temprary file - var tmpFile = new Java.IO.File(tmpDir, file.FileName); - tmpFile.DeleteOnExit(); - - var fileUri = AndroidUri.Parse(file.FullPath); - if (fileUri.Scheme == "content") - { - using var stream = Application.Context.ContentResolver.OpenInputStream(fileUri); - using var destStream = System.IO.File.Create(tmpFile.CanonicalPath); - stream.CopyTo(destStream); - } - else - { - System.IO.File.Copy(file.FullPath, tmpFile.CanonicalPath); - } + System.IO.File.Copy(file.FullPath, tmpFile.CanonicalPath); sharedFile = tmpFile; } @@ -187,47 +167,15 @@ internal static AndroidUri GetShareableFileUri(FileBase file) return AndroidUri.FromFile(sharedFile); } - internal static bool HasApiLevelN => -#if __ANDROID_24__ - HasApiLevel(BuildVersionCodes.N); -#else - false; -#endif - - internal static bool HasApiLevelNMr1 => -#if __ANDROID_25__ - HasApiLevel(BuildVersionCodes.NMr1); -#else - false; -#endif + internal static bool HasApiLevelKitKat => HasApiLevel(BuildVersionCodes.Kitkat); - internal static bool HasApiLevelO => -#if __ANDROID_26__ - HasApiLevel(BuildVersionCodes.O); -#else - false; -#endif + internal static bool HasApiLevelN => HasApiLevel(24); - internal static bool HasApiLevelOMr1 => -#if __ANDROID_27__ - HasApiLevel(BuildVersionCodes.OMr1); -#else - false; -#endif + internal static bool HasApiLevelNMr1 => HasApiLevel(25); - internal static bool HasApiLevelP => -#if __ANDROID_28__ - HasApiLevel(BuildVersionCodes.P); -#else - false; -#endif + internal static bool HasApiLevelO => HasApiLevel(26); - internal static bool HasApiLevelQ => -#if __ANDROID_29__ - HasApiLevel(BuildVersionCodes.Q); -#else - false; -#endif + internal static bool HasApiLevelQ => HasApiLevel(29); static int? sdkInt; @@ -237,6 +185,9 @@ internal static int SdkInt internal static bool HasApiLevel(BuildVersionCodes versionCode) => SdkInt >= (int)versionCode; + internal static bool HasApiLevel(int apiLevel) => + SdkInt >= apiLevel; + internal static CameraManager CameraManager => AppContext.GetSystemService(Context.CameraService) as CameraManager; @@ -387,19 +338,14 @@ class IntermediateActivity : Activity const string actualIntentExtra = "actual_intent"; const string guidExtra = "guid"; const string requestCodeExtra = "request_code"; - const string outputExtra = "output"; - - internal const string OutputUriExtra = "output_uri"; - static readonly ConcurrentDictionary> pendingTasks = - new ConcurrentDictionary>(); + static readonly ConcurrentDictionary pendingTasks = + new ConcurrentDictionary(); bool launched; Intent actualIntent; string guid; int requestCode; - string output; - global::Android.Net.Uri outputUri; protected override void OnCreate(Bundle savedInstanceState) { @@ -412,14 +358,10 @@ protected override void OnCreate(Bundle savedInstanceState) actualIntent = extras.GetParcelable(actualIntentExtra) as Intent; guid = extras.GetString(guidExtra); requestCode = extras.GetInt(requestCodeExtra, -1); - output = extras.GetString(outputExtra, null); - if (!string.IsNullOrEmpty(output)) + if (GetIntermediateTask(guid) is IntermediateTask task) { - var javaFile = new Java.IO.File(output); - var providerAuthority = FileProvider.Authority; - outputUri = FileProvider.GetUriForFile(Platform.AppContext, providerAuthority, javaFile); - actualIntent.PutExtra(MediaStore.ExtraOutput, outputUri); + task.OnCreate?.Invoke(actualIntent); } // if this is the first time, lauch the real activity @@ -436,7 +378,6 @@ protected override void OnSaveInstanceState(Bundle outState) outState.PutParcelable(actualIntentExtra, actualIntent); outState.PutString(guidExtra, guid); outState.PutInt(requestCodeExtra, requestCode); - outState.PutString(outputExtra, output); base.OnSaveInstanceState(outState); } @@ -446,21 +387,26 @@ protected override void OnActivityResult(int requestCode, Result resultCode, Int base.OnActivityResult(requestCode, resultCode, data); // we have a valid GUID, so handle the task - if (!string.IsNullOrEmpty(guid) && pendingTasks.TryRemove(guid, out var tcs) && tcs != null) + if (GetIntermediateTask(guid, true) is IntermediateTask task) { if (resultCode == Result.Canceled) { - tcs.TrySetCanceled(); + task.TaskCompletionSource.TrySetCanceled(); } else { - if (outputUri != null) + try { data ??= new AndroidIntent(); - data.PutExtra(OutputUriExtra, outputUri); - } - tcs.TrySetResult(data); + task.OnResult?.Invoke(data); + + task.TaskCompletionSource.TrySetResult(data); + } + catch (Exception ex) + { + task.TaskCompletionSource.TrySetException(ex); + } } } @@ -468,30 +414,60 @@ protected override void OnActivityResult(int requestCode, Result resultCode, Int Finish(); } - public static Task StartAsync(Intent intent, int requestCode, Java.IO.File extraOutput = null) + public static Task StartAsync(Intent intent, int requestCode, Action onCreate = null, Action onResult = null) { // make sure we have the activity var activity = Platform.GetCurrentActivity(true); - var tcs = new TaskCompletionSource(); - // create a new task - var guid = Guid.NewGuid().ToString(); - pendingTasks[guid] = tcs; + var data = new IntermediateTask(onCreate, onResult); + pendingTasks[data.Id] = data; // create the intermediate intent, and add the real intent to it var intermediateIntent = new Intent(activity, typeof(IntermediateActivity)); intermediateIntent.PutExtra(actualIntentExtra, intent); - intermediateIntent.PutExtra(guidExtra, guid); + intermediateIntent.PutExtra(guidExtra, data.Id); intermediateIntent.PutExtra(requestCodeExtra, requestCode); - if (extraOutput != null) - intermediateIntent.PutExtra(outputExtra, extraOutput.AbsolutePath); - // start the intermediate activity activity.StartActivityForResult(intermediateIntent, requestCode); - return tcs.Task; + return data.TaskCompletionSource.Task; + } + + static IntermediateTask GetIntermediateTask(string guid, bool remove = false) + { + if (string.IsNullOrEmpty(guid)) + return null; + + if (remove) + { + pendingTasks.TryRemove(guid, out var removedTask); + return removedTask; + } + + pendingTasks.TryGetValue(guid, out var task); + return task; + } + + class IntermediateTask + { + public IntermediateTask(Action onCreate, Action onResult) + { + Id = Guid.NewGuid().ToString(); + TaskCompletionSource = new TaskCompletionSource(); + + OnCreate = onCreate; + OnResult = onResult; + } + + public string Id { get; } + + public TaskCompletionSource TaskCompletionSource { get; } + + public Action OnCreate { get; } + + public Action OnResult { get; } } } } diff --git a/Xamarin.Essentials/Share/Share.android.cs b/Xamarin.Essentials/Share/Share.android.cs index afc95bcc1..1c0d9641d 100644 --- a/Xamarin.Essentials/Share/Share.android.cs +++ b/Xamarin.Essentials/Share/Share.android.cs @@ -23,7 +23,7 @@ static Task PlatformRequestAsync(ShareTextRequest request) } var intent = new Intent(Intent.ActionSend); - intent.SetType("text/plain"); + intent.SetType(FileSystem.MimeTypes.TextPlain); intent.PutExtra(Intent.ExtraText, string.Join(System.Environment.NewLine, items)); if (!string.IsNullOrWhiteSpace(request.Subject)) @@ -46,7 +46,10 @@ static Task PlatformRequestAsync(ShareMultipleFilesRequest request) foreach (var file in request.Files) contentUris.Add(Platform.GetShareableFileUri(file)); - intent.SetType(request.Files.Count() > 1 ? "*/*" : request.Files.FirstOrDefault().ContentType); + var type = request.Files.Count > 1 + ? FileSystem.MimeTypes.All + : request.Files.FirstOrDefault().ContentType; + intent.SetType(type); intent.SetFlags(ActivityFlags.GrantReadUriPermission); intent.PutParcelableArrayListExtra(Intent.ExtraStream, contentUris); diff --git a/Xamarin.Essentials/Share/Share.shared.cs b/Xamarin.Essentials/Share/Share.shared.cs index a264401d6..ab6fc5ba3 100644 --- a/Xamarin.Essentials/Share/Share.shared.cs +++ b/Xamarin.Essentials/Share/Share.shared.cs @@ -116,7 +116,8 @@ public ShareMultipleFilesRequest() { } - public ShareMultipleFilesRequest(IEnumerable files) => Files = files; + public ShareMultipleFilesRequest(IEnumerable files) => + Files = files.ToList(); public ShareMultipleFilesRequest(IEnumerable files) : this(ConvertList(files)) @@ -131,7 +132,7 @@ public ShareMultipleFilesRequest(string title, IEnumerable files) { } - public IEnumerable Files { get; set; } + public List Files { get; set; } public static explicit operator ShareMultipleFilesRequest(ShareFileRequest request) { diff --git a/Xamarin.Essentials/Sms/Sms.android.cs b/Xamarin.Essentials/Sms/Sms.android.cs index f0667c1b7..966619772 100644 --- a/Xamarin.Essentials/Sms/Sms.android.cs +++ b/Xamarin.Essentials/Sms/Sms.android.cs @@ -44,7 +44,7 @@ static Intent CreateIntent(string body, List recipients) if (!string.IsNullOrWhiteSpace(packageName)) { intent = new Intent(Intent.ActionSend); - intent.SetType("text/plain"); + intent.SetType(FileSystem.MimeTypes.TextPlain); intent.PutExtra(Intent.ExtraText, body); intent.SetPackage(packageName); diff --git a/Xamarin.Essentials/Types/FileProvider.android.cs b/Xamarin.Essentials/Types/FileProvider.android.cs index c32a28061..71e2ba114 100644 --- a/Xamarin.Essentials/Types/FileProvider.android.cs +++ b/Xamarin.Essentials/Types/FileProvider.android.cs @@ -4,6 +4,7 @@ using Android.Content; using Android.OS; using AndroidEnvironment = Android.OS.Environment; +using AndroidUri = Android.Net.Uri; #if __ANDROID_29__ using ContentFileProvider = AndroidX.Core.Content.FileProvider; #else @@ -30,15 +31,6 @@ public class FileProvider : ContentFileProvider internal static string Authority => Platform.AppContext.PackageName + ".fileProvider"; - internal static Java.IO.File GetTemporaryDirectory() - { - var root = GetTemporaryRootDirectory(); - var dir = new Java.IO.File(root, "2203693cc04e0be7f4f024d5f9499e13"); - dir.Mkdirs(); - dir.DeleteOnExit(); - return dir; - } - internal static Java.IO.File GetTemporaryRootDirectory() { // If we specifically want the internal storage, no extra checks are needed, we have permission @@ -124,6 +116,9 @@ internal static bool IsFileInPublicLocation(string filename) return false; } + + internal static AndroidUri GetUriForFile(Java.IO.File file) => + FileProvider.GetUriForFile(Platform.AppContext, Authority, file); } public enum FileProviderLocation diff --git a/Xamarin.Essentials/WebAuthenticator/WebAuthenticator.android.cs b/Xamarin.Essentials/WebAuthenticator/WebAuthenticator.android.cs index c78fb55d5..762825e81 100644 --- a/Xamarin.Essentials/WebAuthenticator/WebAuthenticator.android.cs +++ b/Xamarin.Essentials/WebAuthenticator/WebAuthenticator.android.cs @@ -59,9 +59,7 @@ static async Task PlatformAuthenticateAsync(Uri url, Uri intent.SetData(global::Android.Net.Uri.Parse(callbackUrl.OriginalString)); // Try to find the activity for the callback intent - var c = intent.ResolveActivity(Platform.AppContext.PackageManager); - - if (c == null || c.PackageName != packageName) + if (!Platform.IsIntentSupported(intent, packageName)) throw new InvalidOperationException($"You must subclass the `{nameof(WebAuthenticatorCallbackActivity)}` and create an IntentFilter for it which matches your `{nameof(callbackUrl)}`."); // Cancel any previous task that's still pending