Как использовать поддержку FileProvider для обмена контентом с другими приложениями?
Я ищу способ правильно поделиться (не ОТКРЫТЬ) внутренний файл с внешним приложением, используя FileProvider библиотеки поддержки Android.
Следуя примеру на документах,
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.android.supportv4.my_files"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/my_paths" />
</provider>
и используя ShareCompat, чтобы поделиться файлом с другими приложениями следующим образом:
ShareCompat.IntentBuilder.from(activity)
.setStream(uri) // uri from FileProvider
.setType("text/html")
.getIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
не работает, так как FLAG_GRANT_READ_URI_PERMISSION предоставляет разрешение только для Uri, указанного в data
намерения, а не значение EXTRA_STREAM
дополнительно (как было установлено setStream
).
Я попытался поставить под угрозу безопасность, установив android:exported
в true
для провайдера, но FileProvider
внутренне проверяет, экспортирован ли сам, когда это так, он генерирует исключение.
11 ответов
С помощью FileProvider
из библиотеки поддержки вы должны вручную предоставлять и отзывать разрешения (во время выполнения) для других приложений для чтения определенных Uri. Используйте методы Context.grantUriPermission и Context.revokeUriPermission.
Например:
//grant permision for app with package "packegeName", eg. before starting other app via intent
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
//revoke permisions
context.revokeUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
В крайнем случае, если вы не можете предоставить имя пакета, вы можете предоставить разрешение всем приложениям, которые могут обрабатывать определенные намерения:
//grant permisions for all apps that can handle given intent
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
...
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
Альтернативный метод согласно документации:
- Поместите URI содержимого в Intent, вызвав setData().
- Затем вызовите метод Intent.setFlags() либо с FLAG_GRANT_READ_URI_PERMISSION, либо с FLAG_GRANT_WRITE_URI_PERMISSION, либо с обоими.
Наконец, отправьте Намерение в другое приложение. Чаще всего вы делаете это, вызывая setResult().
Разрешения, предоставленные в Intent, остаются в силе, пока активен стек принимающего Activity. Когда стек заканчивается,
разрешения автоматически удаляются. Разрешения предоставлены одному
Активность в клиентском приложении автоматически распространяется на другие
компоненты этого приложения.
Btw. если вам нужно, вы можете скопировать источник FileProvider и изменить attachInfo
метод, позволяющий провайдеру проверить, экспортируется ли он.
Полностью рабочий пример кода, как поделиться файлом из внутренней папки приложения. Проверено на Android 7 и Android 5.
AndroidManifest.xml
</application>
....
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="android.getqardio.com.gmslocationtest"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
</application>
XML /provider_paths
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="share"
path="external_files"/>
</paths>
Сам код
File imagePath = new File(getFilesDir(), "external_files");
imagePath.mkdir();
File imageFile = new File(imagePath.getPath(), "test.jpg");
// Write data in your file
Uri uri = FileProvider.getUriForFile(this, getPackageName(), imageFile);
Intent intent = ShareCompat.IntentBuilder.from(this)
.setStream(uri) // uri from FileProvider
.setType("text/html")
.getIntent()
.setAction(Intent.ACTION_VIEW) //Change if needed
.setDataAndType(uri, "image/*")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
Это решение работает для меня начиная с ОС 4.4. Чтобы это работало на всех устройствах, я добавил обходной путь для старых устройств. Это гарантирует, что всегда используется самое безопасное решение.
manifest.xml:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.package.name.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
file_paths.xml:
<paths>
<files-path name="app_directory" path="directory/"/>
</paths>
Джава:
public static void sendFile(Context context) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
String dirpath = context.getFilesDir() + File.separator + "directory";
File file = new File(dirpath + File.separator + "file.txt");
Uri uri = FileProvider.getUriForFile(context, "com.package.name.fileprovider", file);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Workaround for Android bug.
// grantUriPermission also needed for KITKAT,
// see https://code.google.com/p/android/issues/detail?id=76683
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, attachmentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
if (intent.resolveActivity(context.getPackageManager()) != null) {
context.startActivity(intent);
}
}
public static void revokeFileReadPermission(Context context) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
String dirpath = context.getFilesDir() + File.separator + "directory";
File file = new File(dirpath + File.separator + "file.txt");
Uri uri = FileProvider.getUriForFile(context, "com.package.name.fileprovider", file);
context.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
Разрешение отзывается с помощью revokeFileReadPermission() в методах onResume и onDestroy() фрагмента или действия.
Поскольку, как говорит Фил в своем комментарии к исходному вопросу, это уникально, и в Google нет другой информации о SO, поэтому я решил поделиться своими результатами:
В моем приложении FileProvider работал из коробки, чтобы делиться файлами, используя намерение поделиться. Не было никакой специальной конфигурации или кода, необходимого для настройки FileProvider. В моем файле manifest.xml я разместил:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.my.apps.package.files"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/my_paths" />
</provider>
В my_paths.xml у меня есть:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="files" path="." />
</paths>
В моем коде у меня есть:
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("application/xml");
Uri uri = FileProvider.getUriForFile(this, "com.my.apps.package.files", fileToShare);
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.share_file)));
И я могу без проблем делиться своим хранилищем файлов в личном хранилище своих приложений с такими приложениями, как Gmail и Google Drive.
Насколько я могу судить, это будет работать только на более новых версиях Android, поэтому вам, вероятно, придется придумать другой способ сделать это. Это решение работает для меня на 4.4, но не на 4.0 или 2.3.3, так что это не будет полезным способом делиться контентом для приложения, которое предназначено для запуска на любом устройстве Android.
В manifest.xml:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.mydomain.myapp.SharingActivity"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
Внимательно обратите внимание на то, как вы указали власти. Вы должны указать действие, из которого вы создадите URI и запустите намерение общего ресурса, в этом случае действие называется SharingActivity. Это требование не очевидно из документов Google!
file_paths.xml:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="just_a_name" path=""/>
</paths>
Будьте осторожны, как вы указываете путь. Выше по умолчанию корень вашего частного внутреннего хранилища.
В SharingActivity.java:
Uri contentUri = FileProvider.getUriForFile(getActivity(),
"com.mydomain.myapp.SharingActivity", myFile);
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("image/jpeg");
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(shareIntent, "Share with"));
В этом примере мы делимся изображением JPEG.
Наконец, это, вероятно, хорошая идея, чтобы убедиться, что вы правильно сохранили файл и что вы можете получить к нему доступ примерно так:
File myFile = getActivity().getFileStreamPath("mySavedImage.jpeg");
if(myFile != null){
Log.d(TAG, "File found, file description: "+myFile.toString());
}else{
Log.w(TAG, "File not found!");
}
В моем приложении FileProvider работает просто отлично, и я могу прикреплять внутренние файлы, хранящиеся в каталоге файлов, к почтовым клиентам, таким как Gmail,Yahoo и т. Д.
В моем манифесте, упомянутом в документации по Android, я разместил:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.package.name.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
И так как мои файлы были сохранены в корневом каталоге файлов, filepaths.xml были следующими:
<paths>
<files-path path="." name="name" />
Теперь в коде:
File file=new File(context.getFilesDir(),"test.txt");
Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE);
shareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,
"Test");
shareIntent.setType("text/plain");
shareIntent.putExtra(android.content.Intent.EXTRA_EMAIL,
new String[] {"email-address you want to send the file to"});
Uri uri = FileProvider.getUriForFile(context,"com.package.name.fileprovider",
file);
ArrayList<Uri> uris = new ArrayList<Uri>();
uris.add(uri);
shareIntent .putParcelableArrayListExtra(Intent.EXTRA_STREAM,
uris);
try {
context.startActivity(Intent.createChooser(shareIntent , "Email:").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
catch(ActivityNotFoundException e) {
Toast.makeText(context,
"Sorry No email Application was found",
Toast.LENGTH_SHORT).show();
}
}
Это сработало для меня. Надеюсь, это поможет:)
Если вы получаете изображение с камеры, ни одно из этих решений не работает для Android 4.4. В этом случае лучше проверить версии.
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(getContext().getPackageManager()) != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
uri = Uri.fromFile(file);
} else {
uri = FileProvider.getUriForFile(getContext(), getContext().getPackageName() + ".provider", file);
}
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, CAMERA_REQUEST);
}
grantUriPermission (из документа Android)
Обычно вы должны использовать намерение #FLAG_GRANT_READ_URI_PERMISSION или намерение #FLAG_GRANT_WRITE_URI_PERMISSION с намерением, используемым для запуска действия вместо напрямую этой функции. Если вы используете эту функцию напрямую, вы должны обязательно вызвать revokeUriPermission(Uri,int), когда целевой объект больше не должен иметь к нему доступ.
Я тестирую и вижу это.
Если мы используем перед тем, как начать новую деятельность, нам НЕ нужно преодолевать
Если мы не используем
grantUriPermission
. Нам нужно использоватьFLAG_GRANT_READ_URI_PERMISSION
или жеFLAG_GRANT_WRITE_URI_PERMISSION
преодолеть, но- Ваше намерение ДОЛЖНО содержать в себе или иначе
SecurityException
все равно кидаю . (одно интересное вижу:setData
и не могут хорошо работать вместе, поэтому, если вам нужны обаUri
иtype
тебе нужноsetDataAndType
. Вы можете проверить внутриIntent
код, в настоящее время когда выsetType
, он также установит uri = null, и когда вы установитеUri, он также установит type = null)
- Ваше намерение ДОЛЖНО содержать в себе или иначе
Я хочу поделиться тем, что заблокировало нас на пару дней: код поставщика файлов ДОЛЖЕН быть вставлен между тегами приложения, а не после него. Это может быть тривиально, но это никогда не уточняется, и я подумал, что мог бы кому-то помочь! (еще раз спасибо piolo94)
Просто чтобы улучшить ответ, приведенный выше: если вы получаете NullPointerEx:
Вы также можете использовать getApplicationContext() без контекста
List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
grantUriPermission(packageName, photoURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
Вот мое сообщение в блоге о том, почему нам нужно использовать FileProvider и как его правильно использовать. Полностью обновлен до последних версий Android.