Asset trackers are an often required application for any company that has trucks, buses, or other assets that are on the move. In this codelab you will build the components that are responsible for driving a Transport Tracker solution, based on the I/O Bus Tracker. We will show you how to use the Google Maps Android API, along with the Firebase Realtime Database, to build a mobile system that can be used to track assets in near real time.

What you will build

What you'll learn

What you'll need

Create a Firebase project and user:

Note: You can skip this step if you have an existing Firebase project and user created.

  1. Log into the Firebase console (using your Google account) and choose Add project
  2. Enter a project name and remember it, you'll need it shortly
  3. Create the project
  4. Choose Authentication in the left-hand navigation
  5. Under the Sign-in method tab, enable Email/Password
  6. Under the Users tab, click Add User, create a user, and make a note of the email address and password - these are the credentials we'll use shortly to authenticate the app with Firebase

Start a new Android Studio project:

  1. Application Name: Transport Tracker
  2. Company Domain: example.com
  3. Leave the other settings at their default values
  4. When prompted to add an activity, choose Empty Activity, and call it TrackerActivity

Connect your app to Firebase using the Firebase Assistant:

  1. In Android Studio, click Tools > Firebase to open the Assistant window
  2. Click to expand the Authentication feature, then click Email and password authentication
  3. Click Connect to Firebase (allowing some time for the connection to be made)
  4. In Android Studio, select Choose an existing Firebase or Google project, then select the project you previously created
  5. Click Connect to Firebase (allowing some time for the connection to be made), and wait for the messages in the Android Studio status bar to complete
  6. Click Add Firebase Authentication to your app
  7. Click Accept Changes
  8. Click the back arrow at the top of the Assistant
  9. Click Realtime Database
  10. Click Save and retrieve data
  11. Click Add the Realtime Database to your app
  12. Click Accept Changes
  13. For more information, see the documentation on the Firebase Assistant

You now have an empty Android application created, with Firebase configured. You can close the Firebase Assistant for now.

There's one last step before we dive into the code - we need to manually add the Google Play Services dependency for location services, which is the API used for tracking the device's location.

  1. Open the build.gradle file (there are two under Gradle scripts, choose the one with the label Module:app),
  2. At the bottom of the file you'll find a dependencies section, with lines containing com.google.firebase which were automatically added in the previous steps using the Assistant (the version numbers at the end may be different, we'll change them shortly):

build.gradle

    implementation 'com.google.firebase:firebase-auth:11.2.0'
    implementation 'com.google.firebase:firebase-database:11.2.0'
  1. Add the following line directly after those (but before the trailing bracket toward the end):

build.gradle

    implementation 'com.google.android.gms:play-services-location:11.2.0'
  1. Make sure that the version number at the end of each line for the play-services-location dependency and the two Firebase dependencies, are the same. If Android Studio added the Firebase dependencies with a different version number, go ahead and update those to the same as play-services-location.
  2. Lastly in build.gradle, you'll see a line with compileSdkVersion and a line with targetSdkVersion, each with a version number specified - ensure these are both set to 25. You'll also need to update the following line accordingly so that the version number begins with 25 also:

build.gradle

    implementation 'com.android.support:appcompat-v7:25.4.0'
  1. Click the Sync Now link shown above the top of build.gradle
  2. Run your app. It will show the Hello World screen.

The Tracker application will track the device's location, and store it in Firebase. We'll use an Android Service which will allow location tracking to occur independently of any UI elements.

Create the service in Android Studio:

File > New > Service > Service > Name it TrackerService, leaving the default options selected.

A good practice to apply when tracking the user's location is to ensure they're always aware that their location is being tracked. To do this, we can use a persistent notification that shuts the app down when the notification is tapped.

Replace the code in TrackerService.java with the following code:

TrackerService.java

package com.example.transporttracker;

import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;

import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.location.Location;
import android.Manifest;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;

public class TrackerService extends Service {

    private static final String TAG = TrackerService.class.getSimpleName();

    @Override
    public IBinder onBind(Intent intent) {return null;}

    @Override
    public void onCreate() {
        super.onCreate();
        buildNotification();
        loginToFirebase();
    }

    private void buildNotification() {
        String stop = "stop";
        registerReceiver(stopReceiver, new IntentFilter(stop));
        PendingIntent broadcastIntent = PendingIntent.getBroadcast(
                this, 0, new Intent(stop), PendingIntent.FLAG_UPDATE_CURRENT);
        // Create the persistent notification
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
                .setContentTitle(getString(R.string.app_name))
                .setContentText(getString(R.string.notification_text))
                .setOngoing(true)
                .setContentIntent(broadcastIntent)
                .setSmallIcon(R.drawable.ic_tracker);
        startForeground(1, builder.build());
    }

