Car Location Tracking Android App With Firebase Tutorial

In this tutorial, we’re building a car location tracking app like Careem and Uber. Although we’re not developing complete apps like Uber and Careem. But we’ll see how to online a driver and show the online driver in the passenger app and update the driver whenever its current location changed.

For online and offline the driver we’re going to use the Firebase Real-time Database. The driver simply sends its current location to the firebase and passenger see the driver on the Google Map.

Below is the demo of applications which we gonna make in this article.

Prerequisite:

  1. You’ve to have a Google Map API key for showing the Map. See this link for getting the API key.
  2. Need a Firebase project for the real-time database. Click to this link for creating a Firebase project.

Now we’ve Google Map API key and a Firebase project let’s dive into coding and start building our mobile applications.

Android App Coding:

Like I said earlier, there are two apps one for driver and one for the passenger. So, we’re gonna start making with the driver app.

DriverApp:

First, add the below dependencies to your app level build.gradle file.

implementation 'com.google.android.gms:play-services-location:15.0.1'
implementation 'com.google.android.gms:play-services-maps:15.0.1'
implementation 'com.google.firebase:firebase-database:16.0.1'

After adding the dependencies you need to sync or build your project. Later the successful build adds the Internet and Location permission to Android. Manifest file.

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

Now, to show the Google Maps in the app we need to add the meta tags in Android. Manifest file.


<meta-data
    android:name="com.google.android.gms.version"
    android:value="@integer/google_play_services_version" />

<meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="@string/map_api_key" />   // Change it with your Google Maps API key.

All the prep work are done for showing the Google Maps and for reading the user current Location. Now, we need to add the google-services.json file. In this tutorial, I’m not gonna show you how to create a firebase project, register your Android app to that firebase project, and add the google-services.json file in Android app. There’s a link for a video to add the google-services.json file in Android app.

So, without further ado let’s dive into coding. Below is the UI of the driver app that we’re gonna make.

Driver Tracker App Main Screen

You see the UI is very basic, we’ve one SwitchCompat for online and offline the driver and below is the Google Map itself. Now let’s see the code of the above UI. Below is the activity_main.xml file.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/driverStatusLayout"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/colorPrimary"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/driverStatusTextView"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginStart="15dp"
            android:gravity="center"
            android:text="@string/offline"
            android:textColor="@color/colorIcons"
            android:textSize="22sp" />

        <android.support.v7.widget.SwitchCompat
            android:id="@+id/driverStatusSwitch"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="end"
            android:layout_marginEnd="15dp"
            android:checked="false"
            android:theme="@style/SCBSwitch" />

    </FrameLayout>

    <fragment
        android:id="@+id/supportMap"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/driverStatusLayout"
        tools:context="spartons.com.frisbeeGo.fragments.MapFragment" />

</RelativeLayout>

MainActivity

class MainActivity : AppCompatActivity() {

    companion object {
        private const val MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 2200
    }

