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