If you’ve been working with Firebase on Android, you may have noticed that you don’t normally have to write any lines of code to initialize a feature. You just grab the singleton object for that feature, and start using it right away. And, in the case of Firebase Crash Reporting, you don’t even have to write any code at all for it to start capturing crashes! This question pops up from time to time, and I talked about it a bit at Google I/O 2016, but I’d also like to break it down in detail here.
The problem
Many SDKs need an Android Context
to be able to do their work. This Context is the hook into the Android runtime that lets the SDK access app resources and assets, use system services, and register BroadcastReceivers. Many SDKs ask you to pass a Context into a static init method once, so they can hold and use that reference as long as the app process is alive. In order to get that Context at the time the app starts up, it’s common for the developers of the SDK to ask you to pass that in a custom Application
subclass like this:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SomeSdk.init(this); // init some SDK, MyApplication is the Context
}
}
And if you hadn’t already registered a custom subclass in your app, you’d also have to add that to your manifest in the application tag’s android:name attribute:
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name="package.of.MyApplication"
... >
All this is fine, but Firebase SDKs make this a lot easier for its users!
The solution
There is a little trick that the Firebase SDKs for Android use to install a hook early in the process of an application launch cycle. It introduces a ContentProvider
to implement both the timing and Context needed to initialize an SDK, but without requiring the app developer to write any code. A ContentProvider
is a convenient choice for two reasons:
- They are created and initialized (on the main thread) before all other components, such as Activities, Services, and BroadcastReceivers, after the app process is started.
- They participate in manifest merging at build time, if they are declared in the manifest of an Android library project. As a result, they are automatically added to the app’s manifest.
Let’s investigate those two properties.
ContentProvider
initializes early
When an Android app process is first started, there is well-defined order of operations:
- Each
ContentProvider
declared in the manifest is created, in priority order. - The Application class (or custom subclass) is created.
- If another component that was invoked via some Intent, that is created.
When a ContentProvider
is created, Android will call its onCreate method. This is where the Firebase SDK can get a hold of a Context, which it does by calling the getContext method. This Context is safe to hold on to indefinitely.
This is also a place that can be used to set up things that need to be active throughout the app’s lifetime, such as ActivityLifecycleCallbacks (which are used by Firebase Analytics), or a UncaughtExceptionHandler (which is used by Firebase Crash Reporting). You might also initialize a dependency injection framework here.
ContentProviders participate in manifest merger
Manifest merge is a process that happens at build time when the Android build tools need to figure out the contents of the final manifest that defines your app. In your app’s AndroidManifest.xml file, you declare all your application components, permissions, hardware requirements, and so on. But the final manifest that gets built into the APK contains all of those elements from all of the Android library projects that your app depends on.
It turns out that ContentProviders are merged into the final manifest as well. As a result, any Android library project can simply declare a ContentProvider in its own manifest, and that entry will end up in the app’s final manifest. So, when you declare a dependency on Firebase Crash Reporting, the ContentProvider from its manifest is merged in your own app’s manifest. This ensures that its onCreate is executed, without you having to write any code.
FirebaseInitProvider
(surprise!) initializes your app
All apps using Firebase in some way will have a dependency on the firebase-common library. This library exposes FirebaseInitProvider
, whose responsibility is to call FirebaseApp.initializeApp
in order to initialize the default FirebaseApp
instance using the configurations from the project’s google-services.json file. (Those configurations are injected into the build as Android resources by the Google Services plugin.) However, If you’re referencing multiple Firebase projects in one app, you’ll have to write code to initialize other FirebaseApp
instances, as discussed in an earlier blog post.
Some drawbacks with ContentProvider
init
If you choose to use a ContentProvider
to initialize your app or library, there’s a couple things you need to keep in mind.
First, there can be only one ContentProvider
on an Android device with a given “authority” string. So, if your library is used in more than one app on a device, you have to make sure that they get added with two different authority strings, or the second app will be rejected for installation. That string is defined for the ContentProvider
in the manifest XML, which means it’s effectively hard-coded. But there is a trick you can use with the Android build tools to make sure that each app build declares a different authority.
There is a feature of Android Gradle builds call manifest placeholders that lets you declare and insert a placeholder value that get inserted into manifest strings. The app’s unique application ID is automatically available as a placeholder, so you can declare your ContentProvider
like this:
<provider
android:authorities="${applicationId}.yourcontentprovider"
android:name=".YourContentProvider"
android:exported="false" />
The other thing to know about about ContentProviders is that they are only run in the main process of an app. For a vast majority of apps, this isn’t a problem, as there is only one process by default. But the moment you declare that one of the Android components in your app must run in another process, that process won’t create any ContentProviders, which means your ContentProvider
onCreate will never get invoked. In this case, the app will have to either avoid calling anything that requires the initialization, or safely initialize another way. Note that this behavior is different than a custom Application subclass, which does get invoked in every process for that app.
But why misuse ContentProvider
like this?
Yes, it’s true, this particular application of ContentProvider
seems really weird, since it’s not actually providing any content. And you have to provide implementations of all the other ContentProvider
required methods by returning null. But, it turns out that this is the most reliable way to automatically initialize without requiring extra code. I think the convenience for developers using Firebase more than makes up for this strangeness of this use of a ContentProvider
. Firebase is all about being easy to use, and there’s nothing easier than no code at all!