Build an Android App Using Firebase and the App Engine Flexible Environment

This tutorial demonstrates how to write a mobile app with backend data storage, real-time synchronization, and user-event logging using Firebase. Java servlets running in the Google App Engine flexible environment listen for new user logs stored in Firebase and process them.

The instructions show how to do this using:

  • Firebase — a fully managed platform for building iOS, Android, and web apps that provides automatic data synchronization, authentication services, messaging, file storage, analytics, and more.

  • App Engine flexible environment — an application platform that monitors, updates, and scales the hosting environment. Flexible environment runs your backend service inside Docker containers that you configure with Dockerfiles. This means you can call native binaries, write to the file system, and make other system calls.

If your app needs to process user data or orchestrate events, extending Firebase with the App Engine flexible environment gives you the benefit of automatic real-time data synchronization, without the need to run your code inside the App Engine standard environment sandbox.

The sample app, Playchat, stores chat messages in the Firebase Realtime Database, which automatically synchronizes that data across devices. Playchat also writes user-event logs to Firebase.

Playchat client architecture

A set of Java servlets running in the App Engine flexible environment register as listeners with Firebase. The servlets respond to new user-event logs and process the log data. The servlets use transactions to ensure that only one servlet handles each user-event log.

Playchat server architecture

Communication between the app and the servlet happens in three parts:

  • When a new user logs into Playchat, the app requests a logging servlet for that user by adding an entry under /requestLogger/ in the Firebase Realtime Database.

  • One of the servlets accepts the assignment by updating value of the entry to its servlet identifier. The servlet uses a Firebase transaction to guarantee that it is the only servlet can update the value. Once the value is updated, all other servlets ignore the request.

  • When the user logs in, logs out, or changes to a new channel, Playchat logs the action in /inbox/[SERVLET_ID]/[USER_ID]/, where [SERVLET_ID] is the identifier of the servlet instance and [USER_ID] is a hash value representing the user.

  • The servlet watches the inbox for new entries and collects the log data.

In this sample app, the servlets copy the log data locally and display it on a web page. In a production version of this app, the servlets could process the log data or copy it to Google Cloud Storage, Google Cloud Bigtable, or Google BigQuery for storage and analysis.

Objectives

This tutorial demonstrates how to:

  • Build an Android app, Playchat, that stores data in the Firebase Realtime Database.

  • Run a Java servlet in the App Engine flexible environments that connects to Firebase and receives notifications when the data stored in Firebase changes.

  • Use these two components to build a distributed, streaming, backend service to collect and process log data.

Costs

Firebase has a free level of usage. If your usage of these services is less than the limits specified in the Firebase free plan there is no charge for using Firebase.

Instances within the App Engine flexible environment are charged the cost of the underlying Google Compute Engine Virtual Machines. This is Beta-only pricing and will increase once the flexible environment is GA.

Before you begin

Install the following software:

Install the App Engine Java component of the Cloud SDK by running the following command from the command line.

gcloud components install app-engine-java

Cloning the sample code

  1. Clone the front end client app code.

    git clone https://github.com/GoogleCloudPlatform/firebase-android-client
    
  2. Clone the backend servlet code.

    git clone https://github.com/GoogleCloudPlatform/firebase-appengine-backend
    

Generating a SHA-1 fingerprint for the app

To authenticate your client app for Google Sign-in, you need to provide a SHA-1 fingerprint of the certificate. This tutorial uses the debug keystore For information about creating release versions of the keystore fingerprint, see Authenticating Your Client.

  • Build a SHA-1 of your debug keystore.

    keytool -exportcert -list -v \
    -alias androiddebugkey -keystore ~/.android/debug.keystore
    

