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


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

9 106

9 ответов:

используя FileProvider из библиотеки поддержки вы должны вручную предоставлять и отменять разрешения (во время выполнения) для других приложений для чтения определенного Uri. Используйте контексте.grantUriPermission и контексте.отзыв лицензии методы.

например:

//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().

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

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

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

Манифест.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() фрагмента или действия.

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

поскольку, как говорит Фил в своем комментарии к исходному вопросу, это уникально, и в google нет другой информации о SO on, я подумал, что должен также поделиться своими результатами:

в моем приложении FileProvider работал из коробки, чтобы обмениваться файлами с помощью share intent. Не было никакой специальной конфигурации или кода, необходимых, кроме того, чтобы настроить FileProvider. В моем манифесте.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>

В моем коде I есть:

    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.

в манифесте.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 и запустите намерение share, в этом случае действие называется 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();
                }
            }

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

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

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

Я хочу поделиться тем, что заблокировало нас на пару дней: код fileprovider должны вставить между тегами приложения, а не после него. Это может быть тривиально, но это никогда не указано, и я думал, что мог бы помочь кому-то! (еще раз спасибо piolo94)