Jak korzystać z obsługi FileProvider do udostępniania treści innym aplikacjom?

142

Szukam sposobu na poprawne udostępnienie (nie OTWÓRZ) wewnętrznego pliku z zewnętrzną aplikacją za pomocą FileProvider biblioteki Android Support .

Idąc za przykładem w dokumentach,

<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>

i używając ShareCompat do udostępniania pliku innym aplikacjom w następujący sposób:

ShareCompat.IntentBuilder.from(activity)
.setStream(uri) // uri from FileProvider
.setType("text/html")
.getIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

nie działa, ponieważ FLAG_GRANT_READ_URI_PERMISSION udziela tylko pozwolenia na Uri podanego w dataintencji, a nie na wartość EXTRA_STREAMdodatkowego (jak zostało ustawione przez setStream).

Próbowałem bezpieczeństwa kompromisowego ustawiając android:exportedsię truedo usługodawcy, ale FileProviderwewnętrznie sprawdza, czy sama jest eksportowany, jeśli tak, to zgłasza wyjątek.

Randy Sugianto „Yuku”
źródło
7
+1 to wydaje się być naprawdę oryginalne pytanie - o ile wiem, nie ma nic w Google ani StackOverflow .
Phil
Oto post, aby uzyskać podstawową konfigurację FileProvider przy użyciu minimalnego przykładowego projektu github: stackoverflow.com/a/48103567/2162226 . Zawiera instrukcje, które pliki należy skopiować (bez wprowadzania zmian) do lokalnego, samodzielnego projektu
Gene Bo,

Odpowiedzi:

159

Korzystając FileProviderz biblioteki pomocy technicznej, musisz ręcznie przyznać i odwołać uprawnienia (w czasie wykonywania) dla innych aplikacji do odczytu określonego Uri. Użyj metod Context.grantUriPermission i Context.revokeUriPermission .

Na przykład:

//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);

W ostateczności, jeśli nie możesz podać nazwy pakietu, możesz przyznać uprawnienia wszystkim aplikacjom, które obsługują określone intencje:

//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);
}

Alternatywna metoda wg dokumentacji :

  • Umieść identyfikator URI treści w intencji, wywołując metodę setData ().
  • Następnie wywołaj metodę Intent.setFlags () z FLAG_GRANT_READ_URI_PERMISSION lub FLAG_GRANT_WRITE_URI_PERMISSION lub z obydwoma.
  • Na koniec wyślij intencję do innej aplikacji. Najczęściej robisz to przez wywołanie metody setResult ().

    Uprawnienia przyznane w Intencji pozostają w mocy, dopóki stos działania odbierającego jest aktywny. Po zakończeniu stosu
    uprawnienia są automatycznie usuwane. Uprawnienia przyznane jednemu
    działaniu w aplikacji klienckiej są automatycznie rozszerzane na inne
    składniki tej aplikacji.

Przy okazji. jeśli potrzebujesz, możesz skopiować źródło FileProvider i zmienić attachInfometodę, aby uniemożliwić dostawcy sprawdzanie, czy jest eksportowany.

Leszek
źródło
26
Korzystając z grantUriPermissionmetody, musimy podać nazwę pakietu aplikacji, do której chcemy udzielić pozwolenia. Jednak podczas udostępniania zwykle nie wiemy, jaka aplikacja jest miejscem docelowym udostępniania.
Randy Sugianto „Yuku”
A co, jeśli nie znamy nazwy pakietu i po prostu chcemy, aby inne aplikacje mogły go otwierać
StuStirling,
3
Czy możesz podać działający kod dla najnowszego rozwiązania @Lecho?
StErMi
2
Czy istnieje błąd w śledzeniu, dlaczego wsparcie FileProvider nie działa z setFlags, ale działa z grantUriPermission? A może to nie jest błąd, w którym przypadku dlaczego to nie jest błąd?
NMR
1
Oprócz tego stwierdziłem, że wywołanie grantUriPermission nie działa dla intencji utworzonej przy użyciu ShareCompat, ale działa podczas ręcznego tworzenia intencji.
StuStirling,
33

W pełni działający przykład kodu, jak udostępniać plik z wewnętrznego folderu aplikacji. Testowane na Androidzie 7 i Androidzie 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>

Sam kod

    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);
Nie jeden
źródło
1
Użycie ShareCompat.IntentBuilderi pozwolenie na odczyt URI w końcu zrobiło to za mnie.
Cord Rehn
Przerwy dla innych wersji Androida
Oliver Dixon
@OliverDixon: które z nich? Przetestowałem to na Androidzie 6.0 i 9.0.
Violet Giraffe
1
To jedyna odpowiedź na to, FileProviderże działa. W przypadku innych odpowiedzi intencja jest niewłaściwa (nie używają jej ShareCompat.IntentBuilder), a aplikacje zewnętrzne nie mogą jej otworzyć w żaden znaczący sposób. Spędziłem dosłownie większość dnia na tych bzdurach, aż w końcu znalazłem twoją odpowiedź.
Violet Giraffe
1
@VioletGiraffe Nie używam też ShareCompat, ale mój działa. Mój problem polegał na tym, że przez pomyłkę przyznałem uprawnienia do odczytu intencji mojego wybierającego, kiedy musiałem przyznać uprawnienia do odczytu do mojej intencji PRZED przekazaniem jej intencji wybierającego, a także wybierającego. Mam nadzieję, że to ma sens. Tylko upewnij się, że udzielasz pozwolenia na odczyt we właściwej intencji.
Ryan
23