Creating a Firebase project

  1. Create a Firebase account or log into an existing account.

  2. Click Create new project.

  3. In Project name, enter: Playchat

  4. Select Add Firebase to your Android App.

  5. In Package name, enter: com.google.cloud.solutions.flexenv

  6. In Debug signing certificate SHA-1, enter the SHA-1 value you generated in the previous section.

  7. Register the app and download the generated file, google-services.json.

  8. Make a note of the changes you need to make to the project-level and app-level build.gradle files and click Finish.

  9. From the left menu of the Firebase console, select Database.

    This displays the data you’ve stored in Firebase. In later steps of this tutorial, you can revisit this web page to see data added and updated by the client app and backend servlet.

  10. Make a note of the Firebase URL for your project. This will be in the form https://[FIREBASE_PROJECT_ID].firebaseio.com/ and appears next to a link icon.

Enabling Google authentication for the Firebase project

There are a variety of sign in providers you can configure to connect to your Firebase project. This tutorial walks you through setting up user authentication using their Google account.

  1. From the left menu of the Firebase console, select Authentication in the Develop group.

  2. Click SET UP SIGN-IN METHOD.

  3. Select Google, turn the Enable toggle on, and click Save.

Adding a service account to the Firebase project

The backend servlet is not a person and does not have a Google user account to log in with. Instead, it uses a service account to connect to Firebase. The following steps walk you through creating a service account that can connect to Firebase and adding the service account credentials to the servlet code.

  1. From the left menu of the Firebase console, next to the Playchat project home, select the Settings gear and then Permissions.

  2. Select Service accounts and then Create service account.

  3. Configure the following settings:

    1. In Service account name, enter playchat-servlet.
    2. In Role, select Project > Owner.

    3. Check Furnish a new private key.

    4. Select JSON for Key Type.
  4. Click Create.

  5. Download the JSON key file for the service account and save to the backend service project, firebase-appengine-backend, in the src/main/webapp/WEB-INF/ directory. The filename will be in the form Playchat-[UNIQUE_ID].json.

  6. Edit src/main/webapp/WEB-INF/web.xml and edit the initialization parameters as follows:

  7. Replace JSON_FILE_NAME with the name of the JSON key file you downloaded.

  8. Replace FIREBASE_URL with the Firebase URL you noted earlier.

    <init-param>
      <param-name>credential</param-name>
      <param-value>/WEB-INF/JSON_FILE_NAME</param-value>
    </init-param>
    <init-param>
      <param-name>databaseUrl</param-name>
      <param-value>FIREBASE_URL</param-value>
    </init-param>
    

Enabling billing and APIs for the Cloud Platform project

In order for the backend service to run on Cloud Platform, you need to enable billing and APIs for the project. The Cloud Platform project is the same project you created in Create a Firebase project and has the same project identifier.

  1. In the Cloud Platform Console, select the Playchat project.

    Go to the Projects page

  2. Enable billing for your project.

    Enable billing

  3. Enable the App Engine Admin API and Compute Engine API APIs.

    Enable the APIs

Building and deploying the backend service

The backend service in this sample uses a Docker configuration to specify its hosting environment. This customization means you must use the App Engine flexible environment instead of the standard environment.

To build the backend servlet and deploy it in the App Engine flexible environment, you can use the Cloud SDK App Engine Apache Maven plugin. This plugin is already specified in the Maven build file included with this sample.

Setting the project

In order for Maven to build the backend servlet correctly, it must know which Cloud Platform project to launch the servlet resources into. The Cloud Platform project identifier and the Firebase project identifier are the same.

  1. Set the project to your Firebase project with the following command, replacing [FIREBASE_PROJECT_ID] with the name of the Firebase project ID you noted previously.

    gcloud config set project [FIREBASE_PROJECT_ID]
    
  2. Verify that the project has been set by listing the configuration.

    gcloud config list
    

(Optional) Running the service on the local server

When you are developing a new backend service, it can be useful to run the service locally before you deploy it to App Engine. This makes it possible to rapidly iterate changes without the overhead of a full deployment to App Engine.

