Using Firebase and App Engine Standard Environment in an Android App

This tutorial demonstrates how to write a mobile app with backend data storage, real-time synchronization, and email notifications using Firebase and Google App Engine.

The instructions show how to do this using:

  • Firebase — a powerful platform for building iOS, Android, and web-based apps, offering real-time data storage and synchronization, user authentication, and more.
  • Android Studio — an Android development environment based on IntelliJ IDEA.
  • Cloud Tools for Android Studio — a set of tools, included with Android Studio, that provide integration with Google Cloud Platform services.
  • App Engine — an environment for running application code in the cloud; Platform as a service (PaaS).

The sample app stores a to-do list in Firebase, which automatically synchronizes the data across devices, and then uses backend logic running on App Engine to send out daily reminder emails.

By connecting App Engine to a Firebase database, you can perform complex logic on data without having to manage synchronization and updates; Firebase handles that for you.

Objectives

In the this tutorial, you'll learn how to:

  • Create a simple Android app using Android Studio.
  • Connect your app to Firebase to store data.
  • Create a backend service running on App Engine using Android Studio.
  • Connect the backend service to your data in Firebase.

Costs

Both Firebase and App Engine standard environment have free levels of usage. If your usage of these services is less than the limits specified in the Firebase free plan and the App Engine free quota, there is no charge for doing this tutorial.

Before you begin

Creating an Android Studio project

Follow these steps to create a project.

  1. Open Android Studio and select Start a new Android Studio project.

    Start a new project

  2. In Application name, enter ToDoApp, and select Next.

    Configure your new project

  3. Leave the form factors at the default values and select Next.

    Target Android devices

  4. Select Basic Activity and select Next.

    Add activity

  5. Leave the activity settings at the default values and select Finish.

    Customize activity

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: ToDoFirebaseProject

  4. Select Add Firebase to your Android App.

  5. In Package name, enter: com.example.username.todoapp

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

  7. Select Add 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.

Set security rules for the Realtime Database

By default, the security rules for the Realtime Database do not allow access to the data except through the Firebase console. To make it possible for your app to read and write data to the database, you can change the security rules.

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

  2. Selecte Rules.

  3. Replace the rules content with the following. This makes the data readable to the public, including anonymous users. For more information about setting security rules on the Firebase Realtime Database, see Get Started with Database Rules.

    {
      "rules": {
          ".read": true,
          ".write": true
        }
    }
    

Updating the project-level Gradle build file

  • Add the following dependency to the project-level build.gradle file.

    dependencies {
        ...
        classpath 'com.google.gms:google-services:3.0.0'
    }
    

Updating the app-level Gradle build file

  1. In Android Studio, add the following packagingOptions directive to the app-level build.gradle file. This excludes the listed files from the build to avoid potential build errors due to duplicate files.

    android {
        ...
        packagingOptions {
            exclude 'META-INF/LICENSE'
            exclude 'META-INF/LICENSE-FIREBASE.txt'
            exclude 'META-INF/NOTICE'
        }
    }
    
  2. Add the following line to the dependencies section.

    dependencies {
        ...
        compile 'com.google.firebase:firebase-database:9.0.2'
    }
    
  3. Add the following line to the bottom of the app-level build.gradle file.

    apply plugin: com.google.gms.google-services
    

  4. Click Sync Now to update the Gradle build.

