Showing posts with label featured. Show all posts
Showing posts with label featured. Show all posts

Tuesday, 7 February 2017

Firebase Analytics Quick Tip: The 'value' Parameter

Todd Kerpleman
Todd Kerpelman
Developer Advocate
Firebase Analytics provides a lot of nice reports for you out of the box -- things like retention, active users, and some demographic information about the people using your app. And for specific events, you can see useful stats like the number of events fired over time or the average number of events per session. But one thing that you currently can't do (outside of BigQuery) is see values associated with any custom parameters that you send down in a Firebase Analytics event.

While the team is looking into ways they can make this experience better, there is one feature you can use right now to get some of the answers you're looking for. Every event you record in Firebase Analytics can accept a "value" parameter -- this can be a floating point number or integer -- and Firebase Analytics will give you back the average of this value over time.

For instance, let's say your fitness app is recording an end-of-workout event that looks a little like this[1]:

  FIRAnalytics.logEvent(withName: "workout_complete", parameters: [
"time": 1804
"exercise_type": "interval"
"intensity": 2
])
Within the Firebase Analytics reports, you'll be able to see the total number of workouts completed over time, the average number of workouts completed in a session, and the general age and gender breakdown of people completing workouts. But you can't, for instance, track the average amount of time people spent working out.



On the other hand, if you were to provide that same workout time information in a value parameter like so…

FIRAnalytics.logEvent(withName: "workout_complete", parameters: [
"time": 1804
"exercise_type": "interval"
"intensity": 2
kFIRParameterValue: 1804
])

...you'd be able to see this workout time over the lifetime of your app by looking at the "Value" graphs for the workout_complete event. In the example screenshot below, we can see that in the last 30 days, my users have completed about 1400 workouts (the "Event count" graph), and have spent a total of about 1.5 million seconds (the "Value" graph) doing so. That works out to about 17 minutes per workout.




Of course, the value parameter won't always give you all the information you need. If you were interested in comparing the average workout time for your interval workouts to the average workout time for your yoga workouts, you'd still need to use BigQuery to get that kind of data analysis. But I think you'll find you can get a surprising amount done just with the value parameter.


This value parameter can track completely different values for completely different events. So you could track average workout length in your workout_complete event, number of friends invited in an invite_friends event, or number of calories consumed in an eat_meal event.

I encourage you take a look at the events you're recording in your app and see if there's any useful information you might want to see that could be uncoveblack using these value parameters. You can check out our docs for more info, and find us on StackOverflow with the Firebase tag.

Happy tracking!

[1] I'm cheating a bit here. Swift 3 requires that I cast all of these values as NSObjects, but that tends to hide the point of this sample behind a lot of extra code, so just pretend I'm casting values appropriately. :)

Share:

Thursday, 2 February 2017

Auth flows with Firebase UI on the Web

Laurence Moroney
Laurence Moroney
Developer Advocate
Firebase Auth is a secure authentication system that allows users to sign up and sign in for your application. It allows you to use federated identity through providers, such as Facebook, Twitter, and of course - Google. When doing this, your users will demand a rich user experience, and the burden of implementing this will fall on you as a developer. In addition, creating apps that allow for sign in involves a lot more than just signing in -- there are many other user flows, such as choosing from multiple accounts, signing up for new accounts, resetting passwords and more. This can be a lot of work!

Luckily, the open-source FirebaseUI libraries make all of this really easy. In this post, you'll take a look at building a simple web site that allows for sign in and sign up. You'll use two providers: The built-in Email/Password on Firebase, and federated identity using Google Sign-In.

Getting Started

To use FirebaseUI, you need a Firebase app or site, and you created these on the Firebase Console. From here, select 'Authentication' on the left (1).

When you select 'Sign-In Method' at the top, you'll be given a list of Sign-in providers. Choose 'Email/Password' and 'Google' (2). Make sure they're enabled.


Additionally, if you are going to host these pages on your own domain, make sure the domain is added to the list of OAuth redirect domains at the bottom of the screen. (3).


Now, back on the Overview screen for your project, click 'Add Firebase to your Web app', and you'll see a popup like this:


This contains all the code you need to initialize Firebase on your web site. Let's take a look at a simple 2-page web site next: one page for signing in, and one page for after you've signed in.

A simple site with Sign In

I've built and hosted a simple site using FirebaseUI Auth here. The first page you see when you go to this site looks like this:
As you can see, it's pretty straightforward, with two sign-in options -- Google and Email, matching what we configured in the Firebase console.


Clicking Sign-In with Google can have multiple effects based on the context:

If you've signed in previously on this browser, you'll be taken straight back into the site
 
If you have multiple Google accounts, you'll be given the account chooser, with the choice to pick a different account, or create a new one, like this:

If you haven't signed in on this browser, and don't have a Google account, you'll be given the option to enter one, or create a new one, like this:
Clicking on the Create account link will take you through the standard user flow for creating an account, which will return you to your site to sign in when you're done.


Similarly when using Email/Password auth, you'll get the full user flow.


If you've signed in with an Email/Password combination before, and you're the only user, you'll go straight into the site.


If you've signed in with an Email/Password combination before, but aren't the only user, you'll get a list of accounts to choose from, like this:
This also gives you the facility to add a new account, and doing so creates a new account on firebase that you can use to sign in in future. There are many scenarios, with a very complex flow. To see a detailed flowchart of these, click here.


So let's look at the code needed to implement this!

Coding the Log In Page

Here's the full source code for the Log In page you've seen in this post. You'll see that there are two highlighted blocks. The first is the initialization code that you got from the Firebase console earlier. The second uses the FirebaseUI open source code to create the user interface widgets.