When you run the server locally, it doesn’t use a Docker configuration or run in an App Engine environment. Instead, Maven guarantees all dependent libraries are installed locally and the app runs on Jetty.

  1. In the firebase-appengine-backend directory, build and run the backend module locally with the following command:

    mvn clean gcloud:run
    
    mvn clean gcloud:run -Dgcloud.gcloud_directory=[PATH_TO_TOOL]
    
  2. If you are prompted Do you want the application "Python.app" to accept incoming network connections? select Allow.

When the deployment ends, open http://localhost:8080/printLogs to verify that your backend service is running. You should see a web page that displays “Inbox :” followed by a 16-digit identifier. This is the inbox identifier for the servlet running on your local machine.

As you refresh the page, this identifier does not change; your local server spins up a single servlet instance. This can be useful during testing; there will only be one servlet identifier stored in the Firebase Realtime Database.

To shut down the local server, click Control+C.

Deploying the service to the App Engine flexible environment

When you run the backend service in the App Engine flexible environment, App Engine uses the configuration in /firebase-appengine-backend/src/main/webapp/Dockerfiles to build the hosting environment that the service runs in. The flexible environment will spin up several servlet instances and scale them up and down to meet demand.

  • In the firebase-appengine-backend directory, build and run the backend module locally with the following command:

    mvn gcloud:deploy
    
    mvn clean gcloud:deploy -Dgcloud.gcloud_directory=[PATH_TO_GCLOUD]
    

As the build runs, you will see the lines “Sending build context to Docker daemon …” these steps upload your Docker configuration and set it up in the App Engine flexible environment.

When the deployment ends, open https://[FIREBASE_PROJECT_ID].appspot.com/printLogs, where [FIREBASE_PROJECT_ID] is the identifier from Create a Firebase project. You should see a web page that displays “Inbox :” followed by a 16-digit identifier. This is the inbox identifier for a servlet running in the App Engine flexible environment.

As you refresh the page, this identifier should periodically change as App Engine spins up multiple servlet instances to handle incoming client requests.

Adding Firebase and Google Play Services to the Android app

The client app uses the Firebase Realtime Database to store and sync messages and to record user-event logs. The client app uses Google Play Services to authenticate users with their Google account.

Linux/Windows

  1. In Android Studio, select Open an existing Android Studio project and choose the firebase-android-client directory.

  2. Wait for the Gradle project information to finish building. If you are prompted to use the Gradle wrapper, click OK.

  3. Add the google-services.json file you downloaded in Create a Firebase project to the app directory.

  4. The changes to the project-level and app-level build.gradle files that you noted in Create a Firebase project have already been made in the sample code.

  5. Select Configure.

  6. Select SDK Manager.

  7. At the top of the right pane, select SDK Tools.

  8. Make sure the checkbox next to Google Play Services is checked.

  9. Click OK.

Mac OS X

  1. In Android Studio, select Open an existing Android Studio project and choose the firebase-android-client directory.

  2. Wait for the Gradle project information to finish building. If you are prompted to use the Gradle wrapper, click OK.

  3. Add the google-services.json file you downloaded in Create a Firebase project to the app directory.

  4. The changes to the project-level and app-level build.gradle files that you noted in Create a Firebase project have already been made in the sample code.

  5. From the top menu, select Android Studio > Preferences.

  6. Select Appearance & Behavior > System Settings > Android SDK.

  7. Select SDK Tools.

  8. Make sure the checkbox next to Google Play Services is checked.

  9. Click OK.

Running and testing the Android app

  1. In Android Studio, with the firebase-android-client project open, select Run > Run app.

  2. Select or create a Nexus 6 emulator running Marshmallow (API level 23) as your test device.

  3. When the app is loaded onto the emulator, sign in with your Google account.

    Sign in to Playchat

  4. Click the menu to the left of the PlayChat title and select the books channel.

    Select a channel

  5. Enter a message.

    Send a message

  6. When you do, the Playchat app stores your message in the Firebase Realtime Database. Firebase synchronizes the data stored in the database across devices. Devices running Playchat will display the new message when a user selects the books channel.

    Send a message