    protected BroadcastReceiver stopReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d(TAG, "received stop broadcast");
            // Stop the service when the notification is tapped
            unregisterReceiver(stopReceiver);
            stopSelf();
        }
    };

    private void loginToFirebase() {
        // Functionality coming next step
    }

    private void requestLocationUpdates() {
        // Functionality coming next step
    }

}

Above is the initial structure of TrackerService, with some empty methods that we'll fill in next, and all the imports we'll eventually need. You'll see some errors show up due to some missing resources, let's fix those next.

When creating the notification, you need to specify an icon. To create the icon:

  1. Choose File > New > Image Asset
  2. Specify Icon Type: Notification Icons
  3. Specify Name: ic_tracker
  4. Specify Asset Type: Clip Art
  5. Click the icon next to the Clip Art label and choose an icon (for example, search for Bus)
  6. Click Next and Finish

We'll also add the strings we need for the app, such as the text for the notification, the email and password you created earlier, as well as an ID for the particular device that is used to uniquely identify it, for example the licence plate of the vehicle being tracked.

app/res/values/strings.xml

<resources>
    <string name="app_name">Transport Tracker</string>
    <string name="notification_text">Tracking, tap to cancel</string>
    <string name="transport_id" translatable="false">123</string>
    <string name="firebase_path" translatable="false">locations</string>
    <string name="firebase_email" translatable="false">test@example.com</string>
    <string name="firebase_password" translatable="false">password</string>
</resources>

In the above code, replace the firebase_email and firebase_password values with the email and password for the Firebase user that you created earlier.

We'll also replace the contents of AndroidManifest.xml with the following XML code, to include the required permissions the application needs:

app/manifests/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.transporttracker">

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
        <activity android:name=".TrackerActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <service
                android:name=".TrackerService"
                android:enabled="true"
                android:exported="true">
        </service>
    </application>

</manifest>

With this in place, let's go back to TrackerActivity and have it check that GPS is enabled and that location permissions have been granted (requesting them if they haven't), then start our service. Once the service is started, the activity can then shut down (via the finish() method) since it's no longer needed. Replace the contents of TrackerActivity.java with this code:

TrackerActivity.java

package com.example.transporttracker;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.widget.Toast;

public class TrackerActivity extends Activity {

    private static final int PERMISSIONS_REQUEST = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Check GPS is enabled
        LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);
        if (!lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            Toast.makeText(this, "Please enable location services", Toast.LENGTH_SHORT).show();
            finish();
        }

        // Check location permission is granted - if it is, start
        // the service, otherwise request the permission
        int permission = ContextCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_FINE_LOCATION);
        if (permission == PackageManager.PERMISSION_GRANTED) {
            startTrackerService();
        } else {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
                    PERMISSIONS_REQUEST);
        }
    }

    private void startTrackerService() {
        startService(new Intent(this, TrackerService.class));
        finish();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[]
            grantResults) {
        if (requestCode == PERMISSIONS_REQUEST && grantResults.length == 1
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // Start the service when the permission is granted
            startTrackerService();
        } else {
            finish();
        }
    }
}

Now you can run the application, and see the persistent notification displayed, which when tapped should shut down the application entirely. You'll also be asked to grant the app permission to request location updates when its run.

Awesome. Next we'll go back to TrackerService and look at the loginToFirebase() and requestLocationUpdates() methods.

In TrackerService.java, let's look at the loginToFirebase() and requestLocationUpdates() methods.

Update loginToFirebase() with the following code, to use the email and password you created earlier to authenticate the application to Firebase, so that once we start tracking the device's location, we can store it in Firebase.

TrackerService.java

    private void loginToFirebase() {
        // Authenticate with Firebase, and request location updates
        String email = getString(R.string.firebase_email);
        String password = getString(R.string.firebase_password);
        FirebaseAuth.getInstance().signInWithEmailAndPassword(
                email, password).addOnCompleteListener(new OnCompleteListener<AuthResult>(){
            @Override
            public void onComplete(Task<AuthResult> task) {
                if (task.isSuccessful()) {
                    Log.d(TAG, "firebase auth success");
                    requestLocationUpdates();
                } else {
                    Log.d(TAG, "firebase auth failed");
                }
            }
        });
    }

When we successfully log in to Firebase, we then initiate the request to track the device's location, via the requestLocationUpdates() method - let's fill that out next.

TrackerService.java

    private void requestLocationUpdates() {
        LocationRequest request = new LocationRequest();
        request.setInterval(10000);
        request.setFastestInterval(5000);
        request.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
        FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(this);
        final String path = getString(R.string.firebase_path) + "/" + getString(R.string.transport_id);
        int permission = ContextCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_FINE_LOCATION);
        if (permission == PackageManager.PERMISSION_GRANTED) {
            // Request location updates and when an update is 
            // received, store the location in Firebase
            client.requestLocationUpdates(request, new LocationCallback() {
                @Override
                public void onLocationResult(LocationResult locationResult) {
                    DatabaseReference ref = FirebaseDatabase.getInstance().getReference(path);
                    Location location = locationResult.getLastLocation();
                    if (location != null) {
                        Log.d(TAG, "location update " + location);
                        ref.setValue(location);
                    }
                }
            }, null);
        }
    }

Here we create a request to receive location updates with FusedLocationProvider, and upon receiving a new location, we store it in Firebase.

Now when we run the application, we can take a look at the Realtime database through the Firebase console, and we should see our current location being stored:

  1. Go to the Firebase Realtime Database in your web browser
  2. Go to your project
  3. Click on Database in the left hand navigation

If you're running the app on an emulator rather than a real device, you may need to use the emulator's extended controls to simulate the GPS receiving a location update.

  1. Choose the ellipsis button at the end of the right-hand toolbar
  2. Choose Location
  3. Press Send

With the Tracker application built, we'll now move onto the Display application which can be used to view all of the instances of the running Tracker application that represent devices being tracked.

First let's set up the Display application.

Start another new Android Studio project:

  1. Project Name: Transport Display
  2. Company Doman: example.com
  3. Choose Google Maps Activity, name it DisplayActivity
  4. Open google_maps_api.xml and copy the link (around line 7) into your browser, which will open the Google API Console
  5. Choose Create a project
  6. Select your Firebase project
  7. Click Continue and wait for the API to be enabled
  8. Choose Create API key, and copy the new API key to your clipboard
  9. Choose Restrict Key > Save (scroll to the bottom of the page)
  10. Back in Android Studio, paste the API key you just copied into google_maps_api.xml replacing the string YOUR_KEY_HERE (around line 24; scroll right, if necessary)

Connect your app to Firebase using the Firebase Assistant:

(Note that these steps are the same as the setup for the Tracker app.)

  1. In Android Studio, click Tools > Firebase to open the Assistant window
  2. Click to expand one of the listed features (for example, Authentication), then click the provided tutorial link (for example, Email and password authentication
  3. Click Connect to Firebase
  4. Select Choose an existing Firebase or Google project, then select the project you previously created
  5. Click Connect to Firebase (allowing some time for the connection to be made), and wait for the messages in the Android Studio status bar to complete
  6. Click Add Firebase Authentication to your app
  7. Click Accept Changes
  8. Click the back arrow at the top of the Assistant
  9. Click Realtime Database
  10. Click Save and retrieve data
  11. Click Add the Realtime Database to your app
  12. Click Accept Changes
  13. For more information, see the documentation on the Firebase Assistant

You can close the Firebase Assistant for now.

As was the case with the Tracker application, we also finally need to ensure the version numbers for each of the Google Play Services (including Firebase) are the same. Like before, open the build.gradle file (under Gradle scripts with the label Module:app), and update the version numbers for the following lines to ensure they're consistent:

build.gradle

    implementation 'com.google.android.gms:play-services-maps:11.2.0'
    implementation 'com.google.firebase:firebase-auth:11.2.0'
    implementation 'com.google.firebase:firebase-database:11.2.0'

Lastly, click the Sync Now link shown above the top of build.gradle.

That's the Display application set up with a Google Map as the main activity, and Firebase configured. You should be able to build the application, which just displays the default Google map:

The Display application will display a map with markers corresponding to each of the devices being tracked. It will subscribe to the Firebase Realtime Database, so that it is notified each time one of the device's location is updated. When this happens, it will either create a new marker at the device's location, or move the marker for a device if it exists already. We'll also handle moving the map's camera (the visible part of the map) each time a location changes, so that all of the markers are always visible.

Before we get into the code though, let's set up strings.xml, including the Firebase email and password you created earlier and used for the Tracker application.

app/res/values/strings.xml

<resources>
    <string name="app_name">Transport Display</string>
    <string name="title_activity_display">Transport Display</string>
    <string name="firebase_path" translatable="false">locations</string>
    <string name="firebase_email" translatable="false">test@example.com</string>
    <string name="firebase_password" translatable="false">password</string>
</resources>

As with the Tracker application, in the above code you will need to replace the firebase_email and firebase_password values with the email and password for the Firebase user that you created.

Now onto the code! Copy the following code into DisplayActivity.java

DisplayActivity.java

package com.example.transportdisplay;

import android.support.v4.app.FragmentActivity;
import android.os.Bundle;
import android.util.Log;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
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 java.util.HashMap;

public class DisplayActivity extends FragmentActivity implements OnMapReadyCallback {

    private static final String TAG = DisplayActivity.class.getSimpleName();
    private HashMap<String, Marker> mMarkers = new HashMap<>();
    private GoogleMap mMap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_display);
        SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
                .findFragmentById(R.id.map);
        mapFragment.getMapAsync(this);
    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        // Authenticate with Firebase when the Google map is loaded
        mMap = googleMap;
        mMap.setMaxZoomPreference(16);
        loginToFirebase();
    }

    private void loginToFirebase() {
        String email = getString(R.string.firebase_email);
        String password = getString(R.string.firebase_password);
        // Authenticate with Firebase and subscribe to updates
        FirebaseAuth.getInstance().signInWithEmailAndPassword(
                email, password).addOnCompleteListener(new OnCompleteListener<AuthResult>() {
            @Override
            public void onComplete(Task<AuthResult> task) {
                if (task.isSuccessful()) {
                    subscribeToUpdates();
                    Log.d(TAG, "firebase auth success");
                } else {
                    Log.d(TAG, "firebase auth failed");
                }
            }
        });
    }

    private void subscribeToUpdates() {
        // Functionality coming next step
    }

    private void setMarker(DataSnapshot dataSnapshot) {
        // Functionality coming next step
    }

}