The things to note in the second block are the signInSuccessUrl parameter, which is the address of the page to redirect to once the sign in is successful. Also, note the signInOptions setting where Google Sign-In and Email Sign-In are configured.
<!DOCTYPE html>  
< html lang="en">
< head>
< title>EasyAuth</title>
<meta charset="UTF-8">
</head>
<!-- Below is the initialization snippet for my Firebase project. It will vary for each project -->
<script src="https://www.gstatic.com/firebasejs/3.6.4/firebase.js"></script>
<script>
// Initialize Firebase
var config = {
apiKey: "AIzaSyAPtNmUso5tA8d83vaJlgDHA_4C7HEgYNY",
authDomain: "authui-6818f.firebaseapp.com",
databaseURL: "https://authui-6818f.firebaseio.com",
storageBucket: "authui-6818f.appspot.com",
messagingSenderId: "596916061379"
};
firebase.initializeApp(config);
</script>
<!-- The code below initializes the sign-in widget from FirebaseUI web. -->
<script src="https://cdn.firebase.com/libs/firebaseui/1.0.0/firebaseui.js"></script>
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/1.0.0/firebaseui.css" />
<script type="text/javascript">
var uiConfig = {
signInSuccessUrl: 'loggedIn.html',
signInOptions: [
// Specify providers you want to offer your users.
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID
],
// Terms of service url can be specified and will show up in the widget.
tosUrl: '<your-tos-url>'
};
// Initialize the FirebaseUI Widget using Firebase.
var ui = new firebaseui.auth.AuthUI(firebase.auth());
// The start method will wait until the DOM is loaded.
ui.start('#firebaseui-auth-container', uiConfig);
</script>
<!-- Include a simple background image & and title -->
<div></div>
<body>
<h1 align="center" style="color:white;">Firebase Auth Quickstart Demo</h1>
<div id="firebaseui-auth-container"></div>
</body>
</html>



Note that this is all the code you need to implement to get the user flows mentioned earlier. Everything is encapsulated in the open source library.


The Signed-In page will need the first block of code in order to ensure that it uses Firebase, and then, when the firebase.auth().onAuthStateChanged event fires, you know the user is signed in, and you can query their exposed metadata.


Here's the code:
 

<!DOCTYPE html>
<html lang="en">
<head>
<title>EasyAuth</title>
<meta charset="UTF-8">
</head>
<!-- Below is the initialization snippet for my Firebase project. It will vary for each project -->
<script src="https://www.gstatic.com/firebasejs/3.6.4/firebase.js"></script>
<script>
// Initialize Firebase
var config = {
apiKey: "AIzaSyAPtNmUso5tA8d83vaJlgDHA_4C7HEgYNY",
authDomain: "authui-6818f.firebaseapp.com",
databaseURL: "https://authui-6818f.firebaseio.com",
storageBucket: "authui-6818f.appspot.com",
messagingSenderId: "596916061379"
};
firebase.initializeApp(config);
</script>
<body>
<!-- A simple example script to add text to the page that displays the user's Display Name and Email -->
<script>
// Track the UID of the current user.
var currentUid = null;
firebase.auth().onAuthStateChanged(function(user) {
// onAuthStateChanged listener triggers every time the user ID token changes.
// This could happen when a new user signs in or signs out.
// It could also happen when the current user ID token expires and is refreshed.
if (user && user.uid != currentUid) {
// Update the UI when a new user signs in.
// Otherwise ignore if this is a token refresh.
// Update the current user UID.
currentUid = user.uid;
document.body.innerHTML = '<h1> Congrats ' + user.displayName + ', you are done! </h1> <h2> Now get back to what you love building. </h2> <h2> Need to verify your email address or reset your password? Firebase can handle all of that for you using the email you provided: ' + user.email + '. <h/2>';
} else {
// Sign out operation. Reset the current user UID.
currentUid = null;
console.log("no user signed in");
}
});
</script>
<h1>Congrats you're done! Now get back to what you love building.</h1>
</html>



And that's it! Hopefully this primer will help you understand the power that you get with the FirebaseUI Auth libraries, and how they make it much easier for you to build web apps that sign in!



Share:

Firebase Cloud Messaging integrated into the Admin Node.js SDK

Jacob Wenger
Software Engineer
With Firebase, we've been working towards a world where developers don't have to deal with managing servers and can instead build web and mobile apps with only client-side code. However, there are times when you really do need to spin up your own server. Towards that aim, we announcedthe Firebase Admin SDKs this past November.
Today, I'm excited to share two new Admin SDK features:
  • The Node.js SDK now contains an Admin API for sending messages via Firebase Cloud Messaging (FCM).
  • The Java SDK can now be initialized from a set of built-in credentials, making it easier to use, especially on Google infrastructure.
Admin Node.js FCM API
The new Admin Node.js FCM API simplifies the process of sending messages via FCM. There is no extra setup required to use this new API as the existing credential used to authenticate the Node.js SDK handles everything on your behalf. The new API contains methods for sending messages to individual devices, device groups, topics, and conditions.
As an example, let's assume you are building an app for the upcoming Super Bowl and you want to send a notification to anyone subscribed to either the Atlanta Falcons' topic (/topics/falcons) or the New England Patriots' topic (/topics/patriots):
var admin = require("firebase-admin");

// Fetch the service account key JSON file contents
var serviceAccount = require("path/to/serviceAccountKey.json");

// Initialize the app with a service account, granting admin privileges
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://<DATABASE_NAME>.firebaseio.com"
});

// Define who to send the message to
var condition = "'falcons' in topics || 'patriots' in topics";

// Define the message payload
var payload = {
notification: {
title: "Super Bowl LI: Falcons vs. Patriots",
body: "Your team is Super Bowl bound! Get the inside scoop on the big game."
}
};

// Send a message to the condition with the provided payload
admin.messaging.sendToCondition(condition, payload)
.then(function(response) {
console.log("Successfully sent message! Server response:", response);
})
.catch(function(error) {
console.log("Error sending message:", error);
});
You can optionally provide a third option to any of the FCM methods to provide options for the message. For example, since the game is a little under a week away, let's send the message with high priority and give it a time to live of one week:
// condition and payload are the same as above

var options = {
priority: "high",
timeToLive: 60 * 60 * 24 * 7
};

admin.messaging.sendToCondition(condition, payload, options)
.then(function(response) {
console.log("Successfully sent message! Server response:", response);
})
.catch(function(error) {
console.log("Error sending message:", error);
});
This is just a taste of what the new Admin Node.js FCM API allows you to do. See Send Messages for more code samples and detailed documentation.
Admin Java Credential Interface
Since the launch of the Admin SDKs last November, the Node.js SDK has supported several initialization methods while the Java SDK has only allowed initialization via a service account certificate file. As of the latest Admin Java SDK release, both SDKs now provide a full credential interface with some helpful default implementations.
Upgrading to the new API is straightforward. The previous way of initializing the SDK is via the setServiceAccount() method:
FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccountCredentials.json");

