Doing things like attaching files to emails and sharing files with DataTransfer is difficult on android since one must get a URI from a file provider in newer versions of android.
We should add some functionality to help get a proper URI from an existing filename the developer passes to us which we then transfer into a file provider friendly filename.
You must specify the paths you intend to use in an xml resource file. Most users would want to use one or both of these paths:
<?xml version="1.0" encoding="UTF-8" ?>
<paths>
<external-path path "caboodle" name="caboodle/" />
<!-- NOT NEEDED: -->
<!-- <external-path path="Android/data/com.some.package/" name="files_root" /> -->
<!-- <external-path path="." name="external_storage_root" /> -->
</paths>
EDIT: We probably only need to do something like <external-path path "caboodle" name="caboodle" /> or similar... We don't need external storage and we don't need the package name in the path it appears.. so this is good.
We can just add this as a resource in the library itself.
The AndroidManifest.xml file needs this declared:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
I don't believe we can accomplish this currently with C# attributes (there doesn't seem to be an [assembly: Provider(..)] attribute.
Also note the @xml/file_paths resource name needs to be known for this declaration.
We could get the user to add this manually (again, not nice). Or we can use a custom build task.
Finally we get the actual content uri:
var contentUri = Android.Support.V4.Content.FileProvider.GetUriForFile(context, "com.some.package", new Java.IO.File(filename));
We can use this uri for things like starting an intent chooser:
var intent = new Android.Content.Intent(Android.Content.Intent.ActionSend);
intent.PutExtra(Android.Content.Intent.ExtraStream, contentUri);
intent.SetFlags(Android.Content.ActivityFlags.GrantReadUriPermission);
var intentChooser = Android.Content.Intent.CreateChooser(intent, "Title");
context.StartActivity(intentChooser);
VS bug #732192
This is what I currently have to document in Media Plugin:
Android File Provider Setup
You must also add a few additional configuration files to adhere to the new strict mode:
1.) Add the following to your AndroidManifest.xml inside the
<provider android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
2.) Add a new folder called xml into your Resources folder and add a new XML file called file_paths.xml
Add the following code:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="my_images" path="Pictures" />
<external-files-path name="my_movies" path="Movies" />
</paths>
My thoughts here are that we will have to include this in the documentation, add them to the templates, and maybe add them to a readme.txt pop up..
We can cheat a bit by creating a custom file provider class and adding the [Provider] attribute. This is what PhoneGap does I think - they have a custom provider that just inherits from FileProvider.
This way we can just ask the user to add the xml file - or we could do that via a build task...
So conceivable we could maybe cheat with:
[ContentProvider(
new[] { "${applicationId}.fileProvider" },
Name = "microsoft.caboodle.FileProvider",
Exported = false,
GrantUriPermissions = true)]
[MetaData(
"android.support.FILE_PROVIDER_PATHS",
Resource = "@xml/caboodle_file_paths")]
public class CustomProvider : Android.Support.V4.Content.FileProvider
{
}
So I think we can do this without a custom build task then. We can probably make it as simple as a Platform.GetUriForFile(string filename) method.
EDIT2:
I found a solution for EDIT1: use a resource for the name of the content provider.
[ContentProvider(new[] { "@string/vapolia_picturepicker_fileprovidername" }, Exported = false, GrantUriPermissions = true)]
[MetaData("android.support.FILE_PROVIDER_PATHS", Resource = "@xml/vapolia_picturepicker_paths")]
public class VapoliaPicturePickerFileProvider : FileProvider
{
}
The problem is that the final app must still add the resource string.
<resources>
<string name="vapolia_picturepicker_fileprovidername">com.yourcompany.yourapp.vapolia.picturepicker</string>
</resources>
And code
var stringId = res.GetIdentifier("vapolia_picturepicker_fileprovidername", "string", androidGlobals.ApplicationContext.PackageName);
if(stringId <= 0)
throw new Exception("you must declare a string value in your app's resource: @string/vapolia_picturepicker_fileprovidername with value 'com.yourcompany.yourapp.vapolia.picturepicker'. ex: <resources>\r\n <string name=\"vapolia_picturepicker_fileprovidername\">com.yourcompany.yourapp.vapolia.picturepicker</string>\r\n</resources>\r\n");
var fileProviderName = res.GetString(stringId); //vapolia.mvvmcross.picturepicker.fileProvider
photoUri = FileProvider.GetUriForFile(androidGlobals.ApplicationContext, fileProviderName, file); //Android 22.1+, required on android 24+
__
EDIT1:
Android refuses to install an app if on the same android phone another app declares a content provider which has the same "authority" name. So this solution is not good.
--
Hi,
just tryed that in an android library and it works fine!
[ContentProvider(new[] { "vapolia.mvvmcross.picturepicker.fileProvider" }, Exported = false, GrantUriPermissions = true)]
[MetaData("android.support.FILE_PROVIDER_PATHS", Resource = "@xml/vapolia_picturepicker_paths")]
public class VapoliaPicturePickerFileProvider : FileProvider
{
}
The resource in the android library:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="my_images" path="Pictures" />
<external-files-path name="my_movies" path="Movies" />
</paths>
Usage:
var file = Java.IO.File.CreateTempFile(Guid.NewGuid().ToString("N"), ".jpg", androidGlobals.ApplicationContext.GetExternalFilesDir(Android.OS.Environment.DirectoryPictures));
if(Build.VERSION.SdkInt >= BuildVersionCodes.N)
photoUri = FileProvider.GetUriForFile(androidGlobals.ApplicationContext, "vapolia.mvvmcross.picturepicker.fileProvider", file);
else
photoUri = Uri.FromFile(file);
I want to share something that blocked us for days: the fileprovider code must be inserted between the application tags and not after it. It may be trivial, but it's never specified, and I thought that I could have helped someone!
Grazie @piolo94!! E' stato utilissimo. Quel problema ha bloccato anche noi per giorni.
I am glad I was able to help!! :)
I would like to see this getting integrated. #129 also seems to depend on solving this. I tried to follow @softlion 's instructions but did not get it to work. Once this is done several other file related api's could follow suit and be integrated.
but did not get it to work
It's working fine though. Maybe you missed something ?
Many other features/api's will depend on this which is why we want to get this right. We are going to be looking at this as one of the first things after a 1.0 release.
We need to get this reviewed as we have several features waiting on this.
[VS sync] The field 'Milestone' contains the value '1.1.0' that is not in the list of supported values
1、动态申请权限
```
protected override void OnResume()
{
base.OnResume();
#region 权限请求
if (ActivityCompat.CheckSelfPermission(this, Manifest.Permission.WriteExternalStorage) != Permission.Granted)
{
ActivityCompat.RequestPermissions(this, new[] { Manifest.Permission.ReadExternalStorage, Manifest.Permission.WriteExternalStorage },0);
}
#endregion
}
2、路径地址:/storage/emulated/0/
3、viewer显示
```
var rs = System.IO.File.Exists(filepath);
if (rs)
{
var context = Android.App.Application.Context;
var file = new Java.IO.File(filepath);
//var uri = FileProvider.GetUriForFile(context, "com.jzxy.duoke.provider", new Java.IO.File(filepath));
//var uri = FileProvider.GetUriForFile(MainActivity.AppContext, MainActivity.AppContext.PackageName+ ".provider", new Java.IO.File(filepath));
var uri = Android.Net.Uri.FromFile(file);
try
{
var intent = new Intent(Intent.ActionView);
intent.AddCategory(Intent.CategoryDefault);
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
intent.SetFlags(ActivityFlags.ClearWhenTaskReset | ActivityFlags.NewTask);
intent.SetDataAndType(uri, mimeType);
Forms.Context.StartActivity(Intent.CreateChooser(intent, "选择打开的程序"));
}
catch (Exception e)
{
Toast.MakeText(context, e.Message, ToastLength.Long).Show();
}
}
Most helpful comment
This is what I currently have to document in Media Plugin:
Android File Provider Setup
You must also add a few additional configuration files to adhere to the new strict mode:
1.) Add the following to your AndroidManifest.xml inside the tags:
2.) Add a new folder called xml into your Resources folder and add a new XML file called file_paths.xml
Add the following code:
My thoughts here are that we will have to include this in the documentation, add them to the templates, and maybe add them to a readme.txt pop up..