To rozwiązanie działa dla mnie od wersji OS 4.4. Aby działało na wszystkich urządzeniach, dodałem obejście dla starszych urządzeń. Dzięki temu zawsze stosowane jest najbezpieczniejsze rozwiązanie.

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>

Jawa:

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);
    }
}

Uprawnienie jest cofane za pomocą revokeFileReadPermission () w metodach onResume i onDestroy () fragmentu lub działania.

Oliver Kranz
źródło
1
To rozwiązanie działało u mnie, ale dopiero po dodaniu intent.setDataAndType (uri, "video / *"); BTW brak zmiennej załącznikUri to uri
EdgarK
Czy masz na myśli to, że filenie zmieni się z onResumena onDestroy?
CoolMind
16

Ponieważ, jak mówi Phil w swoim komentarzu do pierwotnego pytania, jest to wyjątkowe i nie ma innych informacji na temat SO w Google, pomyślałem, że powinienem również podzielić się moimi wynikami:

W mojej aplikacji FileProvider działał po wyjęciu z pudełka, aby udostępniać pliki przy użyciu zamiaru udostępniania. Nie była wymagana żadna specjalna konfiguracja ani kod, poza tym, aby skonfigurować FileProvider. W moim manifest.xml umieściłem:

    <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>

W my_paths.xml mam:

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="files" path="." />
</paths>

W swoim kodzie mam:

    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)));

Mogę bez problemu udostępniać mój magazyn plików w prywatnej pamięci aplikacji aplikacjom takim jak Gmail i Dysk Google.

Luke Sleeman
źródło
9
Niestety to nie działa. Ta zmienna fileToShare działa tylko wtedy, gdy plik istnieje na karcie SD lub w zewnętrznym systemie plików. Celem korzystania z FileProvider jest to, że można udostępniać zawartość z systemu wewnętrznego.
Keith Connolly
Wydaje się, że działa to poprawnie z lokalnymi plikami pamięci (w systemie Android 5+). Ale mam problemy na Androidzie 4.
Peterdk,
15

O ile wiem, będzie to działać tylko w nowszych wersjach Androida, więc prawdopodobnie będziesz musiał wymyślić inny sposób, aby to zrobić. To rozwiązanie działa dla mnie w 4.4, ale nie w 4.0 lub 2.3.3, więc nie będzie to przydatny sposób udostępniania treści dla aplikacji, która ma działać na dowolnym urządzeniu z Androidem.

W 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>

Zwróć szczególną uwagę na sposób określenia uprawnień. Musisz określić działanie, na podstawie którego utworzysz identyfikator URI, i uruchomić zamiar udziału, w tym przypadku działanie to nazywa się SharingActivity. Ten wymóg nie jest oczywisty z dokumentów 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>

Uważaj, jak określasz ścieżkę. Powyższe domyślnie dotyczy katalogu głównego Twojej prywatnej pamięci wewnętrznej.

W 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"));

W tym przykładzie udostępniamy obraz JPEG.

Na koniec prawdopodobnie dobrym pomysłem jest upewnienie się, że plik został prawidłowo zapisany i można uzyskać do niego dostęp za pomocą czegoś takiego:

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!");
}
Rasmusob
źródło
8

W mojej aplikacji FileProvider działa dobrze i mogę dołączyć wewnętrzne pliki przechowywane w katalogu plików do klientów poczty e-mail, takich jak Gmail, Yahoo itp.

W moim manifeście, jak wspomniano w dokumentacji Androida, umieściłem:

<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>

Ponieważ moje pliki były przechowywane w katalogu głównym plików, pliki filepaths.xml wyglądały następująco:

 <paths>
<files-path path="." name="name" />

Teraz w kodzie:

 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();
                }
            }

To zadziałało dla mnie, mam nadzieję, że to pomoże :)

Adarsh ​​Chithran
źródło
1
Jesteś prawdziwym MVP do wysyłania ścieżek = "." Pracował dla mnie
robsstackoverflow
Pojawia się błąd typu „Nie udało się znaleźć skonfigurowanego katalogu głównego zawierającego /” Użyłem tego samego kodu
Rakshith Kumar
5

Jeśli otrzymasz obraz z kamery, żadne z tych rozwiązań nie działa w systemie Android 4.4. W takim przypadku lepiej sprawdzić wersje.

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
źródło
1

tylko po to, aby poprawić odpowiedź podaną powyżej: jeśli otrzymujesz NullPointerEx:

możesz także użyć getApplicationContext () bez kontekstu

                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);
                }
Deepak Singh
źródło
1

Chcę udostępnić coś, co blokowało nas na kilka dni: kod dostawcy plików MUSI być wstawiony między znaczniki aplikacji, a nie po nim. Może to trywialne, ale nigdy nie zostało określone i pomyślałem, że mógłbym komuś pomóc! (dzięki jeszcze raz piolo94)

Eric Draven
źródło