FirebaseOptions options = new FirebaseOptions.Builder()
.setServiceAccount(serviceAccount)
.setDatabaseUrl("https://<DATABASE_NAME>.firebaseio.com")
.build();

FirebaseApp.initializeApp(options);
The updated way of initializing the SDK is via the setCredential() method.
FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccountCredentials.json");

FirebaseOptions options = new FirebaseOptions.Builder()
.setCredential(FirebaseCredentials.fromCertificate(serviceAccount))
.setDatabaseUrl("https://<DATABASE_NAME>.firebaseio.com")
.build();

FirebaseApp.initializeApp(options);
The Admin Java SDK now includes a credential implementation based on Google Application Default Credentials. This allows for auto-discovery of service account credentials on Google infrastructure like Google App Engine and Google Compute Engine. This means you don't need to manage service account credentials yourself. Instead, you can make use of Google Application Default Credentials to run the same exact code on your local, staging, and production environments, no configuration required.
FirebaseOptions options = new FirebaseOptions.Builder()
.setCredential(FirebaseCredentials.applicationDefault())
.setDatabaseUrl("https://<DATABASE_NAME>.firebaseio.com")
.build();

FirebaseApp.initializeApp(options);
See Initialize the SDK for more code samples and detailed documentation.
What's next for the Admin SDKs?
We are continually striving to expand our first-class support for backend developers in the Firebase ecosystem. Stay tuned for more features to be added to the Firebase Admin SDKs in the future! If you'd like to see a specific feature, let us know by sending us a note through our feature request support channel.
Share:

Tuesday, 31 January 2017

Debugging Firebase Cloud Messaging on iOS

Todd Kerpleman
Todd Kerpelman
Developer Advocate
Debugging Firebase Cloud Messaging is one of the most common Firebase-on-iOS questions I see on StackOverflow. And so in an effort to garner as many StackOverflow points as I can (oh, yeah, and to educate the developer community), I thought it might be helpful to write up a full debugging guide on what to do when you can't seem to get Firebase Cloud Messaging (FCM) working on your iOS device. First off, I'd recommend taking a moment to watch our Understanding FCM on iOS video. It'll give you a better idea of what's going on underneath the hood, which is always useful when you're trying to debug things. Go ahead! I'll wait.



Okay, back? So you probably noticed in the video that we have several systems all talking to each other:
  1. Your app server (or Firebase Notifications) talks to Firebase Cloud Messaging
  2. Firebase Cloud Messaging then talks to APNs
  3. APNs talks to your user's target device
  4. On your user's target device, iOS communicates with your app.


These four paths of communication means there are four opportunities for things to go wrong. And when they do, they very often manifest as a frustrating "It says my notification was sent, but nothing showed up on my device" kind of bug, which requires some real detective work. So here are the steps I recommend you go through to start tracking down these errors.

1. Temporarily disable any connectToFCM() calls

If you'll recall from the video, your app can explicitly connect to Firebase Cloud Messaging by calling connectToFCM()when it's in the foreground, which allows it to receive data-only messages that don't have a content-available flag directly through FCM.
And while this can be useful in certain circumstances, I recommend disabling this while you're debugging. Simply because it's one extra factor that you want to eliminate. I've seen a few, "My app receives notifications in the foreground, but not in the background" problems out there that probably occurred because the developer was only receiving messages through the FCM channel, and their APNs setup was never actually working properly .
If things break at this point: If you suddenly go from "Hey, I was receiving foreground notifications" to "I'm not receiving any notifications at all", then this is a sign your app was never properly set up to receive notifications from APNs in the first place. So your app might be a little more broken than before, but at least now it's consistently broken. (Hooray!) Keep reading to debug your APNs implementation!
For the next several steps, we're going to go backwards through that "Notifications to FCM to APNs to iOS to your app" chain. Let's start by making sure that iOS can actually speak to your app...

2. Add some good ol' fashioned print() debugging

Thanks to some clever method swizzling, Firebase Cloud Messaging makes it totally unnecessary for you to implement either application(_:didRegisterForRemoteNotificationsWithDeviceToken:) or application(_:didFailToRegisterForRemoteNotificationsWithError:) in your app delegate.
However, for debugging purposes, I like to add in these methods and print out some debug information to see if there are any errors happening that I should be aware of. Start by adding some debug output to your failure method. Something like this:
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Oh no! Failed to register for remote notifications with error \(error)")
}
In theory, any error message printed out here will also probably be printed out by the FCM client library, but I like having my own messages because I can search for specific strings (like "Oh no!" in the above example) among all the Xcode output. This also gives me a handy line of code where I can add a breakpoint.
While you're at it, in your didRegister... method, go ahead and print out a human-readable version of your device token:
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
var readableToken: String = ""
for i in 0..<deviceToken.count {
readableToken += String(format: "%02.2hhx", deviceToken[i] as CVarArg)
}
print("Received an APNs device token: \(readableToken)")
}
You don't need to disable method swizzling or anything to add these debug methods. Firebase will go ahead and call them just as soon as it's done calling its own set of methods.
If you're seeing an error message at this point: If you either receive an error message or don't get back a device token, check the error message for a good hint as to what went wrong. Most mistakes at this point fall under the, "I'm embarrassed to tell anybody why it wasn't working" category. Things like:
  • Testing on the iOS simulator and not the device.
  • Forgetting to enable Push Notifications in your Xcode project settings.
  • Not calling application.registerForRemoteNotifications() when your app starts up.
Sure, these are simple mistakes, but without the benefit of printing out messages to the Xcode console, it's easy for them to go unnoticed.

3. Confirm that you can send user-visible notifications

As with any iOS app, you need to explicitly get the user's permission to show any kind of notification alert or sound. If you're in a situation where your app doesn't appear to be receiving notification messages in the background, your app simply might not have permission from iOS to do so.
You can check this in iOS >= 10 by adding the following code somewhere in your app.
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
print("Alert setting is \(settings.alertSetting ==
UNNotificationSetting.enabled ? "enabled" : "disabled")")
print("Sound setting is \(settings.soundSetting ==
UNNotificationSetting.enabled ? "enabled" : "disabled")")
}
If you're seeing "disabled" messages at this point: Either you accidentally denied granting your app permission to send you notifications, or you never asked for permission in the first place.
If you accidentally clicked on the "Don't allow" button when the app asked you for permission to send notifications, you can fix this by going to Settings, finding your app, clicking on Notifications, then clicking the Allow Notifications switch.

                                   