Verifying the data

After you’ve used the Playchat app to generate some user events using the Playchat app, you can verify that the servlets are registering as listeners and collecting user-event logs.

Open the Firebase Realtime Database for your app, where [FIREBASE_PROJECT_ID] is the identifier from Create a Firebase project.

https://console.firebase.google.com/project/[FIREBASE_PROJECT_ID]/database/data

At the bottom of the Firebase Realtime Database, under the /requestLogger/ data location, you should see a randomly generated key that represents a user’s account login, in this example: 1234AbCd5678EfgH90iJKlmnopQR1. Next to it is the 16-digit numeric identifier of the servlet currently listening to log events from that user, in this example 1782669091044729.

Data stored in Firebase Realtime Database

Immediately above, under the /inbox/ data location, are servlet identifiers for all currently assigned servlets. In this example, only one servlet is collecting logs. Under /inbox/[SERVLET_IDENTIFIER] are the user logs written by the app to that servlet.

Open the App Engine page for your backend service: https://[FIREBASE_PROJECT_ID].appspot.com/printLogs, where [FIREBASE_PROJECT_ID] is the identifier from Create a Firebase project. When the page displays the identifier for the servlet that recorded the user events you generated, you will see log entries for those events below the servlet’s inbox identifier.

Exploring the code

The Playchat Android app defines a class, FirebaseLogger, that it uses to write user-event logs to the Firebase Realtime Database.

import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.cloud.solutions.flexenv.common.LogEntry;

/*
 * FirebaseLogger pushes user event logs to a specified path.
 * A backend Servlet instance listens to
 * the same key and keeps track of event logs.
 */
public class FirebaseLogger {
    private DatabaseReference logRef;
    private String tag;

    public FirebaseLogger(DatabaseReference firebase, String path) {
        logRef = firebase.child(path);
    }

    public void log(String tag, String message) {
        LogEntry entry = new LogEntry(tag, message);
        logRef.push().setValue(entry);
    }

}

When a new user logs in, Playchat calls the requestLogger function to add a new entry to the /requestLogger/ location in the Firebase Realtime Database and set a listener so Playchat can respond when a servlet updates the value of that entry, accepting the assignment.

When a servlet updates the value, Playchat removes the listener and writes the “Signed in” log to the servlet’s inbox.

/*
 * Request that a Servlet instance be assigned.
 */
private void requestLogger() {
    firebase.child(IBX + "/" + inbox).removeValue();
    firebase.child(IBX + "/" + inbox).addValueEventListener(new ValueEventListener() {
        public void onDataChange(DataSnapshot snapshot) {
            if (snapshot.exists()) {
                fbLog = new FirebaseLogger(firebase, IBX + "/" + snapshot.getValue().toString()
                        + "/logs");
                firebase.child(IBX + "/" + inbox).removeEventListener(this);
                fbLog.log(inbox, "Signed in");
            }
        }

        public void onCancelled(DatabaseError error) {
            Log.e(TAG, error.getDetails());
        }
    });

    firebase.child(REQLOG).push().setValue(inbox);
}

On the backend service side, when a servlet instance starts, the init(ServletConfig config) function in MessageProcessorServlet.java connects to the Firebase Realtime Database and adds a listener to the /requestLogger/ data location.

When a new entry is added to the /requestLogger/ data location, the servlet updates the value with its identifier, a signal to the Playchat app that the servlet accepts the assignment to process logs for that user. The servlet uses Firebase transactions to ensure that only one servlet can update the value and accept the assignment.

/*
 * Receive a request from an Android client and reply back its inbox ID.
 * Using a transaction ensures that only a single Servlet instance replies
 * to the client. This lets the client knows to which Servlet instance
 * to send consecutive user event logs.
 */
