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.
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.
swimming_game_end
score
num_stars
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
DEVICE_BRAND
DEVICE_BOARD
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.
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.
OutOfMemoryError
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.
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
DisableSwimmingGame
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.
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.
In September, we launched a new way to search for content in apps on Android phones. With this update, users were able to find personal content like messages, notes, music and more across apps like OpenTable, Ticketmaster, Evernote, Glide, Asana, Gmail, and Google Keep from a single search box. Today, we're inviting all Android developers to enable this functionality for their apps.
Starting with version 10.0, the Firebase App Indexing API on Android lets apps add their content to Google's on-device index in the background, and update it in real-time as users make changes in the app. We've designed the API with three principles in mind:
There are several predefined data types that make it easy to represent common things such as messages, notes, and songs, or you can add custom types to represent additional items. Plus, logging user actions like a user listening to a specific song provides an important signal to help rank user content across the Google app.
Indexable note = Indexables.noteDigitalDocumentBuilder() .setUrl("http://example.net/users/42/lists/23") .setName("Shopping list") .setText("steak, pasta, wine") .setImage("http://example.net/images/shopping.jpg") .build(); FirebaseAppIndex.getInstance().update(note);
Integrating with Firebase App Indexing helps increase user engagement with your app, as users can get back to their personal content in an instant with Google Search. Because that data is indexed directly on the device, this even works when offline.
To get started, check out our implementation guide and codelab.
Nowadays, many users prefer using federated login, such as Google Sign-In, Facebook, Twitter and others, over having to create a new account for every service they use. Each user has their own favorite federated login provider, and it would take a lot of effort to integrate and manage each federated login provider for your app. This is where Firebase Authentication can help.
Firebase Authentication supports many popular federated login providers. You just need to integrate with Firebase Authentication, and let Firebase automatically manage multiple federated login providers under the hood for you.
However, depending on where your users are in the world, they may prefer a federated login providers for which Firebase does not have built-in support yet. For example, in Japan and many Asian countries, LINE Login is very popular with hundreds of millions of users, so you may want to support it as well. Fortunately, it is fairly simple to integrate LINE Login, as well as many other federated login, with Firebase Authentication using Custom Auth.
In one of our previous blog post, we have shown how to support federated login that was not available out-of-the-box with Firebase Authentication on the web using our JavaScript SDK. In this blog post, I will show you how to integrate with LINE Login on iOS and Android.
Here is how the login flow looks like:
Step 1: Use LINE Login SDK to log user in, and acquire their LINE Access Token
Step 2: Send their LINE Access Token to your server and validate it with LINE authentication server. If the token is valid, then create a Firebase Custom Auth token correspond to the user, and send it back to user's device.
Step 3: Use the Firebase Custom Auth token to login to Firebase from the device.
You will need to do some setup work to get your LINE business account and Firebase project ready.
Refer to LINE Login document (iOS / Android) to integrate LINE SDK to your app and implement LINE Login flow. Once user has successfully logged in, you can get their LINE access token as below:
iOS (Objective-C)
NSString *lineAccessToken = self.lineAdapter.getLineApiClient.accessToken;
Android
LineAuthManager authManager = LineSdkContextManager.getSdkContext().getAuthManager(); final String accessToken = authManager.getAccessToken().accessToken;
Then you can use your favorite networking library to send the access token to your own server for validation. In this sample code, I use GTM HTTP Fetcher for iOS and Volley for Android.
NSURL *url = [NSURL URLWithString:@"https:///verifyToken"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; [request setHTTPMethod:@"POST"]; [request setValue:@"application/json" forHTTPHeaderField:@"content-type"]; NSDictionary *token = @{@"token" : lineAccessToken}; NSError *error; NSData *requestBody = [NSJSONSerialization dataWithJSONObject:token options:kNilOptions error:&error]; [request setHTTPBody:requestBody]; GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request]; [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { if (!error) { // Extract Firebase Custom Auth token from response // ・・・ } }];
HashMap validationObject = new HashMap<>(); validationObject.put("token", accessToken); Response.Listener responseListener = new Response.Listener() { @Override public void onResponse(JSONObject response) { // Extract Firebase Custom Auth token from response // ・・・ } }; JsonObjectRequest fbTokenRequest = new JsonObjectRequest( Request.Method.POST, "https:///verifyToken", new JSONObject(validationObject), responseListener, errorListener); NetworkSingleton.getInstance(activity).addToRequestQueue(fbTokenRequest);
You will need a server to validate LINE access token and generate a corresponding Firebase Custom Auth token for that user. You can build a simple one with Firebase Node.js Server SDK and Express web server.
Firstly, your server receives the LINE Access Token from user's device and validates it using LINE Social Rest API. Don't forget to verify the channelId value in API response to make sure that the access token is actually issued for your app. This is to prevent spoof attack, of which attackers reuse access token from other app/channel to attempt login to your app.
Server (Node.js)
app.post('/verifyToken', (req, res) => { if (!req.body.token) { return res.status(400).send('Access Token not found'); } const reqToken = req.body.token; // Send request to LINE server for access token verification const options = { url: 'https://api.line.me/v1/oauth/verify', headers: { 'Authorization': `Bearer ${reqToken}` } }; request(options, (error, response, body) => { if (!error && response.statusCode === 200) { const lineObj = JSON.parse(body); // Don't forget to verify the token's channelId to prevent spoof attack if ((typeof lineObj.mid !== 'undefined') && (lineObj.channelId === myLINEChannelId)) { // Access Token Validation succeed with LINE server // Generate Firebase token and return to device const firebaseToken = generateFirebaseToken(lineObj.mid); // Update Firebase user profile with LINE profile updateUserProfile(reqToken, firebaseToken, lineObj.mid, () => { const ret = { firebase_token: firebaseToken }; return res.status(200).send(ret); }); } } const ret = { error_message: 'Authentication error: Cannot verify access token.' }; return res.status(403).send(ret); }); } });
After successfully validated LINE Access Token, use Firebase Server SDK to generate Firebase Custom Auth token and return it to the user device. You can reuse LINE's user ID for your Firebase user ID.
function generateFirebaseToken(lineMid) { var firebaseUid = 'line:' + lineMid; var additionalClaims = { provider: 'LINE' }; return firebase.auth().createCustomToken(firebaseUid); }
We also have a Firebase Server Java SDK.
You can make use of solutions like App Engine Flexible Environment or Cloud Functions to free yourself from managing the server.
After receiving the Firebase Custom Auth token, just use it to sign the user in Firebase:
[[FIRAuth auth] signInWithCustomToken:firebaseToken completion:^(FIRUser * _Nullable user, NSError * _Nullable error) { // Process sign in result // ・・・ }];
FirebaseAuth.getInstance() .signInWithCustomToken(firebaseToken) .addOnCompleteListener(new OnCompleteListener() { // Process sign in result // ・・・ });
Our sample code is open source. Feel free to download and give it a try. https://github.com/firebase/custom-auth-samples/tree/master/Line
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. For example, you may want to integrate with a third-party API (such as an email or SMS service), complete a computationally expensive task, or have a need for a trusted actor. We want to make your experience on this part of your stack as simple as it is on the front-end. Towards that aim, we announced the Firebase Admin SDKs for Node.js and Java at the Firebase Dev Summit in Berlin earlier this week.
What are the Admin SDKs?
The Firebase Admin SDKs provide developers with programmatic, second-party access to Firebase services from server environments. Second-party here refers to the fact that the SDKs are granted elevated permissions that allow them to do more than a normal, untrusted client device can. The Admin SDKs get these elevated permissions since they are authenticated with a service account, a special Google account that can be used by applications to access Google services programmatically. The Admin SDKs are meant to complement the existing Firebase web and mobile clients which provide third-party, end-user access to Firebase services on client devices.
Some of this may sound familiar for those of you who have used the existing Firebase Node.js and Java SDKs. The difference is that we have now split the second-party (aka "admin") and third-party (aka "end-user") access use cases into separate SDKs instead of conflating them together. This should make it easier for beginners and experts alike to know which SDK to use and which documentation to follow. It also allows us to tailor the Admin SDKs towards server-specific use cases. A great example of this is the new user management auth API which we will go into in the next section.
What can the Admin SDKs do?
The Admin SDKs for Node.js and Java offer the following admin capabilities that already existed in the prior server SDKs:
In addition, the Node.js SDK brings some exciting new functionality:
How can I get started with the Admin SDKs?
The best place to start is with our Admin SDKs setup guide. The guide will walk you through how to download the SDK, generate a service account key file, and use that key file to initialize the Admin SDK. Thanks to our new Service Accounts panel in your Firebase Console settings, generating service account keys should be a breeze.
What's next for the Admin SDKs?
This is really just the beginning for the Admin SDKs. We plan to expand the Admin SDKs across two dimensions. Firstly, we want to provide Admin SDKs in more programming languages, allowing you to write code in the language you feel most comfortable. Secondly, we plan to integrate with more Firebase services, including adding support for services like Firebase Cloud Messaging and bringing the new user management API to Java.
Would you like us to build an Admin SDK in a particular language? Do you want the Admin SDKs to support a certain Firebase service or feature? Let us know in the comments below or by sending us a note through our feature request support channel.
We are excited to expand our first-class support for backend developers in the Firebase ecosystem. Stay tuned for more to come in the future!
We recently announced that the first Firebase Dev Summit will be held in Berlin on Nov 7th. A few spots for app developers are still available and we encourage you to register today!
In our experience there are a number of things required that make a great developer event- and it just happens we’ll have them all at the Firebase Dev Summit in Berlin next month:
1. Announcements. Since our announcement at I/O, we’ve worked hard to add even more exciting features that will help you develop and grow your app. Be the first to hear about our new releases and to exclusively try them out.
2. Meet the Firebase team. Firebase founders, product managers, and engineers will be on hand to answer questions, hear how you're using Firebase and discuss how we can make it even better.
3. Content. The full speaker list and schedule will be released next week! Spoilers: sessions on growth hacking with Firebase, app quality, developing without infrastructure, trainings for new features (and more!) will be part of the full day agenda.
4. Cool swag. This is our first ever Firebase Dev Summit and we take it pretty seriously…
5. Travel grant. We believe a diversity of attributes, experiences, and perspectives are needed to build tools and apps that can change the world. So, we’ve partnered with the Women Techmakers team to offer travel grants to women in technology interested in attending. Apply here to join us in Berlin.
6. Network. The Firebase Dev Summit is for app developers like you. This will be a great chance to meet other folks that are working on similar challenges, have a drink together and dance to the sounds of Drum & Bass while chatting about Firebase. :)
Oh, did we mention that the event is free of charge? We hope you can make it - don’t forget to reserve your spot before we sell out.
Can’t make it? Sign up here if you’d like to receive updates on the livestream and tune in live on November 7th.
This is the last post in this blog series about the Play Services Task API and its use in Firebase. If you missed the first three parts, consider jumping back to those before continuing here. When you're all caught up, let's finish up this series!
Throughout this series, we've only ever talked about units of work that are themselves represented by a Task or a Continuation. In reality, however, there are lots of other ways to get work done. Various utilities and libraries may have their own ways of performing threaded work. You might wonder if you have to switch to the Task API to unify all this if you want to switch to Firebase. But you certainly don't have to. The Task API was designed with the capability of integrating with other ways of doing threaded work.
For example, Java has always had the ability to simply fire up a new thread to process something in parallel with other threads. You can write code like this (though I heartily recommend against it on Android):
new Thread(new Runnable() { @Override public void run() { String result = "the output of some long-running compute"; // now figure out what to do with the result... } }).start();
Here we fire up that new thread from the main thread and do exciting work that ends with a String of interest. All that work that went into creating that string happens in parallel with the main thread, which continued executing after the thread was started. If that threaded work happened to block at any point, the main thread would not be held up by it. However, something must be done to get that String result into the place where it's expected. On Android, if that needs to be back on the main thread, you'll have to write more code to arrange for that to happen. This can get hairy. And we can use Tasks to help.
The Play Services Task API provides a way to make other units of work behave like Tasks, even if they weren't implemented as such. The class of interest here is TaskCompletionSource. This allows you to effectively create a Task "placeholder" that some other bit of code can trigger for success or failure. If you wanted that thread from above to behave like a Task without implementing it as a Task (as we learned last time by passing a Callable to Tasks.call()), you could do this:
final TaskCompletionSource<String> source = new TaskCompletionSource<>(); new Thread(new Runnable() { @Override public void run() { String result = "the output of some long-running compute"; source.setResult(result); } }).start(); Task<String> task = source.getTask(); task.addOnCompleteListener(new OnCompleteListener<String>() { ... });
We now have the thread offering its result String to the TaskCompletionSource using its setResult() method. Then, in the original thread, we simply ask the TaskCompletionSource for its "placeholder" Task, and add a listener to that. The result is now handled inside the listener running on the main thread. You can do the same in the failure case by calling the setException() method on the TaskCompletionSource. That will end up triggering any failure listeners, and they'll get a hold of the exception.
This strategy might seem a little bit silly up front, because there are less verbose ways of putting the result of some work back on the main thread. The value here is in the ability to work with that new placeholder Task along with other Tasks you might be working with in a unified fashion.
Imagine you're writing an app that absolutely depends on some values in Firebase Realtime Database, along with the values in Firebase Remote Config. However, to keep your users entertained while they wait for this data to load, you’d like to create a splash screen that shows some animation until that data is available to work with. Oh, and you don't want that screen to appear and disappear in a jarring way in the event that the data happens to be locally cached, so you want the screen to show for a minimum of 2 seconds. How might you implement this screen?
For starters, you'll need to create a new Activity and design and implement the views for the splash screen. That's straightforward. Then you'll need to coordinate the work between Realtime Database and Remote Config, as well as factor in the two second timer. You'll probably want to kick off all that work during the Activity's onCreate() after you create the splash screen views. You could use a series of Continuations to make sure all these things happen in serial, one after another. But why do that if you could instead start them all at once to run in parallel, and make the user wait only as long as it takes to complete the longest item of work? Let's see how!
The Task API provides a couple methods to help you know when several Tasks are all complete. These static utility methods create a new Task that gets triggered in response to the completion of a collection of Tasks that you provide.
Task<Void> Tasks.whenAll(Collection<? extends Task<?>> tasks) Task<Void> Tasks.whenAll(Task...<?> tasks)
One version of whenAll() accepts a Java Collection (such as a List or Set), and the other uses the varargs style of passing multiple parameters to easily form an array of any length. Either way, the returned Task will now get triggered for success when all the other Tasks succeed, and trigger for failure if any one of them fails. Note that the new Task result is parameterized with Void, meaning it doesn't contain any results directly. If you want the results of each individual Task, you'll have to get the results from each of them directly.
This whenAll() function looks pretty handy for knowing when all our concurrent work is done, so we can move the user past the splash screen. The trick for this case is to somehow get a bunch of Task objects the represent each thing we're waiting on.
The Remote Config fetch is easy, because it will give you a Task you can use to listen to the availability your values. Let's kick off that task and remember it:
private Task<Void> fetchTask; // during onCreate: fetchTask = FirebaseRemoteConfig.getInstance().fetch();
Realtime Database isn't as easy, because it doesn't provide a Task for triggering on the completion of available data. However, we can use the TaskCompletionSource we just learned about to trigger a placeholder task when the data is available:
private TaskCompletionSource<DataSnapshot> dbSource = new TaskCompletionSource<>(); private Task dbTask = dbSource.getTask(); // during onCreate: DatabaseReference ref = FirebaseDatabase.getInstance().getReference("/data/of/interest"); ref.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { dbSource.setResult(dataSnapshot); } @Override public void onCancelled(DatabaseError databaseError) { dbSource.setException(databaseError.toException()); } });
Here, we're registering a listener for the data we need to continue launching the app. That listener will then trigger dbTask to success or failure via dbSource depending on the callback it received.
Lastly, there's the minimum two second delay for the splash screen to stay up. We can also represent that delay as a Task using TaskCompletionSource:
private TaskCompletionSource<Void> delaySource = new TaskCompletionSource<>(); private Task<Void> delayTask = delaySource.getTask(); // during onCreate: new Handler().postDelayed(new Runnable() { @Override public void run() { delaySource.setResult(null); } }, 2000);
For the delay, we're just scheduling a Runnable to execute on the main thread after 2000ms, and that Runnable will then trigger delayTask via delaySource.
Now, we have three Tasks, all operating in parallel, and we can use Tasks.whenAll() to create another Task that triggers when they're all successful:
private Task<Void> allTask; // during onCreate(): allTask = Tasks.whenAll(fetchTask, dbTask, delayTask); allTask.addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { DataSnapshot data = dbTask.getResult(); // do something with db data? startActivity(new Intent(SplashScreenActivity.this, MainActivity.class)); } }); allTask.addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // apologize profusely to the user! } });
And that should do it! When the final allTask succeeds, we can do whatever we need with the data from the database, then we send the user to MainActivity. Without the use of Tasks here, this code becomes more tedious to write because you'd have to check the state of all the other ongoing units of work at the end of each of them, and proceed only when you know they are all done. Here, the Task API handles those details for you. And you can easily add more Tasks as needed without having to change the logic. Just keep adding Tasks to the collection behind allTask.
It's worth noting that there is a way to block the current thread on the result of one or more Tasks. Normally you don't want to block threads at all, if you can help it, but occasionally it's useful when you have to (such as with Loaders). If you do need to wait on the result of a Task, you can use the await() function:
static <TResult> TResult await(Task<TResult> task) static <TResult> TResult await(Task<TResult> task, long timeout, TimeUnit unit)
With await(), the calling thread simply blocks until the task completes, or the given timeout expires. If it was successful, you'll receive the result object, and if it fails, it will throw an ExecutionException which wraps the underlying cause. Please remember that you should never block the main thread! Only use this when you know you're running on some background thread, OK?
Here's what we covered in the four parts of this blog series:
This should be everything you need to know to make effective use of Play Services Task API! I hope you’re able to use Firebase along with the Task API to make efficient and delightful Android apps.
Alrighty! Thanks for joining us for part three of this blog series about the Play services Task API for Android. By now, you've seen the essentials of the API in part one, and how to select the best style of listener in part two. So, at this point, you probably have everything you need to know to make effective use of the Tasks generated by Firebase APIs. But, if you want to press into some advanced usage of Tasks, keep reading!
We know that some of the Firebase features for Android will do work for you and notify a Task upon completion. But, what if you want to create your own Tasks to perform threaded work? The Task API gives you the tools for this. If you want to work with the Task API without having to integrate Firebase into your app, you can get the library with a dependency in your build.gradle:
compile 'com.google.android.gms:play-services-tasks:9.6.1'
But, if you are integrating Firebase, you'll get this library included for free, so no need to call it out specifically in that case.
There is just one method (with two variants) you can use to kick off a new Task. You can use the static method named "call" on the Tasks utility class for this. The variants are as follows:
Task<TResult> call(Callable<TResult> callable) Task<TResult> call(Executor executor, Callable<TResult> callable)
Just like addOnSuccessListener(), you have a version of call() that executes the work on the main thread and another that submits the work to an Executor. You specify the work to perform inside the passed Callable. A Java Callable is similar to a Runnable, except it's parameterized by some result type, and that type becomes the returned object type of its call() method. This result type then becomes the type of the Task returned by call(). Here's a really simple Callable that just returns a String:
public class CarlyCallable implements Callable<String> { @Override public String call() throws Exception { return "Call me maybe"; } }
Notice that CarlyCallable is parameterized by String, which means its call() method must return a String. Now, you can create a Task out of it with a single line:
Task<String> task = Tasks.call(new CarlyCallable());
After this line executes, you can be certain that the call() method on the CarlyCallable will be invoked on the main thread, and you can add a listener to the Task to find the result (even though that result is totally predictable). More interesting Callables might actually load some data from a database or a network endpoint, and you'd want to have those blocking Callables run on an Executor using the second form of call() that accepts the Executor as the first argument.
Let's say, for the sake of example, you want to process the String result of the CarlyCallable Task after it's been generated. Imagine that we're not so much interested in the text of the resulting String itself, and more interested in a List of individual words in the String. But, we don't necessarily want to modify CarlyCallable because it's doing exactly what it's supposed to, and it could be used in other places as it’s written now. Instead, we'd rather encapsulate the logic that splits words into its own class, and use that after the CarlyCallable returns its String. We can do this with a Continuation. An implementation of the Continuation interface takes the output of one Task, does some processing on it, and returns a result object, not necessarily of the same type. Here's a Continuation that splits a string of words into an List of Strings with each word:
public class SeparateWays implements Continuation<String, List<String>> { @Override public List<String> then(Task<String> task) throws Exception { return Arrays.asList(task.getResult().split(" +")); } }
Notice that the Continuation interface being implemented here is parameterized by two types, an input type (String) and an output type (List). The input and output types are used in the signature of the lone method then() to define what it's supposed to do. Of particular note is the parameter passed to then(). It's a Task, and the String there must match the input type of the Continuation interface. This is how the Continuation gets its input - it pulls the finished result out of the completed Task.
Here's another Continuation that randomizes a List of Strings:
public class AllShookUp implements Continuation<List<String>, List<String>> { @Override public List<String> then(@NonNull Task<List<String>> task) throws Exception { // Randomize a copy of the List, not the input List itself, since it could be immutable final ArrayList<String> shookUp = new ArrayList<>(task.getResult()); Collections.shuffle(shookUp); return shookUp; } }
And another one that joins a List of Strings into a single space-separated String:
private static class ComeTogether implements Continuation<List<String>, String> { @Override public String then(@NonNull Task<List<String>> task) throws Exception { StringBuilder sb = new StringBuilder(); for (String word : task.getResult()) { if (sb.length() > 0) { sb.append(' '); } sb.append(word); } return sb.toString(); } }
Maybe you can see where I'm going with this! Let's tie them all together into a chain of operations that randomizes the word order of a String from a starting Task, and generates a new String with that result:
Task<String> playlist = Tasks.call(new CarlyCallable()) .continueWith(new SeparateWays()) .continueWith(new AllShookUp()) .continueWith(new ComeTogether()); playlist.addOnSuccessListener(new OnSuccessListener<String>() { @Override public void onSuccess(String message) { // The final String with all the words randomized is here } });
The continueWith() method on Task returns a new Task that represents the computation of the prior Task after it’s been processed by the given Continuation. So, what we’re doing here is chaining calls to continueWith() to form a pipeline of operations that culminates in a final Task that waits for each stage to complete before completing.
This chain of operations could be problematic if these they have to deal with large Strings, so let's modify it to do all the processing on other threads so we don't block up the main thread:
Executor executor = ... // you decide! Task<String> playlist = Tasks.call(executor, new CarlyCallable()) .continueWith(executor, new SeparateWays()) .continueWith(executor, new AllShookUp()) .continueWith(executor, new ComeTogether()); playlist.addOnSuccessListener(executor, new OnSuccessListener() { @Override public void onSuccess(String message) { // Do something with the output of this playlist! } });
Now, the Callable, all of the Continuations, and the final Task listener will each run on some thread determined by the Executor, freeing up the main thread to deal with UI stuff while this happens. It should be totally jank-free.
At first blush, it could seem a bit foolish to separate all these operations into all the different classes. You could just as easily write this as a few lines in a single method that do only what's required. So, keep in mind that this is a simplified example intended to highlight how Tasks can work for you. The benefit of chaining of Tasks and Continuations (even for relatively simple functions) becomes more evident when you consider the following:
Practically speaking, you're more likely to use Task continuations to perform a series of modular chain of filter, map, and reduce functions on a set of data, and keep those units of work off the main thread, if the collections can be large. But, I had fun with music theme here!
One last thing to know about Continuations. If a runtime exception is thrown during processing at any stage along the way, that exception will normally propagate all the way down to the failure listeners on the final Task in the chain. You can check for this yourself in any Continuation by asking the input Task if it completed successfully with the isSuccessful() method. Or, you can just blindly call getResult() (as is the case in the above samples), and if there was previously a failure, it will get re-thrown and automatically end up in the next Continuation. The listeners on the final Task in the chain should always check for failure, though, if failure is an option.
So, for example, if the CarlyCallable in the above chain returned null, that would cause the SeparateWays continuation to throw a NullPointerException, which would propagate to the end of the Task. And if we had an OnFailureListener registered, that would get invoked with the same exception instance.
What's the most efficient way, with the above chain, of finding out the number of words in the original string, without modifying any of the processing components? Take a moment to think about it before reading on!
The answer is probably more simple than you'd imagine. The most obvious solution is to count the number of words in the final output string, since their order only got randomized. But there is one more trick. Each call to continueWith() returns a new Task instance, but those are all invisible here because we used a chaining syntax to assemble them into the final Task. So you can intercept any of those those tasks and add another listener to it, in addition to the next Continuation:
Task<List<String>> split_task = Tasks.call(new CarlyCallable()) .continueWith(executor, new SeparateWays()); split_task = .continueWith(executor, new AllShookUp()) .continueWith(executor, new ComeTogether()); split_task.addOnCompleteListener(executor, new OnCompleteListener<List<String>>() { @Override public void onComplete(@NonNull Task<List<String>> task) { // Find the number of words just by checking the size of the List int size = task.getResult().size(); } }); playlist.addOnCompleteListener( /* as before... */ );
When a Task finishes, it will trigger both of the Continuations on it, as well as all of the added listeners. All we've done here is intercept the Task that captures the output of the SeparateWays continuation, and listen to the output of that directly, without affecting the chain of continuations. With this intercepted task, we only have to call size() on the List to get the count.
All joking aside, the Task API makes it relatively easy for you to express and execute a sequential pipeline of processing in a modular fashion, while giving you the ability to specify which Executor is used at each stage in the process. You can do this /with or without/ Firebase integrated into your app, using your own Tasks or those that come from Firebase APIs. For the next and final part to this series, we'll look at how Tasks can be used in parallel to kick off multiple units of work simultaneously.
As usual, if you have any questions, consider using Twitter with the #AskFirebase hashtag or the firebase-talk Google Group. We also have a dedicated Firebase Slack channel. And you can follow me @CodingDoug on Twitter to get notified of the next post in this series.
Lastly, if you're wondering about all the songs I referenced in this post, you can find them here:
Ohai! You've just joined us for the second part of a blog series about the Play Services Task API, which is used by some Firebase features to respond to work that its APIs perform asynchronously. Last time, we got acquainted with a Task used by the Firebase Storage API, and learned a little bit about how Tasks work in general. So, if you haven't seen that post, now's good time to circle back to it before continuing here. In this post, we'll take a look at some of the nuances in behavior between the different variations for adding a listener to a Task to capture its result.
Last time, we saw a listener get added to a Task like this, using the Firebase Storage API:
Task task = forestRef.getMetadata(); task.addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(StorageMetadata storageMetadata) { // Metadata now contains the metadata for 'images/forest.jpg' } });
In this code, addOnSuccessListener() is called with a single argument, which is an anonymous listener to invoke upon completion. With this form, the listener is invoked on the main thread, which means we can do things that can only be done on the main thread, such as update a View. It's great that the Task helps put the work back on the main thread, except there is one caveat here. If a listener is registered like this in an Activity, and it's not removed before the Activity is destroyed, there is a possibility of an Activity leak.
Right, nobody wants leaky Activities! So, what's an Activity leak, anyway? Put briefly, an Activity leak occurs when an object holds onto an Activity object reference beyond its onDestroy() lifecycle method, retaining the Activity beyond its useful lifetime. When onDestroy() is called on an Activity, you can be certain that instance is never going to be used by Android again. After onDestroy(), we want the Android runtime garbage collector to clean up that Activity, all of its Views, other dead objects. But the garbage collector won't clean up the Activity and all of its Views if some other object is holding a strong reference to it!
Activity leaks can be a problem with Tasks, unless you take care to avoid it. In the above code (if it was inside an Activity), the anonymous listener object actually holds a strong, implicit reference to the containing Activity. This is how code inside the listener is able to make changes to the Activity and its members - the compiler silently works out the details of that. An Activity leak occurs when an in-progress Task holds on to the listener past the Activity's onDestroy(). We really don't have any guarantees at all about how long that Task will take, so the listener can be held indefinitely. And since the listener implicitly holds a reference to the Activity, the Activity can be leaked if the Task doesn't complete before onDestroy(). If lots of Tasks holding references to Activities back up over time (for example, due to a hung network), that can cause your app to run out of memory and crash. Yow. You can learn more in this video.
If you’re concerned about leaking Activities (and I hope you are!), you should know that the single argument version of addOnSuccessListener() has the caveat of possibly leaking the Activity if you aren't careful to remove the listener at the right time.
It turns out there's a convenient way to do this automatically with the Task API. Let's take the above code in an Activity, and modify its call to addOnSuccessListener() slightly:
Task task = forestRef.getMetadata(); task.addOnSuccessListener(this, new OnSuccessListener() { @Override public void onSuccess(StorageMetadata storageMetadata) { // Metadata now contains the metadata for 'images/forest.jpg' } });
This is exactly like the previous version, except there are now two arguments to addOnSuccessListener(). The first argument is `this`, so when this code is in an Activity, that would make `this` refer to that enclosing Activity instance. When the first parameter is an Activity reference, that tells the Task API that this listener should be "scoped" to the lifecycle of the Activity. This means that the listener will be automatically removed from the task when the Activity goes through its onStop() lifecycle method. This is pretty handy because you don't have to remember to do it yourself for all the Tasks you may create while an Activity active. However, you need to be confident that onStop() is the right place for you to stop listening. onStop() is triggered when an Activity is no longer visible, which is often OK. However, if you intend to keep tracking the Task in the next Activity (such as when an orientation change replaces the current Activity with a new one), you'll need to come up with a way to retain that knowledge in the next Activity. For some information on that, read up on saving Activity state.
There are cases where you simply don't want to react to the completion of a Task on the main thread. Maybe you want to do blocking work in your listener, or you want to be able to handle different Task results concurrently (instead of sequentially). So, you'd like to avoid the main thread altogether and instead process the result on another thread you control. There's one more form of addOnSuccessListener() that can help your app with your threading. It looks like this (with abbreviated listener):
Executor executor = ...; // obtain some Executor instance Task task = RemoteConfig.getInstance().fetch(); task.addOnSuccessListener(executor, new OnSuccessListener() { ... });
Here, we're making a call to the Firebase Remote Config API to fetch new configuration values. Then, the returned Task from fetch() gets a call to addOnSuccessListener() and receives an Executor as the first argument. This Executor determines the thread that will be used to invoke the listener. For those of you unfamiliar with Executor, it's a core Java utility that accepts units of work and routes them to be executed on threads under its control. That could be a single thread, or a pool of threads, all waiting to do work. It's not very common to use for apps to use an Executor directly, and can be seen as an advanced technique for managing the threading behavior of your app. What you should take away from this is the fact that you don't have to receive your listeners on the main thread if that doesn't suit your situation. If you do choose to use an Executor, be sure to manage them as shared singletons, or make sure their lifecycles are managed well so you don’t leak their threads.
One other interesting thing to note about this code is the fact that the Task returned by Remote Config is parameterized by Void. This is the way a Task can say that it doesn't generate any object directly - Void is the data type in Java that indicates absence of type data. The Remote Config API is simply using the Task as an indicator of task completion, and the caller is expected to use other Remote Config APIs to discover any new values that were fetched.
All told, there are three varieties of addOnSuccessListener():
Task addOnSuccessListener(OnCompleteListener listener) Task addOnSuccessListener(Activity activity, OnSuccessListener listener) Task addOnSuccessListener(Executor executor, OnSuccessListener listener)
On top of that, we have the same varieties for failure and completion listeners:
Task addOnFailureListener(OnFailureListener listener) Task addOnFailureListener(Activity activity, OnFailureListener listener) Task addOnFailureListener(Executor executor, OnFailureListener listener) Task addOnCompleteListener(OnCompleteListener listener) Task addOnCompleteListener(Activity activity, OnCompleteListener listener) Task addOnCompleteListener(Executor executor, OnCompleteListener listener)
There's nothing too special going on with OnCompleteListener. It's just a single listener that's capable of receiving both success and failure, and you have to check for that status inside the callback. The file metadata callback from the last post could be rewritten like this, instead of giving the task separate success and failure listeners:
Task task = forestRef.getMetadata(); task.addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete (Task task) { if (task.isSuccessful()) { StorageMetadata meta = task.getResult(); // Do something with metadata... } else { Exception e = task.getException(); // Handle the failure... } } });
So, with OnCompleteListener, you can have a single listener that handles both success and failure, and you find out which one by calling isSuccessful() on the Task object passed to the callback. Practically speaking, this is functionally equivalent to registering both an OnSuccessListener and an OnFailureListener. The style you choose is mostly a matter of preference.
Now you've seen that Tasks can receive three different kinds of listeners: success, failure, and overall completion. And, for each of those kinds of listeners, there are three ways to receive that callback: on the main thread, on the main thread scoped to an Activity, and on a thread determined by an Executor. You have some choices here, and it's up to you to choose which one suits your situation the best. However, these aren't the only ways to handle the results of your Tasks. You can create pipelines of Task results for more complex processing. Please join me for those details next time, where you can continue the journey to become a Firebase Taskmaster!
If you have any questions, consider using Twitter with the #AskFirebase hashtag or the firebase-talk Google Group. We also have a dedicated Firebase Slack channel. And you can follow me @CodingDoug on Twitter.
We’re excited to announce that the registration for the Firebase Dev Summit is opening today!
Six months ago, thousands of developers joined us at Google I/O in Mountain View, CA to hear about the expansion of Firebase to become a unified app platform that helps developers build better apps and grow successful businesses. We want to share these updates with you (and maybe even a few new ones!) at the Firebase Dev Summit in Berlin, Germany. Registration is now open, but keep in mind that space will be filled on a first-come, first-serve basis, so make sure to register today.
Our product managers and engineering team (including me!) will be there, and we’re excited to meet you in person and learn how we can make Firebase easier for you to develop extraordinary experiences for your users on iOS, Android, and the Web.
The Firebase Dev Summit is full day event for app developers that will focus on how to use Firebase with your apps. The day will have a packed agenda with valuable sessions from Firebase and our partners, and is a great chance to meet developers from your local community. But, the day isn’t just about us talking to to you -- we also want to see you get your hands dirty with Firebase. You’ll get a chance to put your new knowledge into practice with a hands-on workshop and codelabs that walk you through all the different features of Firebase. Firebase engineers will be on hand to help you get up and running, and answer any questions you may have.
We’re looking forward to meeting you in person. Danke!
Hey there, iOS Developers!
We wanted to let you know that Firebase version 3.6 is now available for iOS. This contains a number of important bug fixes and features needed for iOS 10 support, and we encourage you to run a pod update (or manually update your frameworks) and recompile your projects at your earliest convenience.
pod update
If you want to see a full list of fixes and improvements, you can review the release notes, but here's a quick summary of what's new.
Firebase Cloud Messaging now has support for the new iOS 10 user notifications. If your app is running on iOS 10, you can handle incoming notifications using the userNotificationCenter:willPresentNotification: withCompletionHandler method. And don't worry -- if your app only has the older application:didReceiveRemoteNotification: completionHandler methods supported, APNs will call those instead if it can't find the newer ones. Need more info? Refer to the updated FCM documentation for more information.
userNotificationCenter:willPresentNotification: withCompletionHandler
application:didReceiveRemoteNotification: completionHandler
With the iOS 10 update, Apple made a number of changes to their App Store review guidelines. The latest version of Firebase has made several changes in response to these new guidelines. Most importantly, you should no longer encounter iTunes Connect errors asking you to provide text for things like NSCalendarsUsageDescription and NSBluetoothPeripheralUsageDescription.
NSCalendarsUsageDescription
NSBluetoothPeripheralUsageDescription
One consequence of following these guidelines is that we have removed the technology which up until recently gave you the ability to measure iOS Search app install ads from Safari.
For those of you who are using Firebase Invites, you will need to supply some content for NSContactsUsageDescription in your plist file. Firebase Invites uses this contact information to populate the list of friends that your user might want to send an invitation to.
NSContactsUsageDescription
plist
Of course, this is an ongoing process. We will monitor the impact of these changes closely, and publish further updates if it ever becomes necessary.
You may recall in a recent blog post that Firebase Auth was encountering errors in Xcode 8 due to it not being able to write values to the keychain in the simulator. While that issue still exists, we have developed a workaround where we use NSUserDefaults in the simulator, and continue to use the keychain on the device. This means you can now develop and test out Firebase Auth in the simulator and everything should be working again.
NSUserDefaults
You found bugs; we fixed 'em! Please continue to report any issues or feature requests you might have to our online form, and we'll make sure they get handled appropriately.
And if you have any questions, you can always ask them on Stack Overflow with the Firebase tag, or send them to our Google group.
Thanks again for being a Firebase developer! Now go forth and update your apps!
Sometimes, when using the Firebase client APIs for Android, it's required that Firebase perform some work at the request of the developer in an asynchronous fashion. Perhaps some requested data is not immediately available, or work needs to be queued for eventual execution. When we say some work must be done asynchronously in an app, that means the work needs to happen at the same time as the app performs its primary job of rendering the app’s views, but not get in the way of that work. To perform this asynchronous work correctly in Android apps, the work can't occupy time on the Android main thread, otherwise the app may delay rendering of some frames, causing "jank" in the user experience, or worse, the dreaded ANR! Typical examples of work that can cause delays are network requests, reading and writing files, and lengthy computations. In general, we call this blocking work, and we never want to block the main thread!
When a developer uses a Firebase API to request work that would normally block the main thread, the API needs to arrange that work to run on a different thread, in order to avoid jank and ANRs. Upon completion, the results of that work sometimes have to make it back to the main thread in order to safely update views.
That's what the Play services Task API is for. The goal of the Task API is to provide an easy, lightweight, and Android-aware framework for Firebase (and Play services) client APIs to perform work asynchronously. It was introduced in Play services version 9.0.0 along with Firebase. If you've been using Firebase features in your app, it's possible that you may have been using the Task API without even realizing it! So, what I'd like to do in this blog series is unpack some of the ways the Firebase APIs make use of Tasks, and discuss some patterns for advanced use.
Before we begin, it's important to know that the Task API isn't a full replacement for other threading techniques on Android. The Android team has put together some great content that describe other tools for threading, such as Services, Loaders, and Handlers. There's also a whole season of Application Performance Patterns on YouTube that discusses your options. Some developers even opt into third party libraries that will help you with your threading in Android apps. So, it's up to you to learn about those and determine which solution is the best for your particular threading needs. Firebase APIs uniformly use Tasks to manage threaded work, and you can use those in conjunction with other strategies as you see fit.
If you're using Firebase Storage, you'll definitely encounter Tasks at some point. Here's a straightforward example of fetching metadata about a file that's already uploaded to Storage, taken directly from the documentation for file metadata:
// Create a storage reference from our app StorageReference storageRef = storage.getReferenceFromUrl("gs://"); // Get reference to the file StorageReference forestRef = storageRef.child("images/forest.jpg"); forestRef.getMetadata().addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(StorageMetadata storageMetadata) { // Metadata now contains the metadata for 'images/forest.jpg' } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception exception) { // Uh-oh, an error occurred! } });
Even though we never see a "Task" anywhere in this code, there is actually a Task in play here. The last part of the above code could be rewritten equivalently like this:
Task task = forestRef.getMetadata(); task.addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(StorageMetadata storageMetadata) { // Metadata now contains the metadata for 'images/forest.jpg' } }); task.addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception exception) { // Uh-oh, an error occurred! } });
Ah, it looks like there was a Task hidden in that code after all!
With the sample code rewritten above, it's now more clear how a Task is being used to obtain file metadata. The getMetadata() method on the StorageReference has to assume that the file metadata is not immediately available, so it will make a network request to get a hold of it. So, in order to avoid blocking the calling thread on that network access, getMetadata() returns a Task that can be listened to for eventual success or failure. The API then arranges to perform the request on a thread it controls. The details of this threading are hidden by the API, but the returned Task is used to indicate when the results become available. The returned Task then guarantees that any added listeners will be invoked upon completion. This form of API to manage the results of asynchronous work is sometimes called a Promise in other programming environments.
Notice here that the returned Task is parameterized by the type StorageMetadata, and that's also the type of object that gets passed to onSuccess() in the OnSuccessListener. In fact, all Tasks must declare a generic type in this way to indicate the type of data they generate, and the OnSuccessListener must share that generic type. Also, when an error occurs, an Exception is passed to onFailure() in the OnFailureListener, which will probably be the specific exception that caused the failure. If you want to know more about that Exception, you may have to check its type in order to safely cast it to the expected type.
The last thing to know about this code is that the listeners will be called on the main thread. The Task API arranges for this to happen automatically. So, if you want to do something in response to the StorageMetadata becoming available that must happen on the main thread, you can do that right there in the listener method. (But remember that you still shouldn’t be doing any blocking work in that listener on the main thread!) You have some options about how these listeners work, and I'll say more in a future post about your alternatives.
Some Firebase features provide other APIs that accept listeners that are not associated with Tasks. For example, if you're using Firebase Authentication, you've almost certainly registered a listener to find out when the user successfully logs in or out of your app:
private FirebaseAuth auth = FirebaseAuth.getInstance(); private FirebaseAuth.AuthStateListener authStateListener = new FirebaseAuth.AuthStateListener() { @Override public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { // Welcome! Or goodbye? } }; @Override protected void onStart() { super.onStart(); auth.addAuthStateListener(authStateListener); } @Override protected void onStop() { super.onStop(); auth.removeAuthStateListener(authStateListener); }
The FirebaseAuth client API makes two main guarantees for you here when you add a listener with addAuthStateListener(). First, it will call your listener immediately with the currently known login state for the user. Then, it will call the listener again with all subsequent changes to the user's login state, for as long as the listener is added to the FirebaseAuth object. This behavior is very different than the way Tasks work!
Tasks only call any added listener at most once, and only after the result is available. Also, the Task will invoke a listener immediately if the result was already available before that listener was added. The Task object effectively remembers the final result object and continues to deal it out to any future listeners, until it has no more listeners and is eventually garbage collected. So if you're using a Firebase API that works with listeners on something other than a Task object, be sure to understand its own behaviors and guarantees. Don't assume that all Firebase listeners behave like Task listeners!
Consider the active lifetime of your added Task listeners. There are two things that can go wrong if you don’t do this. First, you can cause an Activity leak if the Task continues beyond the lifetime of an Activity and its Views that are being referenced by an added listener. Second, the listener might execute when it’s no longer needed, causing wasteful work to be done, and possibly doing things that access Activity state when it’s no longer valid. The next part of this blog series will go into these issues in more detail, and how to avoid them.
We've taken a brief look at the Play Services Task API and uncovered its (sometimes hidden!) use in some Firebase sample code. Tasks are the way that Firebase lets you respond to work that has an unknown duration and must be executed off the main thread. Tasks can also arrange for listeners to be executed back on the main thread to deal with the result of the work. However, we've only just scratched the surface of what Tasks can do for you. Next time, we'll look at the variations on Task listeners so you can decide which one best suits your use cases.
We’re happy to see many developers experimenting with Firebase! Some of you have been transitioning your experiments to production, and that’s raised some questions about how to manage the builds of your Android projects. The most common question goes something like this: "How do I keep my everyday development data separate from my release data?"
We'd all like our analytics to reflect only the actual usage of the app (as opposed to artificial usage during development). Also, it's not useful to see crashes from moment-to-moment development interleaved with those from our publicly released versions. On top of that, as your team grows and your app becomes more complex, you may want to separate each team member’s working space into separate sandboxes so that their work won’t collide with each other.
So, let’s explore some ways to configure your project to best handle these cases. For Android app builds, the preferred path is to take advantage of some configuration features of the Android Gradle plugin. These can be applied alongside some configurations in the Firebase Console.
But before I discuss these configurations, let's get some terminology settled! Here are some terms we'll use:
Firebase project
Firebase app
Android Gradle build type
Android Gradle build flavor
Android Gradle build variant
Android Application ID
The key concept, for effective configuration of an app with Firebase, is to assign a distinct application ID to each build variant of an app that requires its own collection of data. This is something you do in your app's build.gradle first, then mirror in the Firebase Console. But first, to make a decision about configuration that's best for your app, there are some more things you need to know about how the different Firebase features work between your Firebase projects and apps.
Some Firebase features share their data between all the apps (Android, iOS, and web) within the same project. You could say that the data for these features are "scoped" to an entire Firebase project:
Some Firebase features have independent data for all the apps within the same project. You could say that these features are scoped to the individual apps in your Firebase project:
You'll notice that the dashboards for both Analytics and Crash Reporting have an app selector near the top of their dashboards that let you select the individual app (of all those created in the project) whose data you want to view.
Some Firebase features have a hybrid scope, where any number of apps may be affected by a particular operation:
Firebase Test Lab for Android has its own special case because it requires a project with billing enabled, but it can be used with any APK without any constraint in a single project. So, if you want to develop with Firebase on a free plan, but test the APK using Test Lab on a paid plan, it's recommended to create a whole new project and enable billing just for use of Test Lab. You can test any app in this project, with or without Firebase integrated.
Now, this is a all good to knowledge to have, but why don't we make this more practical with some actual examples? I'll share some recipes for configuration next. The best case for your situation might be one of these exactly, or some hybrid.
Let's say you're an individual developer or on a small team, your app is relatively simple, and you just need to separate your analytics and crash reports between your daily debug and published release builds. In this case, it would suffice to configure your app to have a different application ID for debug and release. Here's a bit of a Gradle configuration that might help:
defaultConfig { applicationId "com.company.project" // etc... } buildTypes { debug { applicationIdSuffix ".debug" } release { // etc... } }
Here, the application ID is "com.company.project", which gets applied to the release build. But the application ID for the debug build becomes "com.company.project.debug". You don't have to use a suffix like this - instead you could specify a whole new value with applicationId.
Then, in the Firebase Console, you would create a single project, and within that project, create two apps, one for each build type. The debug app would use the application ID "com.company.project.debug", and the release app would use "com.company.project". The SHA-1 hashes would also have to reflect the different keys used to sign each build, if you're using Firebase features that require it.
After both apps are created, download a google-services.json file from the console and place it into your app. If you look inside that file, you'll notice that both apps will appear in there. The Google Services plugin will figure out which set of configurations to use during the build of each variant.
"client_info": { "mobilesdk_app_id": "...", "android_client_info": { "package_name": "com.company.project.debug" } }, "client_info": { "mobilesdk_app_id": "...", "android_client_info": { "package_name": "com.company.project" }
google-services.json will contain info for all Android apps in a project.
It's important to know that if this project is on a billing plan, you'll be billed for all bandwidth and storage generated by both apps. So if you're pulling lots of data during development, that may result in additional charges. Be sure to understand the [pricing plans] to plan for this so you're not surprised by the bill.
It's also important to note that, with this configuration, you will be working against all the same data during development as your active users on your fully released app. This may not be the safest thing, if you intend to disrupt your Realtime Database data or experiment with Remote Config values during development!
The prior recipe of doing development against your live data may be problematic. If you have a large team with lots of people making unsafe updates to the data, or you generally want to prevent the risk of corrupting production data, you'll need to set up multiple projects to isolate development data from production data. In fact, you could have everyone on the team use their own individual "sandbox" projects on the free tier so they can experiment safely without affecting others or incurring any billing.
To set this up, you don't really need to do anything special in your build.gradle. Everyone can use the same application ID to create an app in their sandbox project. However, they'll each need their own unique debug key to sign with. The Android SDK tools create a unique debug key for each user of the SDK, so normally that shouldn't be a problem. But it should be known that the Firebase Console will not allow an app to be created that has a duplicate pair of application ID and SHA-1 key as any other app in any project in any account. So if your team members were sharing a debug key, that won't work with this setup.
This arrangement is great to keep everyone isolated, but there's one caveat. Since all the developers will be creating their own project, they may also have to duplicate some configurations to make the project work correctly. For example, the database for a new project may need to be bootstrapped with some useful data. And the correct security rules should be duplicated. Remote Configs may need to created with appropriate values. Authentication may need to be configured as well. And, of course, every developer will need to use the google-services.json file generated for their own project, and should not be checked into source control, in order to avoid conflicts between team members.
If you have a situation where you need data isolation between different environments, the best way to set that up is similar to the large team setup above. You'll definitely need to create different projects for each environment. They can all be owned by the same account or by different accounts.
To make it easy to select the environment to build for, you can take advantage of build flavors to configure each version of the app. For example, if you need isolation for development, QA, and production, you could define three build flavors in the productFlavors block, which goes next to the buildTypes block in the app’s build.gradle:
productFlavors { dev { } qa { } prod { } }
Here, we're not indicating that there's anything different between the variants except that they exist separately. They'll all have the same application ID, which is OK. Or you could assign them distinct IDs if that helps your situation. In either case, you'll need to use a flavor-specific directory to contain the google-services.json file from each project. By default, the Android Gradle plugin recognizes the following convention for organizing the directories for each flavor as defined above:
app/ src/ main/ dev/ google-services.json (for dev only) qa/ google-services.json (for qa only) prod/ google-services.json (for prod only)
Notice that each named flavor becomes the name of a directory that sits in src adjacent to the main directory where your project code typically lives. With this structure, you can drop the google-services.json for each project directly into its dedicated directory. Now, if you want to build the “dev” flavor of the app, you can select "devDebug" from the build variants window in Android Studio, or target the variant's build task "assembleDevDebug" on the Gradle command line.
If you have an unusual situation with your app build that isn't really helped by the information here, please don't hesitate to ask a question at the firebase-talk Google Group. And, for more urgent support matters, please file an issue on the Firebase Support site. And please follow me on Twitter as CodingDoug!