On the other hand, if you never asked for permission to show user-visible permissions, then it means you need to add code like this (for iOS >= 10) within your app somewhere:
let authOptions : UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(options: authOptions)
{ (granted, error) in
if (error != nil) {
print("I received the following error: \(error)")
} else if (granted) {
print ("Authorization was granted!")
} else {
print ("Authorization was not granted. :(")
}
}
But if everything looks good at this point, you can move on to debugging the APNs connection!

4. Make a call directly through APNs

Remember; just because you're using FCM to handle your notifications doesn't mean you can't also use APNs directly. There are a few ways you can try this; one option is to use an open-source tool like NWPusher to send test notifications. But personally, I prefer sending APNs requests directly through a curl call.
Making an APNs curl request is easier these days now that APNs supports HTTP/2. But it does mean you'll need to make sure your copy of curl is up-to-date. To find out, run curl --version. You'll probably see something like this:
curl 7.47.1 (x86_64-apple-darwin15.6.0) libcurl/7.47.1 OpenSSL/1.0.2f zlib/1.2.5 nghttp2/1.8.0
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets
If you want to talk to APNs, you'll need a version of curl that's greater than 7.43, and you'll need to see HTTP2 among the features. If your version of curl doesn't meet these requirements, you'll need to update it. This blog post by Simone Carletti gives you some pretty nice instructions on how to do that.
Next, you'll need to convert the .p12 file you downloaded from the Apple Developer Portal to a .pem file. You can do that with this command:
openssl pkcs12 -in MyApp_APNS_Certificate.p12 -out myapp-push-cert.pem -nodes -clcerts
You'll also need the APNs device token for your target device. If you added the debug text listed earlier in your application(_:didRegisterForRemoteNotificationsWithDeviceToken:) method, you'll be able to grab this from your Xcode console. It should look something like ab8293ad24537c838539ba23457183bfed334193518edf258385266422013ac0d
Now you can make a curl call that looks something like this:
> curl --http2 --cert ./myapp-push-cert.pem \
-H "apns-topic: com.example.yourapp.bundleID" \
-d '{"aps":{"alert":"Hello from APNs!","sound":"default"}}' \
https://api.development.push.apple.com/3/device/ab8293ad24537c838539ba23457183bfed334193518edf258385266422013ac0d
Three things to notice here:
  1. That --cert argument should link to the .pem file you created in the previous step.
  2. For the apns-topic, include the bundle ID of your app. And yes, the concept of apns-topics are completely different than Firebase Cloud Messaging topics.
  3. Make sure your device ID is included at the end of that URL there. Don't just copy-and-paste the one that I have. It won't work.
If all has gone well, you'll see a push notification on the device, and you can move on to the next step. If not, here's a few things to look for:
  1. Are you getting back any error message from APNs? That's a pretty good sign something has gone wrong. Common messages include:
    1. "Bad device token" -- This is what it sounds like. Your device token is incorrect. Double-check that you've copied it correctly from your app
    2. "Device token not for topic" -- This might mean that your topic isn't properly set to your app's bundle ID. But it also might mean that you're not using the correct certificate here -- I've gotten this message when I've used the wrong .pem file.
  2. Is your app in the background? Remember that iOS will not automatically show notification alerts or sounds if your app is in the foreground.
    1. However, in iOS 10, they've made it significantly easier to have iOS show these alerts even if your app is in the foreground. You just need to call completionHandler([.alert]) at the end of userNotificationCenter(_:willPresent:withCompletionHandler:)
  3. Are you sending valid APNs requests? There are a few types of requests that, while syntactically correct, may still get rejected. At the time of this writing, these include sending silent notifications that don't include the content-available flag, or sending silent notifications high priority.
    1. In addition, iOS may throttle silent notifications if your app neglects to call its completionHandler in a reasonable amount of time upon receiving them or uses too much power to process these notifications. Refer to Apple's documentation for more information.
  4. Is APNs having issues? You can double-check the status of APNs and the APNs Sandbox over at https://developer.apple.com/system-status/
But if things seem to be working correctly here, it's time to move on to the next step...

5. Make a curl call directly through FCM

Once you've confirmed your APNs call seems to be working properly, the next step is to confirm the FCM part of the process is working. For that, I also like to make another curl call. For this to work, you're going to need two things: The server key and the FCM device token of your target device.
To get the server key, you'll need to go to the Cloud Messaging settings of your project in the Firebase Console. Your server key should be listed there as a giant 175-character string.



Getting your FCM device token is slightly more work. When your app first receives an APNs token, it will send that off to the FCM servers in exchange for an FCM device token. When it gets this FCM token back, the FCM library will trigger an "Instance ID Token Refresh" notification. 1
So listening to this firInstanceIDTokenRefresh NSNotification will let you know what your FCM device token is, but this notification only gets triggered when your device token changes. This happens infrequently -- like when you switch from a debug to production build, or when you run your app for the first time. Otherwise, this notification will not be called.
However, you can retrieve your cached FCM device token simply through the InstanceID library, which will give you any stored device token if it's available. So to get your latest-and-greatest FCM token, you'll want to write some code like this:
  func application(_ application: UIApplication, didFinishLaunchingWithOptions
// ...
printFCMToken() // This will be nil the first time, but it will give you a value on most subsequent runs
NotificationCenter.default.addObserver(self,
selector: #selector(tokenRefreshNotification),
name: NSNotification.Name.firInstanceIDTokenRefresh,
object: nil)
application.registerForRemoteNotifications()
//...
}

func printFCMToken() {
if let token = FIRInstanceID.instanceID().token() {
print("Your FCM token is \(token)")
} else {
print("You don't yet have an FCM token.")
}
}