firebase.child(REQLOG).addChildEventListener(new ChildEventListener() {
  public void onChildAdded(DataSnapshot snapshot, String prevKey) {
    firebase.child(IBX + "/" + snapshot.getValue()).runTransaction(new Transaction.Handler() {
      public Transaction.Result doTransaction(MutableData currentData) {
        // The only first Servlet instance will write
        // its ID to the client inbox.
        if (currentData.getValue() == null) { currentData.setValue(inbox); }
        return Transaction.success(currentData);
      }

      public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) {}
    });
    firebase.child(REQLOG).removeValue();
  }

After a servlet has accepted an assignment to process a user’s event logs, it adds a listener that detects when the Playchat app writes a new log file to the servlet’s inbox. The servlet responds by retrieving the new log data from the Firebase Realtime Database.

/*
 * Initialize user event logger. This is just a sample implementation to
 * demonstrate receiving updates. A production version of this application
 * should transform, filter or load to other data store such as Google BigQuery.
 */
private void initLogger() {
  String loggerKey = IBX + "/" + inbox + "/logs";
  purger.registerBranch(loggerKey);
  firebase.child(loggerKey).addChildEventListener(new ChildEventListener() {
    public void onChildAdded(DataSnapshot snapshot, String prevKey) {
      if (snapshot.exists()) {
        LogEntry entry = snapshot.getValue(LogEntry.class);
        logs.add(entry);
      }
    }

    public void onCancelled(DatabaseError error) { localLog.warning(error.getDetails()); }

    public void onChildChanged(DataSnapshot arg0, String arg1) {}

    public void onChildMoved(DataSnapshot arg0, String arg1) {}

    public void onChildRemoved(DataSnapshot arg0) {}
  });
}

Cleaning up

To avoid incurring charges to your Google Cloud Platform account for the resources used in this tutorial:

Delete the Cloud Platform and Firebase project

The simplest way to stop billing charges is to delete the project you created for this tutorial. Although you created the project in the Firebase console, you can also delete it in the Cloud Platform console, since the Firebase and Cloud Platform projects are one and the same.

  1. In the Cloud Platform Console, go to the Projects page.

    Go to the Projects page

  2. In the project list, select the project you want to delete and click Delete project. After selecting the checkbox next to the project name, click
      Delete project
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Delete non-default versions your App Engine app

If you don't want to delete your Cloud Platform and Firebase project, you can reduce costs by deleting the non-default versions of your App Engine flexible environment app.

  1. In the Cloud Platform Console, go to the App Engine Versions page.

    Go to the Versions page

  2. Click the checkbox next to the non-default app version you want to delete.
  3. Click the Delete button at the top of the page to delete the app version.

What's next

  • Analyze and archive data — In this sample, the servlets store log data only in memory. To extend this sample, you could have the servlets archive, transform, and analyze the data, using services such as Cloud Storage, Cloud Bigtable, Google Cloud Dataflow, and BigQuery.

  • Evenly distribute the workload across servlets — App Engine provides both automatic and manual scaling. With automatic scaling, the flexible environmment detects increases or decreases in workload and responds by adding or removing VMs in the cluster. With manual scaling, you specify a static number of VMs to handle traffic. For more information about how to configure scaling, see Service scaling settings in the App Engine documentation.

    Because user activity logs are assigned to the servlets through accessing the Firebase Realtime Database, the workload might not be evenly distributed. For example, one servlet might process more user-event logs than other servlets.

    You can improve efficiency by implementing a workload manager that independently controls workload on each VM. This workload balancing could be based on metrics like logging requests per second or number of concurrent clients.

  • Recover unprocessed user-event logs — In this sample implementation, if a servlet instance crashes, the Android app associated with that instance continues to send log events to the servlet’s inbox in the Firebase Realtime Database. In a production version of this app, the backend service should detect this situation to recover the unprocessed user-event logs.

Monitor your resources on the go

Get the Google Cloud Console app to help you manage your projects.

Send feedback about...