Как использовать поддержку FileProvider для обмена контентом с другими приложениями?

148

Я ищу способ правильно поделиться (не ОТКРЫТЬ) внутренним файлом с внешним приложением с помощью 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внутренне проверяет , является ли сам по себе идет на экспорт, если это так, то бросает исключение.

Рэнди Сугианто 'Юку'
источник
7
+1 это действительно оригинальный вопрос - насколько я могу судить, ничего нет ни в Google, ни в StackOverflow .
Фил
Вот сообщение, чтобы получить базовую настройку FileProvider с использованием минимального примера проекта github: stackoverflow.com/a/48103567/2162226 . Он предоставляет шаги, по которым файлы копируются (без внесения изменений) в локальный автономный проект
Джин Бо

Ответы:

161

Используя 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 контента в намерение, вызвав setData ().
  • Затем вызовите метод Intent.setFlags () либо с FLAG_GRANT_READ_URI_PERMISSION, либо с FLAG_GRANT_WRITE_URI_PERMISSION, либо с обоими.
  • Наконец, отправьте намерение в другое приложение. Чаще всего это делается путем вызова setResult ().

    Разрешения, предоставленные в Intent, остаются в силе, пока активен стек принимающей Activity. Когда стек заканчивается,
    разрешения автоматически удаляются. Разрешения, предоставленные одному
    Activity в клиентском приложении, автоматически распространяются на другие
    компоненты этого приложения.

Кстати. при необходимости вы можете скопировать источник FileProvider и изменить attachInfoметод, чтобы поставщик не проверял, экспортируется ли он.

Лешек
источник
27
Используя grantUriPermissionметод, который нам нужен, мы должны предоставить имя пакета приложения, которому мы хотим предоставить разрешение. Однако при совместном использовании мы, как правило, не знаем, какое приложение предназначено для обмена.
Рэнди Сугианто 'Yuku'
Что делать, если мы не знаем имя пакета и просто хотим, чтобы другие приложения могли его открывать,
StuStirling,
3
Не могли бы вы предоставить рабочий код для последнего решения @Lecho?
StErMi
2
Есть ли открытая ошибка отслеживания, почему поддержка FileProvider не работает с setFlags, но работает с grantUriPermission? Или это не ошибка, и в таком случае как это не ошибка?
nmr
1
В дополнение к этому я обнаружил, что вызов grantUriPermission не работал для намерения, созданного с помощью ShareCompat, но он работал при создании намерения вручную.
StuStirling,
35

Пример полностью работающего кода, как поделиться файлом из внутренней папки приложения. Проверено на Android 7 и Android 5.

AndroidManifest.xml

</application>
   ....
    <provider
        android:name="androidx.core.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);
Дайверы
источник
1
Использование ShareCompat.IntentBuilderразрешения URI и чтения наконец сделало это за меня.
Cord Rehn,
Перерывы для других версий Android
Оливер Диксон,
@OliverDixon: какие? Тестировал на Android 6.0 и 9.0.
Violet Giraffe
3
Это единственный ответ на использование того, FileProviderчто работает. В других ответах неправильно обрабатывается намерение (они не используются ShareCompat.IntentBuilder), и это намерение не может быть открыто внешними приложениями каким-либо значимым образом. Я потратил буквально большую часть дня на эту ерунду, пока наконец не нашел ваш ответ.
Violet Giraffe
1
@VioletGiraffe Я тоже не использую ShareCompat, но мой работает. Моя проблема заключалась в том, что я по ошибке предоставлял разрешения на чтение намерению моего селектора, когда мне нужно было предоставить разрешения на чтение моему намерению ДО того, как оно было передано намерению выбора, а также выбору. Я надеюсь, что в этом есть смысл. Просто убедитесь, что вы предоставляете разрешение на чтение правильному намерению.
Райан
23

Это решение работает у меня с OS 4.4. Чтобы он работал на всех устройствах, я добавил обходной путь для старых устройств. Это гарантирует, что всегда будет использоваться самое безопасное решение.

Manifest.xml:

    <provider
        android:name="androidx.core.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 () Фрагмента или Действия.

Оливер Кранц
источник
1
Это решение сработало для меня, но только после того, как я добавил intent.setDataAndType (uri, "video / *"); Кстати, отсутствует переменная attachmentUri - uri
EdgarK
Вы имеете в виду, что fileэто не изменится с onResumeна onDestroy?
CoolMind
16

Поскольку, как Фил говорит в своем комментарии к исходному вопросу, это уникально, и в 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.

Люк Слиман
источник
11
К сожалению, это не работает. Эта переменная fileToShare работает, только если файл существует на SD-карте или во внешней файловой системе. Смысл использования FileProvider заключается в том, чтобы вы могли делиться контентом из внутренней системы.
Кейт Коннолли
Похоже, это правильно работает с файлами локального хранилища (на Android 5+). Но у меня проблемы с Android 4.
Peterdk
15

Насколько я могу судить, это будет работать только на более новых версиях 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!");
}
Расмусоб
источник
9

В моем приложении 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>

И поскольку мои файлы хранились в каталоге корневых файлов, файл paths.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();
                }
            }

У меня это сработало. Надеюсь, это поможет :)

Адарш Читран
источник
1
Вы настоящий MVP для публикации paths = "."
Сработал
Я получаю сообщение об ошибке типа «Не удалось найти настроенный корень, содержащий /». Я использовал тот же код
Ракшит Кумар
5

Если вы получаете изображение с камеры, ни одно из этих решений не работает на 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);
}
CoolMind
источник
1

просто для улучшения ответа, указанного выше: если вы получаете 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);
                }
Дипак Сингх
источник
1

Хочу поделиться тем, что нас заблокировало на пару дней: код файлового провайдера ДОЛЖЕН быть вставлен между тегами приложения, а не после него. Это может быть банально, но это нигде не указано, и я подумал, что мог бы кому-то помочь! (еще раз спасибо piolo94)

Эрик Дрейвен
источник