func tokenRefreshNotification(_ notification: NSNotification?) {
if let updatedToken = FIRInstanceID.instanceID().token() {
printFCMToken()
// Do other work here like sending the FCM token to your server
} else {
print("We don't have an FCM token yet")
}
}
The very first time your app runs, you'll see a message that you don't have an FCM token, followed by a message a short time later with your actual token. In subsequent runs, you should see your cached token right away. It's a 153-character random string that looks a lot like your server key, so don't get 'em confused.
So, now that you have both pieces of information, you can make a curl call. Try calling something like this:
> curl --header "Content-Type: application/json" \
--header "Authorization: key=AU...the rest of your server key...s38txvmxME-W1N4" \
https://fcm.googleapis.com/fcm/send \
-d '{"notification": {"body": "Hello from curl via FCM!", "sound": "default"},
"priority": "high",
"to": "gJHcrfzW2Y:APA91...the rest of your FCM token...-JgS70Jm"}'
If all has gone well, you'll see a notification on your device, as well as receive a "Success" response from FCM.
{"multicast_id":86655058283942579,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1486683492595106961%9e7ad9838bdea651f9"}]}
Don't get too excited by this success response. That just means FCM has successfully received your message; it doesn't mean that it has successfully delivered it to APNs. You really want to look for the notification on your device.
If your notification doesn't seem to be getting received, here's a few things to look out for:
  • Are you seeing an error message in your response? Don't ignore those. They're usually pretty big hints as to what's going on.
    • InvalidRegistration means you don't have the correct FCM device token. (Remember, it's actually called a "registration token")
    • A 401 error with the message "The request's Authentification (Server-) Key contained an invalid or malformed FCM-Token" means the server key is probably incorrect. Make sure you've copied the whole thing correctly from the Firebase console.
  • Is your priority set to high? Android and iOS devices have different interpretations of medium and high priority values.
  • On Android, medium priority basically means, "Send the message, but be respectful of your user's device if it's in doze mode". This is generally why FCM uses "medium" as its default priority if you don't specify one.
    • On iOS, medium (or 5) priority can best be summarized as, "Maybe we'll send it at some point. But in this crazy world of ours, who can say for sure?!? ¯\_(ツ)_/¯".
    • This is why APNs defaults to a priority value of 10 (or "high") when no priority value is specified and they really only ask you to send messages medium priority when you're sending a data-only content-available message.
    • Ideally, you should send most user-visible messages with medium priority to Android devices and high priority to iOS devices. If you're using the Firebase Notifications panel, you can do this pretty easily.
  • Are you using APNs syntax instead of FCM syntax? While FCM will properly translate FCM-speak to APNs, it will get confused if you send it APNs syntax in the first place. So double-check that you're sending messages properly formatted for FCM. In particular, confirm that you're setting "priority" to "high" and not "10".
    • If you're sending a content available message, make sure you're specifying "content_available": true with an underscore and not "content-available": 2
    • I also recommend trying to send a Notification through the Firebase Notifications panel at this point. If you can make a call through Notifications but not through a curl call, it might be a sign that your message isn't properly formatted.
  • Have you uploaded your APNs certificate to the Firebase console? And has it expired? Remember, FCM needs that certificate in order to communicate with APNs.

6. Make a call through the Notifications panel and/or your server

So if you've gotten to this point, we've basically established that the path from FCM to APNs to iOS to your app is working correctly. So I would be very surprised if the Notifications panel wasn't working at this point. If it is, the best thing to do is check status.firebase.google.com and see if there are any issues with the Cloud Messaging service. (This also includes the Notifications panel)
If the problem is with your server code, well… that's up to you and your server. But now that you've figured out exactly what data you need to generate to make a proper FCM call, I'm hoping you can tackle this part on your own with confidence. Or, at least, the appearance of confidence, which is enough to fool most people.

Whew! Well, I know that was a lot to go through, and hopefully you were able to stop at, like, step 2 because it turns out you forgot to flip some switch in your Xcode project. If that's the case, you're probably not even reading this conclusion. Come to think of it, if you made it this far, it's probably because you're still having problems with your implementation, in which case… uh… go check out our support channels. Or ask @lmoroney, because I'm basically out of suggestions at this point.

Thanks for reading!

[1] That's an NSNotification, not an APNs notification. Hooray for overloaded terms!
[2] One interesting error I ran into was a developer who couldn't understand why his content-available messages were only being received when his app was in the foreground. It turns out he had explicitly connected to FCM (like in step 1) and was using the (invalid) "content-available" key in his message. Since FCM didn't translate this into a valid APNs content-available message, it interpreted it as a data-only message that should only be sent over FCM, which is why it only worked when his app was in the foreground.
Share:

Tuesday, 27 December 2016

Building a NativeScript + Angular 2 Mobile App using Firebase


Jen Looper
Developer Advocate at ProgressSW
Guest blogger Jen Looper
The powerful combination of NativeScript, Firebase, and Angular 2 can kickstart your app building into high gear, especially during the holidays when you find yourself confronted with the need to speed up your app development AND meet your family's gift-giving needs! Just in time, I am happy to presentto you (see what I did there 🎁) a demo of how to leverage Firebase in your Angular 2-powered NativeScript apps using several elements of Eddy Verbruggen's famousNativeScript-Firebase plugin.
In this tutorial, I'm going to show you how to use four popular Firebase elements in your NativeScript app: Authentication with a login and registration routine; Database for data storage and real-time updates; Remote Config to make changes to an app remotely; and Storage for saving photos. To do this, I decided to rewrite my Giftler app, originally written in Ionic.
Before we get started, I encourage you to read through thedocumentation before starting in on your project, and make sure that a few prerequisites are in place:
  • Ensure thatNativeScript is installed on your local machine and that the CLI works as expected
  • Configure your preferred IDE for NativeScript and Angular development. You're going to need TypeScript, so ensure that your transpiling process is working. There are excellent NativeScript plugins available forVisual Studio, Visual Studio Code, andJetbrains-compatible IDEs, among others. Visual Studio Code in particular has handysnippets that speed up development
  • Log in to your Firebase account and find your console
  • Create a new project in the Firebase console. I named mine 'Giftler'. Also create an iOS and Android app in the Firebase console. As part of this process you'll download both a GoogleServices-Info.plist and google-services.json file. Make sure you note where you place those files, and you'll need them in a minute.

Install the dependencies

