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.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
<queries>
|
<queries>
|
||||||
|
<!-- Email -->
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.SENDTO" />
|
<action android:name="android.intent.action.SENDTO" />
|
||||||
<data android:scheme="mailto" />
|
<data android:scheme="mailto" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<!-- Browser -->
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<data android:scheme="http" />
|
<data android:scheme="http" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<!-- Browser -->
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<!-- Sms -->
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<data android:scheme="smsto" />
|
<data android:scheme="smsto" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<!-- PhoneDialer -->
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.DIAL" />
|
<action android:name="android.intent.action.DIAL" />
|
||||||
<data android:scheme="tel" />
|
<data android:scheme="tel" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<!-- MediaPicker -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||||
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
<uses-feature android:name="android.hardware.location" android:required="false" />
|
<uses-feature android:name="android.hardware.location" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.location.gps" 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 System.Windows.Input;
|
||||||
using Samples.Helpers;
|
using Samples.Helpers;
|
||||||
using Xamarin.Essentials;
|
using Xamarin.Essentials;
|
||||||
|
@ -157,7 +158,7 @@ namespace Samples.ViewModel
|
||||||
await Share.RequestAsync(new ShareMultipleFilesRequest
|
await Share.RequestAsync(new ShareMultipleFilesRequest
|
||||||
{
|
{
|
||||||
Title = ShareFilesTitle,
|
Title = ShareFilesTitle,
|
||||||
Files = new ShareFile[] { new ShareFile(file1), new ShareFile(file2) },
|
Files = new List<ShareFile> { new ShareFile(file1), new ShareFile(file2) },
|
||||||
PresentationSourceBounds = GetRectangle(element)
|
PresentationSourceBounds = GetRectangle(element)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,5 +17,26 @@ namespace Tests
|
||||||
{
|
{
|
||||||
await Assert.ThrowsAsync<NotImplementedInReferenceAssemblyException>(() => FileSystem.OpenAppPackageFileAsync("filename.txt"));
|
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)
|
if (action == Intent.ActionSendto)
|
||||||
intent.SetData(Uri.Parse("mailto:"));
|
intent.SetData(Uri.Parse("mailto:"));
|
||||||
else
|
else
|
||||||
intent.SetType("message/rfc822");
|
intent.SetType(FileSystem.MimeTypes.EmailMessage);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(message?.Body))
|
if (!string.IsNullOrEmpty(message?.Body))
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,11 +2,8 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Android.App;
|
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.Provider;
|
|
||||||
|
|
||||||
namespace Xamarin.Essentials
|
namespace Xamarin.Essentials
|
||||||
{
|
{
|
||||||
|
@ -22,7 +19,7 @@ namespace Xamarin.Essentials
|
||||||
var action = Intent.ActionOpenDocument;
|
var action = Intent.ActionOpenDocument;
|
||||||
|
|
||||||
var intent = new Intent(action);
|
var intent = new Intent(action);
|
||||||
intent.SetType("*/*");
|
intent.SetType(FileSystem.MimeTypes.All);
|
||||||
intent.PutExtra(Intent.ExtraAllowMultiple, allowMultiple);
|
intent.PutExtra(Intent.ExtraAllowMultiple, allowMultiple);
|
||||||
|
|
||||||
var allowedTypes = options?.FileTypes?.Value?.ToArray();
|
var allowedTypes = options?.FileTypes?.Value?.ToArray();
|
||||||
|
@ -33,23 +30,30 @@ namespace Xamarin.Essentials
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await IntermediateActivity.StartAsync(pickerIntent, Platform.requestCodeFilePicker);
|
|
||||||
var resultList = new List<FileResult>();
|
var resultList = new List<FileResult>();
|
||||||
|
void OnResult(Intent intent)
|
||||||
var clipData = new List<global::Android.Net.Uri>();
|
|
||||||
|
|
||||||
if (result.ClipData == null)
|
|
||||||
{
|
{
|
||||||
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
|
||||||
else
|
// an intermediate activity.
|
||||||
{
|
|
||||||
for (var i = 0; i < result.ClipData.ItemCount; i++)
|
if (intent.ClipData == null)
|
||||||
clipData.Add(result.ClipData.GetItemAt(i).Uri);
|
{
|
||||||
|
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)
|
await IntermediateActivity.StartAsync(pickerIntent, Platform.requestCodeFilePicker, onResult: OnResult);
|
||||||
resultList.Add(new FileResult(contentUri));
|
|
||||||
|
|
||||||
return resultList;
|
return resultList;
|
||||||
}
|
}
|
||||||
|
@ -65,31 +69,31 @@ namespace Xamarin.Essentials
|
||||||
static FilePickerFileType PlatformImageFileType() =>
|
static FilePickerFileType PlatformImageFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
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() =>
|
static FilePickerFileType PlatformPngFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
{ DevicePlatform.Android, new[] { "image/png" } }
|
{ DevicePlatform.Android, new[] { FileSystem.MimeTypes.ImagePng } }
|
||||||
});
|
});
|
||||||
|
|
||||||
static FilePickerFileType PlatformJpegFileType() =>
|
static FilePickerFileType PlatformJpegFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
{ DevicePlatform.Android, new[] { "image/jpeg" } }
|
{ DevicePlatform.Android, new[] { FileSystem.MimeTypes.ImageJpg } }
|
||||||
});
|
});
|
||||||
|
|
||||||
static FilePickerFileType PlatformVideoFileType() =>
|
static FilePickerFileType PlatformVideoFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
{ DevicePlatform.Android, new[] { "video/*" } }
|
{ DevicePlatform.Android, new[] { FileSystem.MimeTypes.VideoAll } }
|
||||||
});
|
});
|
||||||
|
|
||||||
static FilePickerFileType PlatformPdfFileType() =>
|
static FilePickerFileType PlatformPdfFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
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
|
try
|
||||||
{
|
{
|
||||||
// there was a cancellation
|
|
||||||
tcs.TrySetResult(GetFileResults(urls));
|
tcs.TrySetResult(GetFileResults(urls));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -72,13 +71,10 @@ namespace Xamarin.Essentials
|
||||||
return tcs.Task;
|
return tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
static IEnumerable<FileResult> GetFileResults(NSUrl[] urls)
|
static IEnumerable<FileResult> GetFileResults(NSUrl[] urls) =>
|
||||||
{
|
urls?.Length > 0
|
||||||
if (urls?.Length > 0)
|
? urls.Select(url => new UIDocumentFileResult(url))
|
||||||
return urls.Select(url => new UIDocumentFileResult(url));
|
: Enumerable.Empty<FileResult>();
|
||||||
else
|
|
||||||
return Enumerable.Empty<FileResult>();
|
|
||||||
}
|
|
||||||
|
|
||||||
class PickerDelegate : UIDocumentPickerDelegate
|
class PickerDelegate : UIDocumentPickerDelegate
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,7 +24,7 @@ namespace Xamarin.Essentials
|
||||||
appControl.LaunchMode = AppControlLaunchMode.Single;
|
appControl.LaunchMode = AppControlLaunchMode.Single;
|
||||||
|
|
||||||
var fileType = options?.FileTypes?.Value?.FirstOrDefault();
|
var fileType = options?.FileTypes?.Value?.FirstOrDefault();
|
||||||
appControl.Mime = fileType ?? "*/*";
|
appControl.Mime = fileType ?? FileSystem.MimeTypes.All;
|
||||||
|
|
||||||
var fileResults = new List<FileResult>();
|
var fileResults = new List<FileResult>();
|
||||||
|
|
||||||
|
@ -51,31 +51,31 @@ namespace Xamarin.Essentials
|
||||||
static FilePickerFileType PlatformImageFileType() =>
|
static FilePickerFileType PlatformImageFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
{ DevicePlatform.Tizen, new[] { "image/*" } },
|
{ DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.ImageAll } },
|
||||||
});
|
});
|
||||||
|
|
||||||
static FilePickerFileType PlatformPngFileType() =>
|
static FilePickerFileType PlatformPngFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
{ DevicePlatform.Tizen, new[] { "image/png" } }
|
{ DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.ImagePng } }
|
||||||
});
|
});
|
||||||
|
|
||||||
static FilePickerFileType PlatformJpegFileType() =>
|
static FilePickerFileType PlatformJpegFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
{ DevicePlatform.Tizen, new[] { "image/jpeg" } }
|
{ DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.ImageJpg } }
|
||||||
});
|
});
|
||||||
|
|
||||||
static FilePickerFileType PlatformVideoFileType() =>
|
static FilePickerFileType PlatformVideoFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
{ DevicePlatform.Tizen, new[] { "video/*" } }
|
{ DevicePlatform.Tizen, new[] { FileSystem.MimeTypes.VideoAll } }
|
||||||
});
|
});
|
||||||
|
|
||||||
static FilePickerFileType PlatformPdfFileType() =>
|
static FilePickerFileType PlatformPdfFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
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)
|
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;
|
hasAtLeastOneType = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,31 +68,31 @@ namespace Xamarin.Essentials
|
||||||
static FilePickerFileType PlatformImageFileType() =>
|
static FilePickerFileType PlatformImageFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
{ DevicePlatform.UWP, new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp" } }
|
{ DevicePlatform.UWP, FileSystem.Extensions.AllImage }
|
||||||
});
|
});
|
||||||
|
|
||||||
static FilePickerFileType PlatformPngFileType() =>
|
static FilePickerFileType PlatformPngFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
{ DevicePlatform.UWP, new[] { "*.png" } }
|
{ DevicePlatform.UWP, new[] { FileSystem.Extensions.Png } }
|
||||||
});
|
});
|
||||||
|
|
||||||
static FilePickerFileType PlatformJpegFileType() =>
|
static FilePickerFileType PlatformJpegFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||||
{
|
{
|
||||||
{ DevicePlatform.UWP, new[] { "*.jpg", "*.jpeg" } }
|
{ DevicePlatform.UWP, FileSystem.Extensions.AllJpeg }
|
||||||
});
|
});
|
||||||
|
|
||||||
static FilePickerFileType PlatformVideoFileType() =>
|
static FilePickerFileType PlatformVideoFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
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() =>
|
static FilePickerFileType PlatformPdfFileType() =>
|
||||||
new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
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;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Android.App;
|
|
||||||
using Android.Provider;
|
using Android.Provider;
|
||||||
using Android.Webkit;
|
using Android.Webkit;
|
||||||
|
using AndroidUri = Android.Net.Uri;
|
||||||
|
|
||||||
namespace Xamarin.Essentials
|
namespace Xamarin.Essentials
|
||||||
{
|
{
|
||||||
public partial class FileSystem
|
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
|
static string PlatformCacheDirectory
|
||||||
=> Platform.AppContext.CacheDir.AbsolutePath;
|
=> Platform.AppContext.CacheDir.AbsolutePath;
|
||||||
|
|
||||||
|
@ -31,67 +52,292 @@ namespace Xamarin.Essentials
|
||||||
throw new FileNotFoundException(ex.Message, filename, ex);
|
throw new FileNotFoundException(ex.Message, filename, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public partial class FileBase
|
internal static Java.IO.File GetEssentialsTemporaryFile(Java.IO.File root, string fileName)
|
||||||
{
|
|
||||||
internal FileBase(Java.IO.File file)
|
|
||||||
: this(file?.Path)
|
|
||||||
{
|
{
|
||||||
|
// 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)
|
internal static string EnsurePhysicalPath(AndroidUri uri)
|
||||||
: 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)
|
|
||||||
{
|
{
|
||||||
// if this is a file, use that
|
// if this is a file, use that
|
||||||
if (contentUri.Scheme == "file")
|
if (uri.Scheme.Equals(UriSchemeFile, StringComparison.OrdinalIgnoreCase))
|
||||||
return contentUri.Path;
|
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
|
#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
|
#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))
|
if (!string.IsNullOrEmpty(path) && Path.IsPathRooted(path))
|
||||||
return path;
|
return path;
|
||||||
|
|
||||||
// fallback: use content URI
|
return null;
|
||||||
return contentUri.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static string GetFileName(global::Android.Net.Uri contentUri)
|
static string GetFileExtension(AndroidUri uri)
|
||||||
{
|
{
|
||||||
// resolve file name by querying content provider for display name
|
var mimeType = Platform.ContentResolver.GetType(uri);
|
||||||
var filename = QueryContentResolverColumn(contentUri, MediaStore.MediaColumns.DisplayName);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(filename))
|
return mimeType != null
|
||||||
{
|
? MimeTypeMap.Singleton.GetExtensionFromMimeType(mimeType)
|
||||||
filename = Path.GetFileName(WebUtility.UrlDecode(contentUri.ToString()));
|
: 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;
|
string text = null;
|
||||||
|
|
||||||
var projection = new[] { columnName };
|
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)
|
if (cursor?.MoveToFirst() == true)
|
||||||
{
|
{
|
||||||
var columnIndex = cursor.GetColumnIndex(columnName);
|
var columnIndex = cursor.GetColumnIndex(columnName);
|
||||||
|
@ -101,35 +347,26 @@ namespace Xamarin.Essentials
|
||||||
|
|
||||||
return text;
|
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 void PlatformInit(FileBase file)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
internal virtual Task<Stream> PlatformOpenReadAsync()
|
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);
|
var stream = File.OpenRead(FullPath);
|
||||||
return Task.FromResult<Stream>(stream);
|
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;
|
uiImage = image;
|
||||||
|
|
||||||
FullPath = Guid.NewGuid().ToString() + ".png";
|
FullPath = Guid.NewGuid().ToString() + FileSystem.Extensions.Png;
|
||||||
FileName = FullPath;
|
FileName = FullPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,77 @@ namespace Xamarin.Essentials
|
||||||
|
|
||||||
public static Task<Stream> OpenAppPackageFileAsync(string filename)
|
public static Task<Stream> OpenAppPackageFileAsync(string filename)
|
||||||
=> PlatformOpenAppPackageFileAsync(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
|
public abstract partial class FileBase
|
||||||
{
|
{
|
||||||
internal const string DefaultContentType = "application/octet-stream";
|
internal const string DefaultContentType = FileSystem.MimeTypes.OctetStream;
|
||||||
|
|
||||||
string contentType;
|
string contentType;
|
||||||
|
|
||||||
|
@ -76,7 +142,8 @@ namespace Xamarin.Essentials
|
||||||
if (!string.IsNullOrWhiteSpace(content))
|
if (!string.IsNullOrWhiteSpace(content))
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
return "application/octet-stream";
|
|
||||||
|
return DefaultContentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
string fileName;
|
string fileName;
|
||||||
|
|
|
@ -45,7 +45,7 @@ namespace Xamarin.Essentials
|
||||||
var appControl = new AppControl
|
var appControl = new AppControl
|
||||||
{
|
{
|
||||||
Operation = AppControlOperations.View,
|
Operation = AppControlOperations.View,
|
||||||
Mime = "*/*",
|
Mime = FileSystem.MimeTypes.All,
|
||||||
Uri = "file://" + request.File.FullPath,
|
Uri = "file://" + request.File.FullPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Android.App;
|
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.Provider;
|
using Android.Provider;
|
||||||
|
using AndroidUri = Android.Net.Uri;
|
||||||
|
|
||||||
namespace Xamarin.Essentials
|
namespace Xamarin.Essentials
|
||||||
{
|
{
|
||||||
|
@ -24,20 +20,30 @@ namespace Xamarin.Essentials
|
||||||
|
|
||||||
static async Task<FileResult> PlatformPickAsync(MediaPickerOptions options, bool photo)
|
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.
|
// to ask the user first, then show the picker.
|
||||||
await Permissions.RequestAsync<Permissions.StorageRead>();
|
await Permissions.RequestAsync<Permissions.StorageRead>();
|
||||||
|
|
||||||
var intent = new Intent(Intent.ActionGetContent);
|
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);
|
var pickerIntent = Intent.CreateChooser(intent, options?.Title);
|
||||||
|
|
||||||
try
|
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)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
@ -57,32 +63,47 @@ namespace Xamarin.Essentials
|
||||||
await Permissions.EnsureGrantedAsync<Permissions.StorageWrite>();
|
await Permissions.EnsureGrantedAsync<Permissions.StorageWrite>();
|
||||||
|
|
||||||
var capturePhotoIntent = new Intent(photo ? MediaStore.ActionImageCapture : MediaStore.ActionVideoCapture);
|
var capturePhotoIntent = new Intent(photo ? MediaStore.ActionImageCapture : MediaStore.ActionVideoCapture);
|
||||||
if (capturePhotoIntent.ResolveActivity(Platform.AppContext.PackageManager) != null)
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
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)
|
||||||
{
|
{
|
||||||
var activity = Platform.GetCurrentActivity(true);
|
// 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 storageDir = Platform.AppContext.ExternalCacheDir;
|
outputUri ??= FileProvider.GetUriForFile(tmpFile);
|
||||||
var tmpFile = Java.IO.File.CreateTempFile(Guid.NewGuid().ToString(), photo ? ".jpg" : ".mp4", storageDir);
|
|
||||||
tmpFile.DeleteOnExit();
|
|
||||||
|
|
||||||
capturePhotoIntent.AddFlags(ActivityFlags.GrantReadUriPermission);
|
intent.PutExtra(MediaStore.ExtraOutput, outputUri);
|
||||||
capturePhotoIntent.AddFlags(ActivityFlags.GrantWriteUriPermission);
|
|
||||||
|
|
||||||
var result = await IntermediateActivity.StartAsync(capturePhotoIntent, Platform.requestCodeMediaCapture, tmpFile);
|
|
||||||
|
|
||||||
var outputUri = result.GetParcelableExtra(IntermediateActivity.OutputUriExtra) as global::Android.Net.Uri;
|
|
||||||
|
|
||||||
return new FileResult(outputUri);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,8 @@ namespace Xamarin.Essentials
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var packageManager = Platform.AppContext.PackageManager;
|
|
||||||
var dialIntent = ResolveDialIntent(intentCheck);
|
var dialIntent = ResolveDialIntent(intentCheck);
|
||||||
return dialIntent.ResolveActivity(packageManager) != null;
|
return Platform.IsIntentSupported(dialIntent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Android.App;
|
using Android.App;
|
||||||
|
@ -12,7 +11,6 @@ using Android.Locations;
|
||||||
using Android.Net;
|
using Android.Net;
|
||||||
using Android.Net.Wifi;
|
using Android.Net.Wifi;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Provider;
|
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using AndroidIntent = Android.Content.Intent;
|
using AndroidIntent = Android.Content.Intent;
|
||||||
using AndroidUri = Android.Net.Uri;
|
using AndroidUri = Android.Net.Uri;
|
||||||
|
@ -130,12 +128,11 @@ namespace Xamarin.Essentials
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsIntentSupported(AndroidIntent intent)
|
internal static bool IsIntentSupported(AndroidIntent intent) =>
|
||||||
{
|
intent.ResolveActivity(AppContext.PackageManager) != null;
|
||||||
var manager = AppContext.PackageManager;
|
|
||||||
var activities = manager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
|
internal static bool IsIntentSupported(AndroidIntent intent, string expectedPackageName) =>
|
||||||
return activities.Any();
|
intent.ResolveActivity(AppContext.PackageManager) is ComponentName c && c.PackageName == expectedPackageName;
|
||||||
}
|
|
||||||
|
|
||||||
internal static AndroidUri GetShareableFileUri(FileBase file)
|
internal static AndroidUri GetShareableFileUri(FileBase file)
|
||||||
{
|
{
|
||||||
|
@ -147,28 +144,11 @@ namespace Xamarin.Essentials
|
||||||
}
|
}
|
||||||
else
|
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 tmpFile = FileSystem.GetEssentialsTemporaryFile(root, file.FileName);
|
||||||
var tmpDir = new Java.IO.File(rootDir, Guid.NewGuid().ToString("N"));
|
|
||||||
tmpDir.Mkdirs();
|
|
||||||
tmpDir.DeleteOnExit();
|
|
||||||
|
|
||||||
// create the new temprary file
|
System.IO.File.Copy(file.FullPath, tmpFile.CanonicalPath);
|
||||||
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;
|
sharedFile = tmpFile;
|
||||||
}
|
}
|
||||||
|
@ -187,47 +167,15 @@ namespace Xamarin.Essentials
|
||||||
return AndroidUri.FromFile(sharedFile);
|
return AndroidUri.FromFile(sharedFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool HasApiLevelN =>
|
internal static bool HasApiLevelKitKat => HasApiLevel(BuildVersionCodes.Kitkat);
|
||||||
#if __ANDROID_24__
|
|
||||||
HasApiLevel(BuildVersionCodes.N);
|
|
||||||
#else
|
|
||||||
false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
internal static bool HasApiLevelNMr1 =>
|
internal static bool HasApiLevelN => HasApiLevel(24);
|
||||||
#if __ANDROID_25__
|
|
||||||
HasApiLevel(BuildVersionCodes.NMr1);
|
|
||||||
#else
|
|
||||||
false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
internal static bool HasApiLevelO =>
|
internal static bool HasApiLevelNMr1 => HasApiLevel(25);
|
||||||
#if __ANDROID_26__
|
|
||||||
HasApiLevel(BuildVersionCodes.O);
|
|
||||||
#else
|
|
||||||
false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
internal static bool HasApiLevelOMr1 =>
|
internal static bool HasApiLevelO => HasApiLevel(26);
|
||||||
#if __ANDROID_27__
|
|
||||||
HasApiLevel(BuildVersionCodes.OMr1);
|
|
||||||
#else
|
|
||||||
false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
internal static bool HasApiLevelP =>
|
internal static bool HasApiLevelQ => HasApiLevel(29);
|
||||||
#if __ANDROID_28__
|
|
||||||
HasApiLevel(BuildVersionCodes.P);
|
|
||||||
#else
|
|
||||||
false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
internal static bool HasApiLevelQ =>
|
|
||||||
#if __ANDROID_29__
|
|
||||||
HasApiLevel(BuildVersionCodes.Q);
|
|
||||||
#else
|
|
||||||
false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static int? sdkInt;
|
static int? sdkInt;
|
||||||
|
|
||||||
|
@ -237,6 +185,9 @@ namespace Xamarin.Essentials
|
||||||
internal static bool HasApiLevel(BuildVersionCodes versionCode) =>
|
internal static bool HasApiLevel(BuildVersionCodes versionCode) =>
|
||||||
SdkInt >= (int)versionCode;
|
SdkInt >= (int)versionCode;
|
||||||
|
|
||||||
|
internal static bool HasApiLevel(int apiLevel) =>
|
||||||
|
SdkInt >= apiLevel;
|
||||||
|
|
||||||
internal static CameraManager CameraManager =>
|
internal static CameraManager CameraManager =>
|
||||||
AppContext.GetSystemService(Context.CameraService) as CameraManager;
|
AppContext.GetSystemService(Context.CameraService) as CameraManager;
|
||||||
|
|
||||||
|
@ -387,19 +338,14 @@ namespace Xamarin.Essentials
|
||||||
const string actualIntentExtra = "actual_intent";
|
const string actualIntentExtra = "actual_intent";
|
||||||
const string guidExtra = "guid";
|
const string guidExtra = "guid";
|
||||||
const string requestCodeExtra = "request_code";
|
const string requestCodeExtra = "request_code";
|
||||||
const string outputExtra = "output";
|
|
||||||
|
|
||||||
internal const string OutputUriExtra = "output_uri";
|
static readonly ConcurrentDictionary<string, IntermediateTask> pendingTasks =
|
||||||
|
new ConcurrentDictionary<string, IntermediateTask>();
|
||||||
static readonly ConcurrentDictionary<string, TaskCompletionSource<Intent>> pendingTasks =
|
|
||||||
new ConcurrentDictionary<string, TaskCompletionSource<Intent>>();
|
|
||||||
|
|
||||||
bool launched;
|
bool launched;
|
||||||
Intent actualIntent;
|
Intent actualIntent;
|
||||||
string guid;
|
string guid;
|
||||||
int requestCode;
|
int requestCode;
|
||||||
string output;
|
|
||||||
global::Android.Net.Uri outputUri;
|
|
||||||
|
|
||||||
protected override void OnCreate(Bundle savedInstanceState)
|
protected override void OnCreate(Bundle savedInstanceState)
|
||||||
{
|
{
|
||||||
|
@ -412,14 +358,10 @@ namespace Xamarin.Essentials
|
||||||
actualIntent = extras.GetParcelable(actualIntentExtra) as Intent;
|
actualIntent = extras.GetParcelable(actualIntentExtra) as Intent;
|
||||||
guid = extras.GetString(guidExtra);
|
guid = extras.GetString(guidExtra);
|
||||||
requestCode = extras.GetInt(requestCodeExtra, -1);
|
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);
|
task.OnCreate?.Invoke(actualIntent);
|
||||||
var providerAuthority = FileProvider.Authority;
|
|
||||||
outputUri = FileProvider.GetUriForFile(Platform.AppContext, providerAuthority, javaFile);
|
|
||||||
actualIntent.PutExtra(MediaStore.ExtraOutput, outputUri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this is the first time, lauch the real activity
|
// if this is the first time, lauch the real activity
|
||||||
|
@ -436,7 +378,6 @@ namespace Xamarin.Essentials
|
||||||
outState.PutParcelable(actualIntentExtra, actualIntent);
|
outState.PutParcelable(actualIntentExtra, actualIntent);
|
||||||
outState.PutString(guidExtra, guid);
|
outState.PutString(guidExtra, guid);
|
||||||
outState.PutInt(requestCodeExtra, requestCode);
|
outState.PutInt(requestCodeExtra, requestCode);
|
||||||
outState.PutString(outputExtra, output);
|
|
||||||
|
|
||||||
base.OnSaveInstanceState(outState);
|
base.OnSaveInstanceState(outState);
|
||||||
}
|
}
|
||||||
|
@ -446,21 +387,26 @@ namespace Xamarin.Essentials
|
||||||
base.OnActivityResult(requestCode, resultCode, data);
|
base.OnActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
// we have a valid GUID, so handle the task
|
// 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)
|
if (resultCode == Result.Canceled)
|
||||||
{
|
{
|
||||||
tcs.TrySetCanceled();
|
task.TaskCompletionSource.TrySetCanceled();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (outputUri != null)
|
try
|
||||||
{
|
{
|
||||||
data ??= new AndroidIntent();
|
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();
|
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
|
// make sure we have the activity
|
||||||
var activity = Platform.GetCurrentActivity(true);
|
var activity = Platform.GetCurrentActivity(true);
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<Intent>();
|
|
||||||
|
|
||||||
// create a new task
|
// create a new task
|
||||||
var guid = Guid.NewGuid().ToString();
|
var data = new IntermediateTask(onCreate, onResult);
|
||||||
pendingTasks[guid] = tcs;
|
pendingTasks[data.Id] = data;
|
||||||
|
|
||||||
// create the intermediate intent, and add the real intent to it
|
// create the intermediate intent, and add the real intent to it
|
||||||
var intermediateIntent = new Intent(activity, typeof(IntermediateActivity));
|
var intermediateIntent = new Intent(activity, typeof(IntermediateActivity));
|
||||||
intermediateIntent.PutExtra(actualIntentExtra, intent);
|
intermediateIntent.PutExtra(actualIntentExtra, intent);
|
||||||
intermediateIntent.PutExtra(guidExtra, guid);
|
intermediateIntent.PutExtra(guidExtra, data.Id);
|
||||||
intermediateIntent.PutExtra(requestCodeExtra, requestCode);
|
intermediateIntent.PutExtra(requestCodeExtra, requestCode);
|
||||||
|
|
||||||
if (extraOutput != null)
|
|
||||||
intermediateIntent.PutExtra(outputExtra, extraOutput.AbsolutePath);
|
|
||||||
|
|
||||||
// start the intermediate activity
|
// start the intermediate activity
|
||||||
activity.StartActivityForResult(intermediateIntent, requestCode);
|
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);
|
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));
|
intent.PutExtra(Intent.ExtraText, string.Join(System.Environment.NewLine, items));
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.Subject))
|
if (!string.IsNullOrWhiteSpace(request.Subject))
|
||||||
|
@ -46,7 +46,10 @@ namespace Xamarin.Essentials
|
||||||
foreach (var file in request.Files)
|
foreach (var file in request.Files)
|
||||||
contentUris.Add(Platform.GetShareableFileUri(file));
|
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.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||||
intent.PutParcelableArrayListExtra(Intent.ExtraStream, contentUris);
|
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)
|
public ShareMultipleFilesRequest(IEnumerable<FileBase> files)
|
||||||
: this(ConvertList(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)
|
public static explicit operator ShareMultipleFilesRequest(ShareFileRequest request)
|
||||||
{
|
{
|
||||||
|
|
|
@ -44,7 +44,7 @@ namespace Xamarin.Essentials
|
||||||
if (!string.IsNullOrWhiteSpace(packageName))
|
if (!string.IsNullOrWhiteSpace(packageName))
|
||||||
{
|
{
|
||||||
intent = new Intent(Intent.ActionSend);
|
intent = new Intent(Intent.ActionSend);
|
||||||
intent.SetType("text/plain");
|
intent.SetType(FileSystem.MimeTypes.TextPlain);
|
||||||
intent.PutExtra(Intent.ExtraText, body);
|
intent.PutExtra(Intent.ExtraText, body);
|
||||||
intent.SetPackage(packageName);
|
intent.SetPackage(packageName);
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ using Android.App;
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using AndroidEnvironment = Android.OS.Environment;
|
using AndroidEnvironment = Android.OS.Environment;
|
||||||
|
using AndroidUri = Android.Net.Uri;
|
||||||
#if __ANDROID_29__
|
#if __ANDROID_29__
|
||||||
using ContentFileProvider = AndroidX.Core.Content.FileProvider;
|
using ContentFileProvider = AndroidX.Core.Content.FileProvider;
|
||||||
#else
|
#else
|
||||||
|
@ -30,15 +31,6 @@ namespace Xamarin.Essentials
|
||||||
|
|
||||||
internal static string Authority => Platform.AppContext.PackageName + ".fileProvider";
|
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()
|
internal static Java.IO.File GetTemporaryRootDirectory()
|
||||||
{
|
{
|
||||||
// If we specifically want the internal storage, no extra checks are needed, we have permission
|
// If we specifically want the internal storage, no extra checks are needed, we have permission
|
||||||
|
@ -124,6 +116,9 @@ namespace Xamarin.Essentials
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static AndroidUri GetUriForFile(Java.IO.File file) =>
|
||||||
|
FileProvider.GetUriForFile(Platform.AppContext, Authority, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FileProviderLocation
|
public enum FileProviderLocation
|
||||||
|
|
|
@ -59,9 +59,7 @@ namespace Xamarin.Essentials
|
||||||
intent.SetData(global::Android.Net.Uri.Parse(callbackUrl.OriginalString));
|
intent.SetData(global::Android.Net.Uri.Parse(callbackUrl.OriginalString));
|
||||||
|
|
||||||
// Try to find the activity for the callback intent
|
// Try to find the activity for the callback intent
|
||||||
var c = intent.ResolveActivity(Platform.AppContext.PackageManager);
|
if (!Platform.IsIntentSupported(intent, packageName))
|
||||||
|
|
||||||
if (c == null || c.PackageName != packageName)
|
|
||||||
throw new InvalidOperationException($"You must subclass the `{nameof(WebAuthenticatorCallbackActivity)}` and create an IntentFilter for it which matches your `{nameof(callbackUrl)}`.");
|
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
|
// Cancel any previous task that's still pending
|
||||||
|
|
Загрузка…
Ссылка в новой задаче