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

Я ищу способ правильно разделить (не OPEN) внутренний файл с внешним приложением с помощью библиотеки поддержки Android FileProvider.

Следуя примеру документов,

<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 extra (как было установлено setStream).

Я попытался скомпрометировать безопасность, установив android:exported в true для провайдера, но FileProvider внутренне проверяет, будет ли он сам экспортироваться, когда это делает, оно выдает исключение.

Ответ 1

Используя 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().

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

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

Ответ 2

Это решение работает для меня с OS 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>

Java:

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() фрагмента или активности.

Ответ 3

Полностью рабочий пример кода, как обмениваться файлом из внутренней папки приложения. Протестировано на 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

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

Ответ 5

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

Ответ 6

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

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

Ответ 7

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