I've built Giftler as an example of an authenticated NativeScript app where users can list the gifts that they would like to receive for the holidays, including photos and text descriptions. For the time being, this app does the following on iOS and Android:
  • allows login and logout, registration, and a 'forgot password' routine
  • lets users enter gift items into a list
  • lets users delete items from a list
  • lets users edit items in the list individually by adding descriptions and photos
  • provides messaging from the Remote Config service in Firebase that can be quickly changed in the backend
Now, fork the Giftler source code, which is a complete and functional app. Once your app is cloned, replace the app's current Firebase-oriented files that you downloaded when you created your app:
  • In the /app/App_Resources/Android folder, put the google.services.json file that you downloaded from Firebase.
  • Likewise, in /app/App_Resources/iOS folder, put the GoogleService-Info.plist file also downloaded from Firebase.
These files are necessary to initialize Firebase in your app and connect it to the relevant external services.
Now, let's take a look at the package.json at the root of this app. It contains the plugins that you'll use in this app. I want to draw your attention to the NativeScript-oriented plugins:
"nativescript-angular": "1.2.0",
"nativescript-camera": "^0.0.8",
"nativescript-iqkeyboardmanager": "^1.0.1",
"nativescript-plugin-firebase": "^3.8.4",
"nativescript-theme-core": "^1.0.2",
The NativeScript-Angular plugin is NativeScript's integration of Angular. The Camera plugin makes managing the camera a bit easier. IQKeyboardManager is an iOS-specific plugin that handles the finicky keyboard on iOS. The Theme plugin is a great way to add default styles to your app without having to skin the app entirely yourself. And finally, the most important plugin in this app is the Firebase plugin.
With the dependencies in place and the plugins ready to install, you can build your app to create your platforms folder with iOS and Android-specific code and initialize the Firebase plugin along with the rest of the npm-based plugins. Using the NativeScript CLI, navigate to the root of your cloned app and type tns run ios or tns run android. This will start the plugin building routines and, in particular, you'll see the various parts of the Firebase plugin start to install. The install script that runs will prompt you to install several elements to integrate to the various Firebase services. We're going to select everything except Messaging and social authentication for the moment. A great feature is that a firebase.nativescript.json file is installed at the root of the app, so if you need to install a new part of the plugin later, you can edit that file and reinstall the plugin.
At this point, if you run tns livesync ios --watch or tns livesync android --watch to see the app running on an emulator and watching for changes, you would see a the app running and ready to accept your new login. Before you initialize a login, however, ensure that Firebase handles Email/Password type logins by enabling this feature in the Firebase console in the Authentication tab:
Let's take a look under the covers a bit to see what's happening behind the scenes. Before you can log in to Firebase, you need to initialize the Firebase services that you installed. In app/main.ts, there are a few interesting bits.
// this import should be first in order to load some required settings (like
globals and reflect-metadata)
import { platformNativeScriptDynamic } from "nativescript-angular/platform";
import { AppModule } from "./app.module";
import { BackendService } from "./services/backend.service";


import firebase = require("nativescript-plugin-firebase");


firebase.init({
//persist should be set to false as otherwise numbers aren't returned during
livesync
persist: false,
storageBucket: 'gs://giftler-f48c4.appspot.com',
onAuthStateChanged: (data: any) => {
console.log(JSON.stringify(data))
if (data.loggedIn) {
BackendService.token = data.user.uid;
}
else {
BackendService.token = "";
}
}
}).then(
function (instance) {
console.log("firebase.init done");
},
function (error) {
console.log("firebase.init error: " + error);
}
);
platformNativeScriptDynamic().bootstrapModule(AppModule);




First, we import firebase from the plugin, and then we call .init(). Edit the storageBucket property to reflect the value in the Storage tab of your Firebase console:
Now your app is customized to your own Firebase account and you should be able to register a new user and login in the app. You can edit the user.email and password variables in app/login/login.component.ts file to change the default login credentials from user@nativescript.org to your own login and password if you like.
The iOS and Android login screens
Note: you should be able to emulate your app right away on iOS, using the Xcode simulator.  On Android, make sure that you select an Android SDK emulator image that supports Google Services, such as "Google APIs Intel x86 Atom System Image." It can be hard to get the versions lined up perfectly, so pay close attention to the Firebase dependencies and version info.

Code Structure and Authentication

Angular 2 design patterns require that you modularize your code, so we will oblige by using the following code structure:
—login
  1. login.component.ts
  2. login.html
  3. login.module.ts
  4. login.routes.ts
—list …
—list-detail …
—models
  1. gift.model.ts
  2. user.model.ts
  3. index.ts
—services
  1. backend.service.ts
  2. firebase.service.ts
  3. utils.service.ts
  4. index.ts