Adding a user interface to your Android app

  1. Replace the contents of app/src/main/res/layout/activity_main.xml with the following code . This adds a ListView control to display the to-do items stored in Firebase, an EditText control to enter new to-do items, and a Button control to save the new to-do item.

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"
        android:orientation="vertical">
    
        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1">
        </ListView>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:layout_gravity="bottom">
            <EditText
                android:id="@+id/todoText"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"/>
            <Button
                android:id="@+id/addButton"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="Add New Item"/>
        </LinearLayout>
    </LinearLayout>
    
  2. In app/src/main/java/[BUNDLE_ID]/MainActivity.java, add the following import statements.

    import com.google.firebase.database.ChildEventListener;
    import com.google.firebase.database.DataSnapshot;
    import com.google.firebase.database.DatabaseError;
    import com.google.firebase.database.DatabaseReference;
    import com.google.firebase.database.FirebaseDatabase;
    import com.google.firebase.database.Query;
    import com.google.firebase.database.ValueEventListener;
    
  3. In app/src/main/java/[BUNDLE_ID]/MainActivity.java, replace the onCreate function with the following code.

    This code uses Firebase to listen for changes when to-do items are added or removed.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
    
          // Get ListView object from xml
          final ListView listView = (ListView) findViewById(R.id.listView);
    
          // Create a new Adapter
          final ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
                  android.R.layout.simple_list_item_1, android.R.id.text1);
    
          // Assign adapter to ListView
          listView.setAdapter(adapter);
    
          // Connect to the Firebase database
          FirebaseDatabase database = FirebaseDatabase.getInstance();
    
          // Get a reference to the todoItems child items it the database
          final DatabaseReference myRef = database.getReference("todoItems");
    
          // Assign a listener to detect changes to the child items
          // of the database reference.
          myRef.addChildEventListener(new ChildEventListener(){
    
          // This function is called once for each child that exists
          // when the listener is added. Then it is called
          // each time a new child is added.
          @Override
          public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
              String value = dataSnapshot.getValue(String.class);
              adapter.add(value);
          }
    
          // This function is called each time a child item is removed.
          public void onChildRemoved(DataSnapshot dataSnapshot){
              String value = dataSnapshot.getValue(String.class);
              adapter.remove(value);
          }
    
          // The following functions are also required in ChildEventListener implementations.
          public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName){}
          public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName){}
    
          @Override
          public void onCancelled(DatabaseError error) {
              // Failed to read value
              Log.w("TAG:", "Failed to read value.", error.toException());
          }
      });
    
      // Add items via the Button and EditText at the bottom of the window.
      final EditText text = (EditText) findViewById(R.id.todoText);
      final Button button = (Button) findViewById(R.id.addButton);
    
      button.setOnClickListener(new View.OnClickListener() {
          public void onClick(View v) {
    
              // Create a new child with a auto-generated ID.
              DatabaseReference childRef = myRef.push();
    
              // Set the child's data to the value passed in from the text box.
              childRef.setValue(text.getText().toString());
    
          }
      });
    
      // Delete items when clicked
      listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    
          public void onItemClick(AdapterView<?> parent, View view,
                                  int position, long id) {
    
              Query myQuery = myRef.orderByValue().equalTo((String)
                    listView.getItemAtPosition(position));
    
              myQuery.addListenerForSingleValueEvent(new ValueEventListener() {
                  @Override
                  public void onDataChange(DataSnapshot dataSnapshot) {
                      if (dataSnapshot.hasChildren()) {
                          DataSnapshot firstChild = dataSnapshot.getChildren().iterator().next();
                          firstChild.getRef().removeValue();
                      }
                  }
    
                  @Override
                  public void onCancelled(DatabaseError databaseError) {
                  }
              })
          ;}
        })
    ;}
    
  4. Resolve any unrecognized symbols by hovering over them and pressing ALT + Enter. This imports any required packages that are missing.

  5. Build and run your app.

  6. Enter Finish this app. in the text field and click Add new item. Then enter two additional entries: Test the app. and Write the backend servlet.

    Data displayed on mobile device

  7. Check that your item was sent to the Firebase Realtime Database by opening the Firebase URL in a browser. You can find this value in google-services.json. It is in the form https://[FIREBASE_PROJECT_ID].firebaseio.com/.

    Data added to Firebase

  8. Hover over the Finish the app. entry in the Firebase console and click the red X to delete the entry from the Firebase Realtime Database. Notice how quickly this change is reflected in the data displayed in your mobile app.

  9. Tap the Test the app. entry in the mobile app to delete it from the mobile device. Notice how fast the change is reflected in the Firebase Realtime Database entries displayed in the Firebase console.

In a few steps, you've created a simple task-management application that adds and removes to-do items. Storing the data in Firebase ensures that the data is synchronized across user devices and Firebase gracefully handles situations when the device is off-line. Firebase also provides a dashboard that you can use to test the app during development.

The next step is to add a backend service running on App Engine standard environment to extend the functionality of the mobile app to send daily email reminders.

Adding backend logic using App Engine

The Firebase Java client library runs on App Engine. This means you don't have to call the raw REST interface, which makes writing backend logic for your apps easier.

To add backend logic to the app:

  • Add a backend module to the existing Android Studio project.
  • Add the required dependencies to the App Engine SDK and Firebase.
  • Create a simple servlet that runs in the background, and sends an email each morning with your outstanding to-do items.
  • Deploy the backend module to App Engine and test it with your app.

Adding the backend module

Android Studio includes Cloud Tools for Android Studio that make it easy to add backend logic running on App Engine to your Firebase application.

  1. Select File > New > New Module.

  2. Select Google Cloud Module and click Next.

    Select the module

  3. Change Module type to App Engine Java Servlet Module and click Finish.

    Change the module type

Adding dependencies to the backend module

In order to connect to Firebase and send email, you need to add library dependencies to your backend module.

  1. Open File > Project Structure and select the backend module.

  2. Click the plus sign (+) at the bottom and add the following library dependencies:

    • com.google.appengine:appengine-api-1.0-sdk
    • com.google.firebase:firebase-server-sdk
    • org.apache.httpcomponents:httpclient

    Add dependencies

Enabling Billing

In order to use the Socket API, you must enable billing on your Cloud Platform project.

To enable billing for your project, do the following:

  1. Visit the Billing page.

  2. If you don't have an active billing account, create one by clicking New billing account and following the instructions.

Adding a service account to the Cloud Platform 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 in the Cloud Platform project that was created when you created the Firebase project.

  1. In the Cloud Platform console, select the top left icon to open the Products & Services menu.

  2. Select IAM & Admin.

  3. Select Service accounts.

  4. Select Create service account.

  5. In Service account name, enter todo-servlet.

  6. In Role, select Project > Owner

  7. Make a note of the value in Service account ID, you will use this value in the next section. The id will be in the form [SERVLET_ACCOUNT_NAME]@[FIREBASE_PROJECT_ID].iam.gserviceaccount.com

    1. Check Furnish a new private key.

    2. Select JSON for Key Type.

  8. Click Create.

  9. Download the JSON key file for the service account and save it in the src/main/webapp/WEB-INF/ directory of the servlet component. The filename will be in the form [FIREBASE_PROJECT_ID]-[UNIQUE_ID].json.

Adding Firebase IAM Permissions for the service account

After you create the service account, you need to grant that service account permission to access the Firebase project.

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

  2. Select IAM.

  3. Select Add.

  4. In Members enter the service account ID you noted in the previous step.

  5. In Select a role, select Project and then Owner.

  6. Click Add.

Configuring web pages for the servlet

  • Replace the contents of web.xml with the following XML, replacing [USERNAME] with your user name.

    <?xml version="1.0" encoding="utf-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
        <servlet>
            <servlet-name>MyServlet</servlet-name>
            <servlet-class>com.example.[USERNAME].myapplication.backend.MyServlet</servlet-class>
        </servlet>
        <servlet-mapping>
            <servlet-name>MyServlet</servlet-name>
            <url-pattern>/send-email</url-pattern>
        </servlet-mapping>
        <welcome-file-list>
            <welcome-file>index.html</welcome-file>
        </welcome-file-list>
    </web-app>
    

After the servlet is deployed, you can open the URL http://[FIREBASE_PROJECT_ID].appspot.com/send-email in a web browser to cause the servlet to immediately send an email. After developing and testing the servlet, you should restrict access to the /send-email web page.

Configuring scheduled tasks for the servlet

To have App Engine run the servlet periodically, set a cron entry in cron.xml.

  1. Switch your view to Project and create a new file, cron.xml, in the WEB-INF directory.

  2. Copy the following XML to the file and save the file.

    <?xml version="1.0" encoding="UTF-8"?>
    <cronentries>
       <cron>
           <url>/send-email</url>
           <description>Email users their list of work items.</description>
           <schedule>every 2 minutes</schedule>
       </cron>
    </cronentries>
    

This configuration causes the servlet to send the reminder email every two minutes, which is useful during testing. For a production app you could change this value to every 24 hours to send a daily email. For more information, see Scheduled Tasks With Cron for Java.

Configuring the App Engine backend to use manual scaling

To use Firebase with App Engine standard environment, you must use manual scaling. This is because Firebase uses background threads to listen for changes and App Engine standard environment allows long-lived background threads only on manually scaled backend instances.

  • In the WEB-INF directory, update appengine-web.xml with a manual scaling directive, as shown in the following XML.

    <?xml version="1.0" encoding="utf-8"?>
    <appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
       <application>myApplicationId</application>
       <version>1</version>
       <threadsafe>true</threadsafe>
       <manual-scaling>
           <instances>1</instances>
       </manual-scaling>
       <system-properties>
           <property name="java.util.logging.config.file" value="WEB-INF/logging.properties" />
       </system-properties>
    </appengine-web-app>
    

Writing the servlet code

Replace the contents of MyServlet.java with the following, making the following substitutions:

  • Change [USERNAME] to your user name
  • Change [FIREBASE_PROJECT_ID] to your Firebase project identifier.
  • Change [PRIVATE_KEY_FILE] to the name of the JSON private key file you downloaded in Adding a service account to the Cloud Platform project
  • Change [EMAIL@GMAIL.COM] to your Google account email address.

    package com.example.[USERNAME].myapplication.backend;
    
    import com.google.api.client.googleapis.auth.clientlogin.ClientLogin;
    import com.google.firebase.FirebaseApp;
    import com.google.firebase.FirebaseOptions;
    import com.google.firebase.database.DataSnapshot;
    import com.google.firebase.database.DatabaseError;
    import com.google.firebase.database.DatabaseReference;
    import com.google.firebase.database.FirebaseDatabase;
    import com.google.firebase.database.ValueEventListener;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.UnsupportedEncodingException;
    import java.util.Iterator;
    import java.util.Properties;
    import java.util.logging.Logger;
    
    import javax.mail.Message;
    import javax.mail.MessagingException;
    import javax.mail.Session;
    import javax.mail.Transport;
    import javax.mail.internet.InternetAddress;
    import javax.mail.internet.MimeMessage;
    import javax.servlet.http.*;
    import javax.xml.ws.Response;
    
    public class MyServlet extends HttpServlet {
        static Logger Log = Logger.getLogger("com.example.[USERNAME].myapplication.backend.MyServlet");
    
        @Override
        public void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws IOException {
            Log.info("Sending the todo list email.");
    
            String outString;
            outString = "<p>Sending the todo list email.</p><p><strong>Note:</strong> ";
            outString = outString.concat("the servlet must be deployed to App Engine in order to ");
            outString = outString.concat("send the email. Running the server locally writes a message ");
            outString = outString.concat("to the log file instead of sending an email message.</p>");
    
            resp.getWriter().println(outString);
    
            // Note: Ensure that the [PRIVATE_KEY_FILENAME].json has read
            // permissions set.
            FirebaseOptions options = new FirebaseOptions.Builder()
                .setServiceAccount(getServletContext().getResourceAsStream("/WEB-INF/[PRIVATE_KEY_FILE]"))
                .setDatabaseUrl("https://[FIREBASE_PROJECT_ID].firebaseio.com/")
                .build();
    
            try {
                FirebaseApp.getInstance();
            }
            catch (Exception error){
                Log.info("doesn't exist...");
            }
    
            try {
                FirebaseApp.initializeApp(options);
            }
            catch(Exception error){
                Log.info("already exists...");
            }
    
            // As an admin, the app has access to read and write all data, regardless of Security Rules
            DatabaseReference ref = FirebaseDatabase
                    .getInstance()
                    .getReference("todoItems");
    
            // This fires when the servlet first runs, returning all the existing values
            // only runs once, until the servlet starts up again.
            ref.addListenerForSingleValueEvent(new ValueEventListener() {
    
                @Override
                public void onDataChange(DataSnapshot dataSnapshot) {
                    Object document = dataSnapshot.getValue();
                    Log.info("new value: "+ document);
    
                    String todoText = "Don't forget to...\n\n";
    
                    Iterator<DataSnapshot> children = dataSnapshot.getChildren().iterator();
    
                    while(children.hasNext()){
                        DataSnapshot childSnapshot = (DataSnapshot) children.next();
                        todoText = todoText + " * " + childSnapshot.getValue().toString() + "\n";
                    }
    
                    // Now send the email
    
                    // Note: When an application running in the development server calls the Mail
                    // service to send an email message, the message is printed to the log.
                    // The Java development server does not send the email message.
    
                    // You can test the email without waiting for the cron job to run by
                    // loading http://[FIREBASE_PROJECT_ID].appspot.com/send-email in your browser.
    
                    Properties props = new Properties();
                    Session session = Session.getDefaultInstance(props, null);
                    try {
                        Message msg = new MimeMessage(session);
                        //Make sure you substitute your project-id in the email From field
                        msg.setFrom(new InternetAddress("reminder@[FIREBASE_PROJECT_ID].appspotmail.com",
                                "Todo Reminder"));
                        msg.addRecipient(Message.RecipientType.TO,
                                new InternetAddress("[EMAIL@GMAIL.COM]", "Recipient"));
                        msg.setSubject("Things to do today");
                        msg.setText(todoText);
                        Transport.send(msg);
                    } catch (MessagingException | UnsupportedEncodingException e) {
                        Log.warning(e.getMessage());
                    }
    
                    // Note: in a production application you should replace the hard-coded email address
                    // above with code that populates msg.addRecipient with the app user's email address.
                }
    
                @Override
                public void onCancelled(DatabaseError error){
                    System.out.println("Error: "+error);
                }
            });
        }
    }
    