That's the initial structure of the map activity, with a loginToFirebase() method implemented, very much like the Tracker application from earlier. We've also got two empty methods - subscribeToUpdates() and setMarker() which are responsible for subscribing to updates in Firebase and setting the marker on the map when an update occurs. Let's fill those out next.

DisplayActivity.java

    private void subscribeToUpdates() {
        DatabaseReference ref = FirebaseDatabase.getInstance().getReference(getString(R.string.firebase_path));
        ref.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
                setMarker(dataSnapshot);
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
                setMarker(dataSnapshot);
            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {
            }

            @Override
            public void onCancelled(DatabaseError error) {
                Log.d(TAG, "Failed to read value.", error.toException());
            }
        });
    }

    private void setMarker(DataSnapshot dataSnapshot) {
        // When a location update is received, put or update
        // its value in mMarkers, which contains all the markers
        // for locations received, so that we can build the 
        // boundaries required to show them all on the map at once
        String key = dataSnapshot.getKey();
        HashMap<String, Object> value = (HashMap<String, Object>) dataSnapshot.getValue();
        double lat = Double.parseDouble(value.get("latitude").toString());
        double lng = Double.parseDouble(value.get("longitude").toString());
        LatLng location = new LatLng(lat, lng);
        if (!mMarkers.containsKey(key)) {
            mMarkers.put(key, mMap.addMarker(new MarkerOptions().title(key).position(location)));
        } else {
            mMarkers.get(key).setPosition(location);
        }
        LatLngBounds.Builder builder = new LatLngBounds.Builder();
        for (Marker marker : mMarkers.values()) {
            builder.include(marker.getPosition());
        }
        mMap.animateCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), 300));
    }

The subscribeToUpdates() method calls setMarker() whenever it receives a new or updated location for a tracked device. setMarker() accepts the location data from Firebase, which contains the latitude and longitude, as well as the key for the device, as defined in the Tracker app's strings.xml earlier. We then keep a mapping of these keys to markers in the mMarkers variable, so that we can decide whether we need to create a new marker, or update the position of a previous one. Finally, setMarker() also creates a LatLngBounds variable using each of the marker locations, that we then use to animate the map's camera into a view that will show all of the markers at once.

Run your app! It should show a marker at the location your Tracker app reported.

Congratulations, you're all done!

If you're developing this sitting at your computer with one device, you won't see the full experience of multiple devices being tracked and moving around the map. To simulate this, you can access the Firebase Realtime Database in your web browser. This allows you to add and update entries in the database manually.

Go ahead and add a few simulated locations with the following steps:

  1. Go to the Firebase Realtime Database in your web browser
  2. Go to your project
  3. Click on Database in the left hand navigation
  4. Mouse over the locations entry and click the + sign
  5. Enter an identifier, such as 234
  6. Do not enter a value, but click the + sign for 234
  7. Click the + sign for 234 again, and enter the next value. Match/copy the name/value pairs of the first location, but change latitude and longitude by, say, 1.
  8. When all the fields are added, click ADD.

As you add new entries and change their values, you'll see the map updating on your device in realtime!