app.component.ts
app.css
app.module.ts
app.routes.ts
auth-guard.service.ts
main.ts
I want to draw your attention to the way Firebase authentication works with the Angular 2 auth-guard.service. When Firebase is initialized in your app in app/main.ts as we saw above, the onAuthStateChanged function is called:
onAuthStateChanged: (data: any) => {
console.log(JSON.stringify(data))
if (data.loggedIn) {
BackendService.token = data.user.uid;
}
else {
BackendService.token = "";
}
}
When the app starts, check the console for the stringified data being returned by Firebase. If this user is flagged as being loggedIn, we will simply set a token which is the userId sent back by Firebase. We'll use the NativeScript application settings module, which functions like localStorage, to keep this userId available and associate it to the data that we create. This token and the authentication tests that use it, managed in the app/services/backend.service.tsfile, are made available to the app/auth-guard.service.ts file. The auth-guard file offers a neat way to manage logged-in and logged-out app state.
The AuthGuard class implements the CanActivate interface from the Angular Router module.
export class AuthGuard implements CanActivate {
constructor(private router: Router) { }
canActivate() {
if (BackendService.isLoggedIn()) {
return true;
}
else {
this.router.navigate(["/login"]);
return false;
}
}
Essentially, if the token is set during the above login routine, and the BackendService.isLoggedIn function returns true, then the app is allowed to navigate to the default route which is our wish list; otherwise, the user is sent back to login:
const listRoutes: Routes = [
{ path: "", component: ListComponent, canActivate: [AuthGuard] },
];
Now that you have initialized your Firebase-powered NativeScript app, let's learn how to populate it with data and use Firebase's amazing realtime power to watch for the database to be updated.

Making your list, checking it twice

Starting in app/list/list.html, which is the basis of the wish list, you'll see a textfield and a blank list. Go ahead, tell Santa what you want! The items are sent to the database and added to your list in realtime. Let's see how this is done.
First, note that in app/list/list.component.ts, we set up an observable to hold the list of gifts:
public gifts$: Observable;
then, we populate that list from the database when the component is initialized:
ngOnInit(){
this.gifts$ = this.firebaseService.getMyWishList();
}
It's in the firebaseService file that things get interesting. Note the way that this function adds a listener and returns an rxjs observable, checking for changes on the Gifts collection in the Firebase database:
getMyWishList(): Observable {
return new Observable((observer: any) => {
let path = 'Gifts';
let onValueEvent = (snapshot: any) => {
this.ngZone.run(() => {
let results = this.handleSnapshot(snapshot.value);
console.log(JSON.stringify(results))
observer.next(results);
});
};
firebase.addValueEventListener(onValueEvent, `/${path}`);
}).share();
}
The results of this query are handled in a handleSnapshot function below, which filters the data by user, populating an _allItems array:
handleSnapshot(data: any) {
//empty array, then refill and filter
this._allItems = [];
if (data) {
for (let id in data) {
let result = (Object).assign({id: id}, data[id]);
if(BackendService.token === result.UID){
this._allItems.push(result);
}
}
this.publishUpdates();
}
return this._allItems;
}
And finally, publishUpdates is called, which sorts the data by date so that newer items are shown first:
publishUpdates() {
// here, we sort must emit a *new* value (immutability!)
this._allItems.sort(function(a, b){
if(a.date < b.date) return -1;
if(a.date > b.date) return 1;
return 0;
})
this.items.next([...this._allItems]);
}
Once the data has populated your $gifts observable, you can edit and delete elements of it and it will be handled by the listener and the front end updated accordingly. Note that the onValueEvent function of getMyWishList method includes the use ofngZone which ensures that, although data updates occur asynchronously, the UI is updated accordingly. A good overview of ngZone in NativeScript apps can be foundhere.

Remotely Configured Messages from Beyond

Another cool piece of Firebase's service includes "Remote Config", a way to provide app updates from the Firebase backend. You can use Remote Config to toggle features on and off in your app, make UI changes, or send messages from Santa, which is what we're going to do!
In app/list/list.html, you'll find a message box:
<Label class="gold card" textWrap="true" [text]="message$ | async"></Label>
The message$ observable is built in much the same way as the data list; changes are picked up in this case each time the app is freshly initialized:
ngOnInit(){
this.message$ = this.firebaseService.getMyMessage();
}
And the magic occurs in the service layer (app/services/firebase.service.ts ):
getMyMessage(): Observable{
return new Observable((observer:any) => {
firebase.getRemoteConfig({
developerMode: false,
cacheExpirationSeconds: 300,
properties: [{
key: "message",
default: "Happy Holidays!"
}]
}).then(
function (result) {
console.log("Fetched at " + result.lastFetch + (result.throttled ? "
(throttled)" : ""));
for (let entry in result.properties)
{
observer.next(result.properties[entry]);
}
}
);
}).share();
}

Publish new messages as often as you like!
Note: tinkering repeatedly with Remote Config may cause throttling of your Firebase instance, so develop with care

Take a picture!

One of the more interesting parts of this project, I think, is the ability to take a picture of your present of choice and store it in Firebase Storage. I leveraged the Camera plugin, as mentioned above, which makes managing the hardware a little easier. To start, ensure that your app has access to the device camera by getting permissions set in the ngOnInit() method in app/list-detail/list-detail.component.ts:
ngOnInit() {
camera.requestPermissions();
...
}
A chain of events begins when the user clicks the 'Photo' button in the detail screen. First,
takePhoto() {
let options = {
width: 300,
height: 300,
keepAspectRatio: true,
saveToGallery: true
};
camera.takePicture(options)
.then(imageAsset => {
imageSource.fromAsset(imageAsset).then(res => {
this.image = res;
//save the source image to a file, then send that file path to
firebase
this.saveToFile(this.image);
})
}).catch(function (err) {
console.log("Error -> " + err.message);
});
}
The camera takes a picture, and then that photo is stored as an imageAsset and displayed on the screen. The image is then named with a date stamp and saved to a file locally. That path is reserved for future use.
saveToFile(res){
let imgsrc = res;
this.imagePath =
this.utilsService.documentsPath(`photo-${Date.now()}.png`);
imgsrc.saveToFile(this.imagePath, enums.ImageFormat.png);
}
Once the 'Save' button is pressed, this image, via its local path, is sent to Firebase and saved in the storage module. Its full path in Firebase is returned to the app and stored in the /Gifts database collection:
editGift(id: string){
if(this.image){
//upload the file, then save all
this.firebaseService.uploadFile(this.imagePath).then((uploadedFile: any) =>
{
this.uploadedImageName = uploadedFile.name;
//get downloadURL and store it as a full path;
this.firebaseService.getDownloadUrl(this.uploadedImageName).then((downloadUrl:
string) => {
this.firebaseService.editGift(id,this.description,downloadUrl).then((result:any)
=> {
alert(result)
}, (error: any) => {
alert(error);
});
})
}, (error: any) => {
alert('File upload error: ' + error);
});
}
else {
//just edit the description
this.firebaseService.editDescription(id,this.description).then((result:any)
=> {
alert(result)
}, (error: any) => {
alert(error);
});
}
}
This chain of events seems complicated, but it boils down to a few lines in the Firebase service file:
uploadFile(localPath: string, file?: any): Promise {
let filename = this.utils.getFilename(localPath);
let remotePath = `${filename}`;
return firebase.uploadFile({
remoteFullPath: remotePath,
localFullPath: localPath,
onProgress: function(status) {
console.log("Uploaded fraction: " + status.fractionCompleted);
console.log("Percentage complete: " + status.percentageCompleted);
}
});
}
getDownloadUrl(remoteFilePath: string): Promise {
return firebase.getDownloadUrl({
remoteFullPath: remoteFilePath})
.then(
function (url:string) {
return url;
},
function (errorMessage:any) {
console.log(errorMessage);
});
}
editGift(id:string, description: string, imagepath: string){
this.publishUpdates();
return firebase.update("/Gifts/"+id+"",{
description: description,
imagepath: imagepath})
.then(
function (result:any) {
return 'You have successfully edited this gift!';
},
function (errorMessage:any) {
console.log(errorMessage);
});
}
The end result is a nice way to capture both photos and descriptions of the gifts for your wish list. No more excuses that Santa didn't know exactly WHICH Kylie Eyeliner to buy. By combining the power of NativeScript and Angular, you can create a native iOS and Android app in a matter of minutes. By adding Firebase you have a powerful way of storing your app's users, images and data, and a way of updating that data in real-time across devices. Cool, huh? It looks like this:
We are well on our way to create a solid wishlist management app! It remains to figure out the best way to inform Santa of our wishes - a Mailgun email integration or using push notifications would be the obvious next route. In the meantime, best wishes for a wonderful holiday season, and I hope you have a great time creating awesome NativeScript apps using Firebase!
Want to learn more about NativeScript? Visit http://www.nativescript.org. If you need help, join the NativeScript Slack channel here.
Share:

Monitoring Santa Tracker with Firebase

Doug Stevenson
Sam Stern
Developer Programs Engineer

The Santa Tracker app for Android is a Google holiday tradition. Every year, millions of people around the world use the app to play games with elves and reindeer and, of course, track Santa, as he flies around the world on December 24th. While the app is live for a few months each year, about 90% of our usage occurs in the last two weeks of December. In order to turn around improvements to Santa Tracker quickly over this time, it's critical that we can monitor and adjust the Santa Tracker app remotely. This year, we decided to go all-in with Firebase as our monitoring solution. In this blog post, I'll talk about how we use a combination Analytics, Crash Reporting, and Remote Config to maintain a high level of quality, without ever having to republish the app.

Firebase Analytics

As users navigate through the app we use Firebase Analytics events to record their behavior. Most of the mini-games in the app live in their own Activity classes, so we can use Firebase Analytics' automatic screen tracking feature to record these events without writing any code.

For events within games we use custom events to record important user actions. For example after the user finishes playing the "Penguin Swim" game, we record the event swimming_game_end with custom parameters score and num_stars. In the first week of December we noticed that 85% of users were getting zero stars when playing the Penguin Swim game. Clearly, the game is too hard, we were hoping that only 60-70% of users would get a score this low! We were able to correct this using Remote Config, which I'll talk about later.

The other feature of Analytics that we put to use is user properties. At the start of each Santa Tracker session, we use user properties to record some information about the user's device. These properties are then attached to every analytics event. Since Santa Tracker is used all over the world, we get a lot of diversity in the devices people use. These user properties help us to make sense of our analytics data. Some examples are:

API_LEVEL The API level of the user's device, like 23 for Marshmallow or 21 for Lollipop.
DEVICE_BRAND The brand of the user's device, such as Samsung for the Galaxy S7 or Google for the Pixel XL.
DEVICE_BOARD The processor or specific SoC name for the user's device.

The combination of our custom events and user properties with Firebase Analytics' automatically tracked events enables us to get a good understanding of what our users are doing in the app by looking at the Firebase console.

Firebase Crash Reporting

Despite our best efforts, the Santa Tracker app is not perfect. With millions of users on hundreds of device types in dozens of countries we are constantly discovering new bugs in the wild. Firebase Crash Reporting lets us see all of the fatal errors in our app within a minute of their occurrence. Since Firebase Analytics events show up in Firebase Crash Reporting logs we can see the progression of events before the crash which was very helpful in diagnosing some issues.

For example there's an OutOfMemoryError crash which seems to happen during the "Penguin Swim" game on some low-RAM devices. We did not see this error during our testing, but the Firebase Analytics data in Crash Reporting tells us that this occurs when playing the game repeatedly.

This integration is invaluable in helping us to reproduce issues that our normal QA setup does not find. We can get the exact device model and then use the analytics log to recreate the crash conditions.

Firebase Remote Config

Once we have analyzed the data from Analytics and Crash Reporting, we need to make changes in the app to improve the user experience. Due to the short active life span of this app there's no time to go through the full development lifecycle of the app to publish changes, and we don't get a second chance at Santa's big day!

Santa Tracker uses Firebase Remote Config to gate access to various features, and to provide remote fine-tuning for experiences in the mini game. For example, in the "Penguin Swim" game, there are two key variables we store in Remote Config:

SwimmingObstacleDensity Control the density of ice cubes and other obstacles in the game, a lower density makes the game easier.
DisableSwimmingGame A kill-switch to completely hide the game from the app.

As mentioned earlier, users were having a hard time getting a score higher than zero stars in the game. In order to make the game more fun, we changed SwimmingObstacleDensity from 1.5 to 1.1, which made it much easier for users to dodge obstacles. By making the game easier in this way, the percentage of users getting 0 stars went down from about 85% to 70%. This change took place instantly over the air, with no need to publish a new version of the app!

Right now the OutOfMemoryError in the swimming game happens for <1% of users. But if this issue became rampant, we could use the DisableSwimmingGame flag to immediately hide the game from affected users whilst we resolve the issue. By taking advantage of the fact that Analytics user properties can be referenced in Remote Config, we can even disable the game only for certain device types! For example, let's say the Penguin Swim stopped working on all KitKat devices (API level 19).

First, we add a condition based on user properties:

Next, we disable the game only for users who match the condition:

Now the game will only appear for users who will have a stable experience, which will lead to fewer crashes for our users and more positive app ratings for us.

Final Thoughts

Adding deep Firebase integration to Santa Tracker gives us the ability to monitor and fine-tune the app over time without releasing app updates. As developers, it's invaluable to have a clear picture of what our users are really doing and how we can improve the app. Throughout December we knew we could rely on Firebase to give Santa Tracker users a magical holiday experience.

Share:

Wednesday, 21 December 2016

How does Firebase initialize on Android?

Doug Stevenson
Doug Stevenson
Developer Advocate

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 Contextto 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 Applicationsubclass 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 applicationtag'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 ContentProviderto 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:

  1. They are created and initialized (on the main thread) before allother components, such as Activities, Services, and BroadcastReceivers, after the app process is started.
  2. 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:

  1. Each ContentProvider declared in the manifest is created, in priority order.
  2. The Application class (or custom subclass) is created.
  3. 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 getContextmethod. 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.initializeAppin 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!

Share: