Possible to use multiple authorities with FileProvider?
Asked Answered
L

2

53

Background

I maintain a library whose core functionality involves sharing programmatically-captured screenshots to external email applications.

I use a FileProvider to accomplish this, which means my library's manifest contains a <provider> tag:

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.bugshaker.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/filepaths" />
</provider>

filepaths.xml is defined as follows:

<paths>
    <files-path path="bug-reports/" name="bug-reports" />
</paths>

A consumer of my library has an application which itself uses a FileProvider to share files. My expectation was that it should be possible to allow both providers to share files if the consuming application used the following manifest <provider> tag:

<provider
    android:authorities="${applicationId}.fileprovider;${applicationId}.bugshaker.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true"
    android:name="android.support.v4.content.FileProvider"
    tools:replace="android:authorities">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"
        tools:replace="android:resource" />
</provider>

This manifest entry:

  • specifies two Provider authorities, ${applicationId}.fileprovider (for application file sharing) and ${applicationId}.bugshaker.fileprovider (for library file sharing);
  • references an updated filepaths.xml, which contains separate directory definitions for application-generated files and library-generated files:
<paths>
    <external-path
        name="redacted"
        path="" />
    <files-path
        name="bug-reports"
        path="bug-reports/" />
</paths>

After building the application, we have confirmed that the generated manifest has had the correct nodes replaced with these updated values.

However, when the application using this configuration is assembled (successfully) and run, we see a crash on launch:

E: FATAL EXCEPTION: main
   Process: com.stkent.bugshakertest, PID: 11636
   java.lang.RuntimeException: Unable to get provider android.support.v4.content.FileProvider: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.res.XmlResourceParser android.content.pm.PackageItemInfo.loadXmlMetaData(android.content.pm.PackageManager, java.lang.String)' on a null object reference
       at android.app.ActivityThread.installProvider(ActivityThread.java:5856)
       at android.app.ActivityThread.installContentProviders(ActivityThread.java:5445)
       at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5384)
       at android.app.ActivityThread.-wrap2(ActivityThread.java)
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1545)
       at android.os.Handler.dispatchMessage(Handler.java:102)
       at android.os.Looper.loop(Looper.java:154)
       at android.app.ActivityThread.main(ActivityThread.java:6119)
       at java.lang.reflect.Method.invoke(Native Method)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
    Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.res.XmlResourceParser android.content.pm.PackageItemInfo.loadXmlMetaData(android.content.pm.PackageManager, java.lang.String)' on a null object reference
       at android.support.v4.content.FileProvider.parsePathStrategy(FileProvider.java:583)
       at android.support.v4.content.FileProvider.getPathStrategy(FileProvider.java:557)
       at android.support.v4.content.FileProvider.attachInfo(FileProvider.java:375)
       at android.app.ActivityThread.installProvider(ActivityThread.java:5853)
       at android.app.ActivityThread.installContentProviders(ActivityThread.java:5445) 
       at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5384) 
       at android.app.ActivityThread.-wrap2(ActivityThread.java) 
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1545) 
       at android.os.Handler.dispatchMessage(Handler.java:102) 
       at android.os.Looper.loop(Looper.java:154) 
       at android.app.ActivityThread.main(ActivityThread.java:6119) 
       at java.lang.reflect.Method.invoke(Native Method) 
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886) 
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776) 

Using the debugger, I am able to see that the method FileProvider.parsePathStrategy invokes PackageManager.resolveContentProvider with the authority string "${applicationId}.fileprovider;${applicationId}.bugshaker.fileprovider". resolveContentProvider then returns null, leading to this NPE.

If I manually call resolveContentProvider while paused at this instruction and pass either "${applicationId}.fileprovider" or "${applicationId}.bugshaker.fileprovider", resolveContentProvider instead returns a non-null ProviderInfo instance (which would seem to be the expected result).

This difference confuses me because the <provider> element documentation states that multiple authorities are supported:

A list of one or more URI authorities that identify data offered by the content provider. Multiple authorities are listed by separating their names with a semicolon. To avoid conflicts, authority names should use a Java-style naming convention (such as com.example.provider.cartoonprovider). Typically, it's the name of the ContentProvider subclass that implements the provider

There is no default. At least one authority must be specified.

Questions

  • Is it possible to have a single application expose a FileProvider with multiple authorities and file paths?
    • If so, what do I need to change to make that work?
    • If not, are there other ways to configure file sharing within my library that avoid conflicts such as this one?