    private lateinit var googleMap: GoogleMap
    private lateinit var locationProviderClient: FusedLocationProviderClient
    private lateinit var locationRequest: LocationRequest
    private lateinit var locationCallback: LocationCallback
    private var locationFlag = true
    private var driverOnlineFlag = false
    private var currentPositionMarker: Marker? = null
    private val googleMapHelper = GoogleMapHelper()
    private val firebaseHelper = FirebaseHelper("0000")
    private val markerAnimationHelper = MarkerAnimationHelper()
    private val uiHelper = UiHelper()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val mapFragment: SupportMapFragment = supportFragmentManager.findFragmentById(R.id.supportMap) as SupportMapFragment
        mapFragment.getMapAsync { googleMap = it }
        createLocationCallback()
        locationProviderClient = LocationServices.getFusedLocationProviderClient(this)
        locationRequest = uiHelper.getLocationRequest()
        if (!uiHelper.isPlayServicesAvailable(this)) {
            Toast.makeText(this, "Play Services did not installed!", Toast.LENGTH_SHORT).show()
            finish()
        } else requestLocationUpdate()
        val driverStatusTextView = findViewById<TextView>(R.id.driverStatusTextView)
        findViewById<SwitchCompat>(R.id.driverStatusSwitch).setOnCheckedChangeListener { _, b ->
            driverOnlineFlag = b
            if (driverOnlineFlag) driverStatusTextView.text = resources.getString(R.string.online_driver)
            else {
                driverStatusTextView.text = resources.getString(R.string.offline)
                firebaseHelper.deleteDriver()
            }
        }
    }

    @SuppressLint("MissingPermission")
    private fun requestLocationUpdate() {
        if (!uiHelper.isHaveLocationPermission(this)) {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION)
            return
        }
        if (uiHelper.isLocationProviderEnabled(this))
            uiHelper.showPositiveDialogWithListener(this, resources.getString(R.string.need_location), resources.getString(R.string.location_content), object : IPositiveNegativeListener {
                override fun onPositive() {
                    startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
                }
            }, "Turn On", false)
        locationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
    }

    private fun createLocationCallback() {
        locationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult?) {
                super.onLocationResult(locationResult)
                if (locationResult!!.lastLocation == null) return
                val latLng = LatLng(locationResult.lastLocation.latitude, locationResult.lastLocation.longitude)
                Log.e("Location", latLng.latitude.toString() + " , " + latLng.longitude)
                if (locationFlag) {
                    locationFlag = false
                    animateCamera(latLng)
                }
                if (driverOnlineFlag) firebaseHelper.updateDriver(Driver(lat = latLng.latitude, lng = latLng.longitude))
                showOrAnimateMarker(latLng)
            }
        }
    }

    private fun showOrAnimateMarker(latLng: LatLng) {
        if (currentPositionMarker == null)
            currentPositionMarker = googleMap.addMarker(googleMapHelper.getDriverMarkerOptions(latLng))
        else markerAnimationHelper.animateMarkerToGB(currentPositionMarker!!, latLng, LatLngInterpolator.Spherical())
    }

    private fun animateCamera(latLng: LatLng) {
        val cameraUpdate = googleMapHelper.buildCameraUpdate(latLng)
        googleMap.animateCamera(cameraUpdate, 10, null)
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION) {
            val value = grantResults[0]
            if (value == PERMISSION_DENIED) {
                Toast.makeText(this, "Location Permission denied", Toast.LENGTH_SHORT).show()
                finish()
            } else if (value == PERMISSION_GRANTED) requestLocationUpdate()
        }
    }
}

In MainActivity there are a bunch of important points which I’m going to explain below.

  1.  createLocationCallback: We’re calling this function from the onCreate method of our MainActivity. In the LocationCallback abstract method, we’ll get the user current location, update the Driver info on Firebase Real-time database if the driver is online, and animate driver car Marker from the previous Location to a new one.
  2. requestLocationUpdates: Calling this function from the onCreate method of MainActivity if the user has installed GooglePlayServices in his/her mobile. In this method first, we request the Location permission from the user, then we check if the Location provider is enabled, and finally start the location updates.
  3. showOrAnimateMarker: In this method first we check if the driver car Marker is null then we simply add a new Marker to Google Maps else we animate the Marker.
  4. animteCamera: This method is called from createLocationCallback method only once because, we only want to animate the Google Maps to driver current location when he/she opens the app.
  5. onRequestPermissionsResult: Callback of the result of requesting location permission. This method invoked when the user performs an action on permission. Now in this method, if the user denies the permission then we simply show the toast else start the current location updates.

UiHelper

class UiHelper {

    fun isPlayServicesAvailable(context: Context): Boolean {
        val googleApiAvailability = GoogleApiAvailability.getInstance()
        val status = googleApiAvailability.isGooglePlayServicesAvailable(context)
        return ConnectionResult.SUCCESS == status
    }

    fun isHaveLocationPermission(context: Context): Boolean {
        return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
    }

    fun isLocationProviderEnabled(context: Context): Boolean {
        val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
        return !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) && !locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
    }

    fun showPositiveDialogWithListener(callingClassContext: Context, title: String, content: String, positiveNegativeListener: IPositiveNegativeListener, positiveText: String, cancelable: Boolean) {
        buildDialog(callingClassContext, title, content)
                .builder
                .positiveText(positiveText)
                .positiveColor(getColor(R.color.colorPrimary, callingClassContext))
                .onPositive { _, _ -> positiveNegativeListener.onPositive() }
                .cancelable(cancelable)
                .show()
    }

    private fun buildDialog(callingClassContext: Context, title: String, content: String): MaterialDialog {
        return MaterialDialog.Builder(callingClassContext)
                .title(title)
                .content(content)
                .build()
    }


    private fun getColor(color: Int, context: Context): Int {
        return ContextCompat.getColor(context, color)
    }

    fun getLocationRequest() : LocationRequest {
        val locationRequest = LocationRequest.create()
        locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        locationRequest.interval = 3000
        return locationRequest
    }
}

The following explains UiHelper class methods.

  1. isPlayServicesAvailable: This method checks that if the user currently has Google Play Services installed or not.
  2. isHaveLocationPermission: Checks that if a user gives the Location permission or not.
  3. isLocationProviderEnabled: Checks if the Location provider enabled or not. If not then open the Settings activity and turn on the Location Provider.
  4. showPositiveDialogWithListener: Utility function to show the Dialog when the user has not enabled the location provider in mobile settings.

GoogleMapHelper

class GoogleMapHelper {

    companion object {
        private const val ZOOM_LEVEL = 18
        private const val TILT_LEVEL = 25
    }

    /**
     * @param latLng in which position to Zoom the camera.
     * @return the [CameraUpdate] with Zoom and Tilt level added with the given position.
     */

    fun buildCameraUpdate(latLng: LatLng): CameraUpdate {
        val cameraPosition = CameraPosition.Builder()
                .target(latLng)
                .tilt(TILT_LEVEL.toFloat())
                .zoom(ZOOM_LEVEL.toFloat())
                .build()
        return CameraUpdateFactory.newCameraPosition(cameraPosition)
    }

    /**
     * @param position where to draw the [com.google.android.gms.maps.model.Marker]
     * @return the [MarkerOptions] with given properties added to it.
     */

    fun getDriverMarkerOptions(position: LatLng): MarkerOptions {
        val options = getMarkerOptions(R.drawable.car_icon, position)
        options.flat(true)
        return options
    }

    private fun getMarkerOptions(resource: Int, position: LatLng): MarkerOptions {
        return MarkerOptions()
                .icon(BitmapDescriptorFactory.fromResource(resource))
                .position(position)
    }
}

MarkerAnimationHelper

class MarkerAnimationHelper {

    fun animateMarkerToGB(marker: Marker, finalPosition: LatLng, latLngInterpolator: LatLngInterpolator) {
        val startPosition = marker.position
        val handler = Handler()
        val start = SystemClock.uptimeMillis()
        val interpolator = AccelerateDecelerateInterpolator()
        val durationInMs = 2000f
        handler.post(object : Runnable {
            var elapsed: Long = 0
            var t: Float = 0.toFloat()
            var v: Float = 0.toFloat()
            override fun run() {
                // Calculate progress using interpolator
                elapsed = SystemClock.uptimeMillis() - start
                t = elapsed / durationInMs
                v = interpolator.getInterpolation(t)
                marker.position = latLngInterpolator.interpolate(v, startPosition, finalPosition)
                // Repeat till progress is complete.
                if (t < 1) {
                    // Post again 16ms later.
                    handler.postDelayed(this, 16)
                }
            }
        })
    }
}

The above MarkerAnimationHelper class animate the driver car Marker from the previous location to user new Location.

LatLngInterpolator

