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<T> for consistency
This commit is contained in:
Родитель
ba911949b6
Коммит
add928f57b
|
@ -13,26 +13,35 @@
|
|||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<queries>
|
||||
<!-- Email -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
<data android:scheme="mailto" />
|
||||
</intent>
|
||||
<!-- Browser -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="http" />
|
||||
</intent>
|
||||
<!-- Browser -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
<!-- Sms -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="smsto" />
|
||||
</intent>
|
||||
<!-- PhoneDialer -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.DIAL" />
|
||||
<data android:scheme="tel" />
|
||||
</intent>
|
||||
<!-- MediaPicker -->
|
||||
<intent>
|
||||
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||
</intent>
|
||||
</queries>
|
||||
<uses-feature android:name="android.hardware.location" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
|
||||
|
|
|
@ -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 @@ namespace Samples.ViewModel
|
|||
await Share.RequestAsync(new ShareMultipleFilesRequest
|
||||
{
|
||||
Title = ShareFilesTitle,
|
||||
Files = new ShareFile[] { new ShareFile(file1), new ShareFile(file2) },
|
||||
Files = new List<ShareFile> { new ShareFile(file1), new ShareFile(file2) },
|
||||
PresentationSourceBounds = GetRectangle(element)
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,5 +17,26 @@ namespace Tests
|
|||
{
|
||||
await Assert.ThrowsAsync<NotImplementedInReferenceAssemblyException>(() => 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace Xamarin.Essentials
|
|||
if (action == Intent.ActionSendto)
|
||||
intent.SetData(Uri.Parse("mailto:"));
|
||||
else
|
||||
intent.SetType("message/rfc822");
|
||||
intent.SetType(FileSystem.MimeTypes.EmailMessage);
|
||||
|
||||
if (!string.IsNullOrEmpty(message?.Body))
|
||||
{
|
||||
|
|
|
@ -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 @@ namespace Xamarin.Essentials
|
|||
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 @@ namespace Xamarin.Essentials
|
|||
|
||||
try
|
||||
{
|
||||
var result = await IntermediateActivity.StartAsync(pickerIntent, Platform.requestCodeFilePicker);
|
||||
var resultList = new List<FileResult>();
|
||||
|
||||
var clipData = new List<global::Android.Net.Uri>();
|
||||
|
||||
if (result.ClipData == null)
|
||||
void OnResult(Intent intent)
|
||||
{
|
||||
clipData.Add(result.Data);
|
||||
// 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 < result.ClipData.ItemCount; i++)
|
||||
clipData.Add(result.ClipData.GetItemAt(i).Uri);
|
||||
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 @@ namespace Xamarin.Essentials
|
|||
static FilePickerFileType PlatformImageFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.Android, new[] { "image/png", "image/jpeg" } }
|
||||
{ DevicePlatform.Android, new[] { FileSystem.MimeTypes.ImagePng, FileSystem.MimeTypes.ImageJpg } }
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformPngFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.Android, new[] { "image/png" } }
|
||||
{ DevicePlatform.Android, new[] { FileSystem.MimeTypes.ImagePng } }
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformJpegFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.Android, new[] { "image/jpeg" } }
|
||||
{ DevicePlatform.Android, new[] { FileSystem.MimeTypes.ImageJpg } }
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformVideoFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.Android, new[] { "video/*" } }
|
||||
{ DevicePlatform.Android, new[] { FileSystem.MimeTypes.VideoAll } }
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformPdfFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.Android, new[] { "application/pdf" } }
|
||||
{ DevicePlatform.Android, new[] { FileSystem.MimeTypes.Pdf } }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ namespace Xamarin.Essentials
|
|||
{
|
||||
try
|
||||
{
|
||||
// there was a cancellation
|
||||
tcs.TrySetResult(GetFileResults(urls));
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -72,13 +71,10 @@ namespace Xamarin.Essentials
|
|||
return tcs.Task;
|
||||
}
|
||||
|
||||
static IEnumerable<FileResult> GetFileResults(NSUrl[] urls)
|
||||
{
|
||||
if (urls?.Length > 0)
|
||||
return urls.Select(url => new UIDocumentFileResult(url));
|
||||
else
|
||||
return Enumerable.Empty<FileResult>();
|
||||
}
|
||||
static IEnumerable<FileResult> GetFileResults(NSUrl[] urls) =>
|
||||
urls?.Length > 0
|
||||
? urls.Select(url => new UIDocumentFileResult(url))
|
||||
: Enumerable.Empty<FileResult>();
|
||||
|
||||
class PickerDelegate : UIDocumentPickerDelegate
|
||||
{
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace Xamarin.Essentials
|
|||
appControl.LaunchMode = AppControlLaunchMode.Single;
|
||||
|
||||
var fileType = options?.FileTypes?.Value?.FirstOrDefault();
|
||||
appControl.Mime = fileType ?? "*/*";
|
||||
appControl.Mime = fileType ?? FileSystem.MimeTypes.All;
|
||||
|
||||
var fileResults = new List<FileResult>();
|
||||
|
||||
|
@ -51,31 +51,31 @@ namespace Xamarin.Essentials
|
|||
static FilePickerFileType PlatformImageFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.Tizen, new[] { "image/*" } },
|
||||
{ DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.ImageAll } },
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformPngFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.Tizen, new[] { "image/png" } }
|
||||
{ DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.ImagePng } }
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformJpegFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.Tizen, new[] { "image/jpeg" } }
|
||||
{ DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.ImageJpg } }
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformVideoFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.Tizen, new[] { "video/*" } }
|
||||
{ DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.VideoAll } }
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformPdfFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.Tizen, new[] { "application/pdf" } }
|
||||
{ DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.Pdf } }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,9 +49,10 @@ namespace Xamarin.Essentials
|
|||
{
|
||||
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 @@ namespace Xamarin.Essentials
|
|||
static FilePickerFileType PlatformImageFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.UWP, new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp" } }
|
||||
{ DevicePlatform.UWP, FileSystem.Extensions.AllImage }
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformPngFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.UWP, new[] { "*.png" } }
|
||||
{ DevicePlatform.UWP, new[] { FileSystem.Extensions.Png } }
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformJpegFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.UWP, new[] { "*.jpg", "*.jpeg" } }
|
||||
{ DevicePlatform.UWP, FileSystem.Extensions.AllJpeg }
|
||||
});
|
||||
|
||||
static FilePickerFileType PlatformVideoFileType() =>
|
||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ 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, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.UWP, new[] { "*.pdf" } }
|
||||
{ DevicePlatform.UWP, new[] { FileSystem.Extensions.Pdf } }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 @@ namespace Xamarin.Essentials
|
|||
throw new FileNotFoundException(ex.Message, filename, ex);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public partial class FileBase
|
||||
{
|
||||
internal FileBase(Java.IO.File file)
|
||||
: this(file?.Path)
|
||||
{
|
||||
}
|
||||
|
||||
internal FileBase(global::Android.Net.Uri contentUri)
|
||||
: this(GetFullPath(contentUri))
|
||||
{
|
||||
this.contentUri = contentUri;
|
||||
FileName = GetFileName(contentUri);
|
||||
}
|
||||
|
||||
readonly global::Android.Net.Uri contentUri;
|
||||
|
||||
internal static string PlatformGetContentType(string extension) =>
|
||||
MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension.TrimStart('.'));
|
||||
|
||||
static string GetFullPath(global::Android.Net.Uri contentUri)
|
||||
internal static string EnsurePhysicalPath(AndroidUri uri)
|
||||
{
|
||||
// if this is a file, use that
|
||||
if (contentUri.Scheme == "file")
|
||||
return contentUri.Path;
|
||||
if (uri.Scheme.Equals(UriSchemeFile, StringComparison.OrdinalIgnoreCase))
|
||||
return uri.Path;
|
||||
|
||||
// ask the content provider for the data column, which may contain the actual file 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}'.");
|
||||
}
|
||||
|
||||
static string ResolvePhysicalPath(AndroidUri uri)
|
||||
{
|
||||
if (Platform.HasApiLevelKitKat && DocumentsContract.IsDocumentUri(Platform.AppContext, uri))
|
||||
{
|
||||
var resolved = ResolveDocumentPath(uri);
|
||||
if (File.Exists(resolved))
|
||||
return resolved;
|
||||
}
|
||||
|
||||
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 ResolveDocumentPath(AndroidUri uri)
|
||||
{
|
||||
Debug.WriteLine($"Trying to resolve document URI: '{uri}'");
|
||||
|
||||
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
|
||||
|
||||
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);
|
||||
|
||||
return tmpFile.CanonicalPath;
|
||||
}
|
||||
|
||||
static Stream OpenContentStream(AndroidUri uri, out string extension)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
var flags = (DocumentContractFlags)flagsInt;
|
||||
return flags.HasFlag(DocumentContractFlags.VirtualDocument);
|
||||
}
|
||||
|
||||
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 null;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// fallback: use content URI
|
||||
return contentUri.ToString();
|
||||
return null;
|
||||
}
|
||||
|
||||
static string GetFileName(global::Android.Net.Uri contentUri)
|
||||
static string GetFileExtension(AndroidUri uri)
|
||||
{
|
||||
// resolve file name by querying content provider for display name
|
||||
var filename = QueryContentResolverColumn(contentUri, MediaStore.MediaColumns.DisplayName);
|
||||
var mimeType = Platform.ContentResolver.GetType(uri);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(filename))
|
||||
{
|
||||
filename = Path.GetFileName(WebUtility.UrlDecode(contentUri.ToString()));
|
||||
return mimeType != null
|
||||
? MimeTypeMap.Singleton.GetExtensionFromMimeType(mimeType)
|
||||
: null;
|
||||
}
|
||||
|
||||
if (!Path.HasExtension(filename))
|
||||
filename = filename.TrimEnd('.') + '.' + GetFileExtensionFromUri(contentUri);
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
static string QueryContentResolverColumn(global::Android.Net.Uri contentUri, string columnName)
|
||||
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 @@ namespace Xamarin.Essentials
|
|||
|
||||
return text;
|
||||
}
|
||||
|
||||
static string GetFileExtensionFromUri(global::Android.Net.Uri uri)
|
||||
{
|
||||
var mimeType = Application.Context.ContentResolver.GetType(uri);
|
||||
return mimeType != null ? global::Android.Webkit.MimeTypeMap.Singleton.GetExtensionFromMimeType(mimeType) : string.Empty;
|
||||
}
|
||||
|
||||
public partial class FileBase
|
||||
{
|
||||
internal FileBase(Java.IO.File file)
|
||||
: this(file?.Path)
|
||||
{
|
||||
}
|
||||
|
||||
internal static string PlatformGetContentType(string extension) =>
|
||||
MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension.TrimStart('.'));
|
||||
|
||||
internal void PlatformInit(FileBase file)
|
||||
{
|
||||
}
|
||||
|
||||
internal virtual Task<Stream> 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>(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class FileResult
|
||||
{
|
||||
internal FileResult(global::Android.Net.Uri contentUri)
|
||||
: base(contentUri)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace Xamarin.Essentials
|
|||
{
|
||||
uiImage = image;
|
||||
|
||||
FullPath = Guid.NewGuid().ToString() + ".png";
|
||||
FullPath = Guid.NewGuid().ToString() + FileSystem.Extensions.Png;
|
||||
FileName = FullPath;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,11 +14,77 @@ namespace Xamarin.Essentials
|
|||
|
||||
public static Task<Stream> 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 @@ namespace Xamarin.Essentials
|
|||
if (!string.IsNullOrWhiteSpace(content))
|
||||
return content;
|
||||
}
|
||||
return "application/octet-stream";
|
||||
|
||||
return DefaultContentType;
|
||||
}
|
||||
|
||||
string fileName;
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace Xamarin.Essentials
|
|||
var appControl = new AppControl
|
||||
{
|
||||
Operation = AppControlOperations.View,
|
||||
Mime = "*/*",
|
||||
Mime = FileSystem.MimeTypes.All,
|
||||
Uri = "file://" + request.File.FullPath,
|
||||
};
|
||||
|
||||
|
|
|
@ -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 @@ namespace Xamarin.Essentials
|
|||
|
||||
static async Task<FileResult> 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<Permissions.StorageRead>();
|
||||
|
||||
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.
|
||||
|
||||
return new FileResult(result.Data);
|
||||
path = FileSystem.EnsurePhysicalPath(intent.Data);
|
||||
}
|
||||
|
||||
await IntermediateActivity.StartAsync(pickerIntent, Platform.requestCodeMediaPicker, onResult: OnResult);
|
||||
|
||||
return new FileResult(path);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
@ -57,32 +63,47 @@ namespace Xamarin.Essentials
|
|||
await Permissions.EnsureGrantedAsync<Permissions.StorageWrite>();
|
||||
|
||||
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 <queries> 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);
|
||||
|
||||
var result = await IntermediateActivity.StartAsync(capturePhotoIntent, Platform.requestCodeMediaCapture, tmpFile);
|
||||
try
|
||||
{
|
||||
var activity = Platform.GetCurrentActivity(true);
|
||||
|
||||
var outputUri = result.GetParcelableExtra(IntermediateActivity.OutputUriExtra) as global::Android.Net.Uri;
|
||||
// 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);
|
||||
|
||||
return new FileResult(outputUri);
|
||||
// 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.
|
||||
|
||||
outputUri ??= FileProvider.GetUriForFile(tmpFile);
|
||||
|
||||
intent.PutExtra(MediaStore.ExtraOutput, outputUri);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,8 @@ namespace Xamarin.Essentials
|
|||
{
|
||||
get
|
||||
{
|
||||
var packageManager = Platform.AppContext.PackageManager;
|
||||
var dialIntent = ResolveDialIntent(intentCheck);
|
||||
return dialIntent.ResolveActivity(packageManager) != null;
|
||||
return Platform.IsIntentSupported(dialIntent);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.Locations;
|
|||
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 @@ namespace Xamarin.Essentials
|
|||
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 @@ namespace Xamarin.Essentials
|
|||
}
|
||||
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);
|
||||
}
|
||||
|
||||
sharedFile = tmpFile;
|
||||
}
|
||||
|
@ -187,47 +167,15 @@ namespace Xamarin.Essentials
|
|||
return AndroidUri.FromFile(sharedFile);
|
||||
}
|
||||
|
||||
internal static bool HasApiLevelN =>
|
||||
#if __ANDROID_24__
|
||||
HasApiLevel(BuildVersionCodes.N);
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
internal static bool HasApiLevelKitKat => HasApiLevel(BuildVersionCodes.Kitkat);
|
||||
|
||||
internal static bool HasApiLevelNMr1 =>
|
||||
#if __ANDROID_25__
|
||||
HasApiLevel(BuildVersionCodes.NMr1);
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
internal static bool HasApiLevelN => HasApiLevel(24);
|
||||
|
||||
internal static bool HasApiLevelO =>
|
||||
#if __ANDROID_26__
|
||||
HasApiLevel(BuildVersionCodes.O);
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
internal static bool HasApiLevelNMr1 => HasApiLevel(25);
|
||||
|
||||
internal static bool HasApiLevelOMr1 =>
|
||||
#if __ANDROID_27__
|
||||
HasApiLevel(BuildVersionCodes.OMr1);
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
internal static bool HasApiLevelO => HasApiLevel(26);
|
||||
|
||||
internal static bool HasApiLevelP =>
|
||||
#if __ANDROID_28__
|
||||
HasApiLevel(BuildVersionCodes.P);
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
|
||||
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 @@ namespace Xamarin.Essentials
|
|||
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 @@ namespace Xamarin.Essentials
|
|||
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<string, TaskCompletionSource<Intent>> pendingTasks =
|
||||
new ConcurrentDictionary<string, TaskCompletionSource<Intent>>();
|
||||
static readonly ConcurrentDictionary<string, IntermediateTask> pendingTasks =
|
||||
new ConcurrentDictionary<string, IntermediateTask>();
|
||||
|
||||
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 @@ namespace Xamarin.Essentials
|
|||
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 @@ namespace Xamarin.Essentials
|
|||
outState.PutParcelable(actualIntentExtra, actualIntent);
|
||||
outState.PutString(guidExtra, guid);
|
||||
outState.PutInt(requestCodeExtra, requestCode);
|
||||
outState.PutString(outputExtra, output);
|
||||
|
||||
base.OnSaveInstanceState(outState);
|
||||
}
|
||||
|
@ -446,21 +387,26 @@ namespace Xamarin.Essentials
|
|||
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 @@ namespace Xamarin.Essentials
|
|||
Finish();
|
||||
}
|
||||
|
||||
public static Task<Intent> StartAsync(Intent intent, int requestCode, Java.IO.File extraOutput = null)
|
||||
public static Task<Intent> StartAsync(Intent intent, int requestCode, Action<Intent> onCreate = null, Action<Intent> onResult = null)
|
||||
{
|
||||
// make sure we have the activity
|
||||
var activity = Platform.GetCurrentActivity(true);
|
||||
|
||||
var tcs = new TaskCompletionSource<Intent>();
|
||||
|
||||
// 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<Intent> onCreate, Action<AndroidIntent> onResult)
|
||||
{
|
||||
Id = Guid.NewGuid().ToString();
|
||||
TaskCompletionSource = new TaskCompletionSource<Intent>();
|
||||
|
||||
OnCreate = onCreate;
|
||||
OnResult = onResult;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public TaskCompletionSource<Intent> TaskCompletionSource { get; }
|
||||
|
||||
public Action<Intent> OnCreate { get; }
|
||||
|
||||
public Action<AndroidIntent> OnResult { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace Xamarin.Essentials
|
|||
}
|
||||
|
||||
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 @@ namespace Xamarin.Essentials
|
|||
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);
|
||||
|
|
|
@ -116,7 +116,8 @@ namespace Xamarin.Essentials
|
|||
{
|
||||
}
|
||||
|
||||
public ShareMultipleFilesRequest(IEnumerable<ShareFile> files) => Files = files;
|
||||
public ShareMultipleFilesRequest(IEnumerable<ShareFile> files) =>
|
||||
Files = files.ToList();
|
||||
|
||||
public ShareMultipleFilesRequest(IEnumerable<FileBase> files)
|
||||
: this(ConvertList(files))
|
||||
|
@ -131,7 +132,7 @@ namespace Xamarin.Essentials
|
|||
{
|
||||
}
|
||||
|
||||
public IEnumerable<ShareFile> Files { get; set; }
|
||||
public List<ShareFile> Files { get; set; }
|
||||
|
||||
public static explicit operator ShareMultipleFilesRequest(ShareFileRequest request)
|
||||
{
|
||||
|
|
|
@ -44,7 +44,7 @@ namespace Xamarin.Essentials
|
|||
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);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ using Android.App;
|
|||
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 @@ namespace Xamarin.Essentials
|
|||
|
||||
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 @@ namespace Xamarin.Essentials
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static AndroidUri GetUriForFile(Java.IO.File file) =>
|
||||
FileProvider.GetUriForFile(Platform.AppContext, Authority, file);
|
||||
}
|
||||
|
||||
public enum FileProviderLocation
|
||||
|
|
|
@ -59,9 +59,7 @@ namespace Xamarin.Essentials
|
|||
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
|
||||
|
|
Загрузка…
Ссылка в новой задаче