Leilaleilah answered 3/4, 2017 at 0:37 Comment(8)
"I am able to see that the method PackageItemInfo.loadXmlMetaData is being invoked with the authority string "${applicationId}.fileprovider;${applicationId}.bugshaker.fileprovider" -- you don't provide an authority string to loadXmlMetaData(), and I don't see that in the FileProvider source code. An authority is provided to resolveContentProvider() on the preceding line. Is that what you mean? If so, ProviderInfo is providing the semicolon-delimited list, and FileProvider doesn't seem to handle that.Valorievalorization
Beyond that, looking at the code in FileProvider, it would appear that they are not handling the multiple-authority scenario. They have hooks for having multiple path strategies by authority, but they never seem to parse the semicolon-delimited list. Probably untested. I have code in my StreamProvider that does parse the list, but I haven't tested it either. :-(Valorievalorization
Ok, updated. It seems odd that parsePathStrategy retrieves a new ProviderInfo instance at all when one is provided to the attachInfo method that calls it. I see that ContentProvider does split the authorities in attachInfo and that FileProvider calls to super, but the multiple-authorities field inside ContentProvider does not seem to be accessible to subclasses at all.Leilaleilah
Maybe a basic question, but what do apps that require different types of content providers normally do in the manifest? Are they required to create one "base" content provider and have that delegate appropriately?Leilaleilah
Final comment for the night; I'll test the StreamProvider in my sample app early this week and report back...Leilaleilah
"what do apps that require different types of content providers normally do in the manifest?" -- usually, they are entirely different classes. Few libraries ship with a ContentProvider; fewer still might be needed in concrete (i.e., registered in manifest form) by 2+ libraries or 1 other library and the app itself. Hence, I don't think there is a "normally". In this case, FileProvider (and StreamProvider) rely upon a static cache of data to manage multiple authorities, and so just using a simple subclass of FileProvider/StreamProvider would be insufficient.Valorievalorization
When using the StreamProvider I still get a crash on launch: Attempt to read from field 'android.os.Bundle android.content.pm.PackageItemInfo.metaData' on a null object reference. Sounds similar.Leilaleilah
Keep tabs on this issue.Valorievalorization
L
52

My solution to this problem has actually been to avoid relying on a single FileProvider parsing multiple authorities. While this doesn't directly address the question as stated, I'm posting it for posterity.


I updated my library to leverage an empty subclass of FileProvider, so that the library's updated manifest provider entry is now:

<provider
    android:name=".flow.email.screenshot.BugShakerFileProvider"
    android:authorities="${applicationId}.bugshaker.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/library_file_paths" />
</provider>

The merged manifest of an application that (1) uses a stock FileProvider and (2) consumes my library will now contain both of the entries shown below (no collision!):

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.consuming.application.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true" >
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/application_file_paths" />
</provider>

<provider
    android:name="com.github.stkent.bugshaker.flow.email.screenshot.BugShakerFileProvider"
    android:authorities="com.consuming.application.bugshaker.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true" >
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/library_file_paths" />
</provider>

I didn't realize that this was a potential solution until a coworker pointed it out. My assumption had previously (and incorrectly) been that all FileProviders in the manifest must set

android:name="android.support.v4.content.FileProvider"

but a quick check of the documentation revealed my error:

The name of the class that implements the content provider, a subclass of ContentProvider. This should be a fully qualified class name (such as, "com.example.project.TransportationProvider"). [...]

Leilaleilah answered 17/4, 2017 at 1:59 Comment(7)
How are you using getUriForFile(Context, "com.my.authority.fileprovider", file);? Are you using it within your library, if so how are you dynamically generating the authority string from the second parameter in .getUriForFile()? I'm using the FileProvider solely in my my library project so I have the authority string statically defined getUriForFile(Context, "com.my.authority.fileprovider", file); but it seems I might need to generate it dynamically.Anklet
If it helps, this is the repo in question: github.com/stkent/bugshaker-android. bugshaker module = library, example module = application. My library provider is only used inside the library, not by the consuming applications - it sounds like that's different from your setup?Leilaleilah
Let us continue this discussion in chat.Leilaleilah
what happen if library is gradle, not with project files?Psalter
@HardikJoshi you mean if you're pulling in a library whose source you don't control? The same sorts of collisions can occur and the same solution should work - making sure your app uses a different authority than the library does.Leilaleilah
In my case subclassing the ContentProvider didn't help, so i subclassed FileProvider and it worked like charmDoit
in case someone doesn't make it to the chat, you can avoid authority paths clashing by having a unique suffix for the library version. And call getUriForFile(context, applicationContext.getPackageName() + LIB_SUFFIX, file);Tomblin
T
1

i also face this issue and use this approach to resolve it. like i have a library image picker which use file provider and also my app use a file provider when i build my app conflict accur.

my file provide is

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="org.contentarcadeapps.photoeditor.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>

change this to

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="org.contentarcadeapps.photoeditor.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true"
            tools:replace="android:authorities">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"
                tools:replace="android:resource"/>
        </provider>
Tolly answered 15/2, 2018 at 12:54 Comment(2)
you're replacing other file providers with yours. It only works properly if other saved their files into the same directory as yours else other libs are not accessing the file where they saved it.Match
What is the better way to implement this ,I mean if library have one file provider and app have one fileProvider how to handle itCulosio

© 2022 - 2024 — McMap. All rights reserved.