interface LatLngInterpolator {

    fun interpolate(fraction: Float, a: LatLng, b: LatLng): LatLng

    class Spherical : LatLngInterpolator {

        override fun interpolate(fraction: Float, a: LatLng, b: LatLng): LatLng {
            // http://en.wikipedia.org/wiki/Slerp
            val fromLat = toRadians(a.latitude)
            val fromLng = toRadians(a.longitude)
            val toLat = toRadians(b.latitude)
            val toLng = toRadians(b.longitude)
            val cosFromLat = cos(fromLat)
            val cosToLat = cos(toLat)

            // Computes Spherical interpolation coefficients.
            val angle = computeAngleBetween(fromLat, fromLng, toLat, toLng)
            val sinAngle = sin(angle)
            if (sinAngle < 1E-6) {
                return a
            }
            val temp1 = sin((1 - fraction) * angle) / sinAngle
            val temp2 = sin(fraction * angle) / sinAngle

            // Converts from polar to vector and interpolate.
            val x = temp1 * cosFromLat * cos(fromLng) + temp2 * cosToLat * cos(toLng)
            val y = temp1 * cosFromLat * sin(fromLng) + temp2 * cosToLat * sin(toLng)
            val z = temp1 * sin(fromLat) + temp2 * sin(toLat)

            // Converts interpolated vector back to polar.
            val lat = atan2(z, sqrt(x * x + y * y))
            val lng = atan2(y, x)
            return LatLng(toDegrees(lat), toDegrees(lng))
        }

        private fun computeAngleBetween(fromLat: Double, fromLng: Double, toLat: Double, toLng: Double): Double {
            val dLat = fromLat - toLat
            val dLng = fromLng - toLng
            return 2 * asin(sqrt(pow(sin(dLat / 2), 2.0) + cos(fromLat) * cos(toLat) * pow(sin(dLng / 2), 2.0)))
        }
    }
}

The LatLngInterpolator interface helps to animate the driver car Marker.

FirebaseHelper

class FirebaseHelper constructor(driverId: String) {

    companion object {
        private const val ONLINE_DRIVERS = "online_drivers"
    }

    private val onlineDriverDatabaseReference: DatabaseReference = FirebaseDatabase
            .getInstance()
            .reference
            .child(ONLINE_DRIVERS)
            .child(driverId)

    init {
        onlineDriverDatabaseReference
                .onDisconnect()
                .removeValue()
    }

    fun updateDriver(driver: Driver) {
        onlineDriverDatabaseReference
                .setValue(driver)
        Log.e("Driver Info", " Updated")
    }

    fun deleteDriver() {
        onlineDriverDatabaseReference
                .removeValue()
    }
}

Before going to start to explain FirebaseHelper, I want to show you Firebase Real-time Database Structure.

Firebase Realtime Database

The following explains about FirebaseHelper class.

  1. onlineDriverDatabaseReference: When creating a DatabaseReference, we’re adding two children one for online drivers node and another one for the Driver itself. We need to inform firebase real-time database in which node to update the Diver that’s why I’m setting driverId as a top node and below the is the complete Driver object. The driverId must be unique because it differs in whole online drivers node which specific driver node to update.
  2. updateDriver: Update the Driver new Location to firebase real-time database.
  3. deleteDriver: Removes the driver node from firebase real-time database.

Driver

data class Driver(val lat: Double, val lng: Double, val driverId: String = "0000")

You can change driverId with the user primary key any anything which is unique.

Protip: The approach we’ve used to show drivers online and offline, the same procedure we can use it when developing Social Media apps, showing last online status, checking if a user is online or not, etc.

Alright, guys, this was all from DriverApp. I’m going to end this article here if you want to see the Passenger app see the next article. If you’ve any queries regarding this post please do comment below.

Download Complete Code

Thank you for being here and keep reading…

Next Part

9 COMMENTS

LEAVE A REPLY

Please enter your comment!
Please enter your name here