Because you defined a schedule in cron.xml, App Engine periodically runs the servlet. When it does, the servlet installs a child listener using the addListenerForSingleValueEvent method. Each listener fires once, sending an email message, and then is removed.

The next step is to deploy the backend and test it.

Deploying the App Engine backend

  1. In Android Studio, select Build > Deploy Module to App Engine.

  2. Select the Firebase project id from the drop-down menu. When you created the Firebase project you also created a Cloud Platform project with the same identifier. Android Studio deploys the backend servlet module to App Engine standard environment running on Cloud Platform.

  3. Click Deploy to deploy your App Engine module. Run your Android app in the emulator and add some to-do items.

  4. To test the servlet without waiting for the cron job to run, open a web browser and load the following URL, http://[FIREBASE_PROJECT_ID].appspot.com/send-email

    You should see an email like the following:

    Subject: Things to do today
    From: reminder@[FIREBASE_PROJECT_ID].appspotmail.com
    -
    Don't forget to...
     * Test the backend servlet.
    

Congratulations! You've created an Android app with real-time data synchronization and backend logic to send daily reminder emails.

What's next

You can extend the functionality of your app.

  • Change the email messaging from App Engine Mail API to a supported third-party mail provider, such as SendGrid, Mailjet, or Mailgun, which have higher quotas than the 10 messages a day default provided by the App Server Mail API.

  • Use Firebase Authentication to support individual to-do lists for each user.

  • Update the Firebase Realtime Database rules to allow read and write access to only logged in app user and the servlet service account.

  • Use Firebase Cloud Messaging to send push notifications to the mobile device as an alternative to daily emails.

  • Make it possible for the user to turn email notifications on and off.

  • Provide a way for users to rank to-do items by priority.

  • Learn more about building mobile solutions on Google Cloud Platform.

Send feedback about...