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.

  1. implementation ‘com.google.android.gms:play-services-location:15.0.1’
  2. implementation ‘com.google.android.gms:play-services-maps:15.0.1’
  3. 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.

  1. <uses-permission android:name=“android.permission.INTERNET” />
  2. <uses-permission android:name=“android.permission.ACCESS_COARSE_LOCATION” />
  3. <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.

  1. <meta-data
  2. android:name=“com.google.android.gms.version”
  3. android:value=“@integer/google_play_services_version” />
  4. <meta-data
  5. android:name=“com.google.android.geo.API_KEY”
  6. 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.

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  3. xmlns:tools=“http://schemas.android.com/tools”
  4. android:layout_width=“match_parent”
  5. android:layout_height=“match_parent”
  6. tools:context=“.MainActivity”>
  7. <FrameLayout
  8. android:id=“@+id/driverStatusLayout”
  9. android:layout_width=“match_parent”
  10. android:layout_height=“50dp”
  11. android:background=“@color/colorPrimary”
  12. android:orientation=“horizontal”>
  13. <TextView
  14. android:id=“@+id/driverStatusTextView”
  15. android:layout_width=“wrap_content”
  16. android:layout_height=“match_parent”
  17. android:layout_marginStart=“15dp”
  18. android:gravity=“center”
  19. android:text=“@string/offline”
  20. android:textColor=“@color/colorIcons”
  21. android:textSize=“22sp” />
  22. <android.support.v7.widget.SwitchCompat
  23. android:id=“@+id/driverStatusSwitch”
  24. android:layout_width=“wrap_content”
  25. android:layout_height=“match_parent”
  26. android:layout_gravity=“end”
  27. android:layout_marginEnd=“15dp”
  28. android:checked=“false”
  29. android:theme=“@style/SCBSwitch” />
  30. </FrameLayout>
  31. <fragment
  32. android:id=“@+id/supportMap”
  33. android:name=“com.google.android.gms.maps.SupportMapFragment”
  34. android:layout_width=“match_parent”
  35. android:layout_height=“match_parent”
  36. android:layout_below=“@+id/driverStatusLayout”
  37. tools:context=“spartons.com.frisbeeGo.fragments.MapFragment” />
  38. </RelativeLayout>

MainActivity

  1. class MainActivity : AppCompatActivity() {
  2. companion object {
  3. private const val MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 2200
  4. }
  5. private lateinit var googleMap: GoogleMap
  6. private lateinit var locationProviderClient: FusedLocationProviderClient
  7. private lateinit var locationRequest: LocationRequest
  8. private lateinit var locationCallback: LocationCallback
  9. private var locationFlag = true
  10. private var driverOnlineFlag = false
  11. private var currentPositionMarker: Marker? = null
  12. private val googleMapHelper = GoogleMapHelper()
  13. private val firebaseHelper = FirebaseHelper(“0000”)
  14. private val markerAnimationHelper = MarkerAnimationHelper()
  15. private val uiHelper = UiHelper()
  16. override fun onCreate(savedInstanceState: Bundle?) {
  17. super.onCreate(savedInstanceState)
  18. setContentView(R.layout.activity_main)
  19. val mapFragment: SupportMapFragment = supportFragmentManager.findFragmentById(R.id.supportMap) as SupportMapFragment
  20. mapFragment.getMapAsync { googleMap = it }
  21. createLocationCallback()
  22. locationProviderClient = LocationServices.getFusedLocationProviderClient(this)
  23. locationRequest = uiHelper.getLocationRequest()
  24. if (!uiHelper.isPlayServicesAvailable(this)) {
  25. Toast.makeText(this, “Play Services did not installed!”, Toast.LENGTH_SHORT).show()
  26. finish()
  27. } else requestLocationUpdate()
  28. val driverStatusTextView = findViewById<TextView>(R.id.driverStatusTextView)
  29. findViewById<SwitchCompat>(R.id.driverStatusSwitch).setOnCheckedChangeListener { _, b ->
  30. driverOnlineFlag = b
  31. if (driverOnlineFlag) driverStatusTextView.text = resources.getString(R.string.online_driver)
  32. else {
  33. driverStatusTextView.text = resources.getString(R.string.offline)
  34. firebaseHelper.deleteDriver()
  35. }
  36. }
  37. }
  38. @SuppressLint(“MissingPermission”)
  39. private fun requestLocationUpdate() {
  40. if (!uiHelper.isHaveLocationPermission(this)) {
  41. ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION)
  42. return
  43. }
  44. if (uiHelper.isLocationProviderEnabled(this))
  45. uiHelper.showPositiveDialogWithListener(this, resources.getString(R.string.need_location), resources.getString(R.string.location_content), object : IPositiveNegativeListener {
  46. override fun onPositive() {
  47. startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
  48. }
  49. }, “Turn On”, false)
  50. locationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
  51. }
  52. private fun createLocationCallback() {
  53. locationCallback = object : LocationCallback() {
  54. override fun onLocationResult(locationResult: LocationResult?) {
  55. super.onLocationResult(locationResult)
  56. if (locationResult!!.lastLocation == null) return
  57. val latLng = LatLng(locationResult.lastLocation.latitude, locationResult.lastLocation.longitude)
  58. Log.e(“Location”, latLng.latitude.toString() + ” , “ + latLng.longitude)
  59. if (locationFlag) {
  60. locationFlag = false
  61. animateCamera(latLng)
  62. }
  63. if (driverOnlineFlag) firebaseHelper.updateDriver(Driver(lat = latLng.latitude, lng = latLng.longitude))
  64. showOrAnimateMarker(latLng)
  65. }
  66. }
  67. }
  68. private fun showOrAnimateMarker(latLng: LatLng) {
  69. if (currentPositionMarker == null)
  70. currentPositionMarker = googleMap.addMarker(googleMapHelper.getDriverMarkerOptions(latLng))
  71. else markerAnimationHelper.animateMarkerToGB(currentPositionMarker!!, latLng, LatLngInterpolator.Spherical())
  72. }
  73. private fun animateCamera(latLng: LatLng) {
  74. val cameraUpdate = googleMapHelper.buildCameraUpdate(latLng)
  75. googleMap.animateCamera(cameraUpdate, 10, null)
  76. }
  77. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  78. super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  79. if (requestCode == MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION) {
  80. val value = grantResults[0]
  81. if (value == PERMISSION_DENIED) {
  82. Toast.makeText(this, “Location Permission denied”, Toast.LENGTH_SHORT).show()
  83. finish()
  84. } else if (value == PERMISSION_GRANTED) requestLocationUpdate()
  85. }
  86. }
  87. }

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

  1. class UiHelper {
  2. fun isPlayServicesAvailable(context: Context): Boolean {
  3. val googleApiAvailability = GoogleApiAvailability.getInstance()
  4. val status = googleApiAvailability.isGooglePlayServicesAvailable(context)
  5. return ConnectionResult.SUCCESS == status
  6. }
  7. fun isHaveLocationPermission(context: Context): Boolean {
  8. 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
  9. }
  10. fun isLocationProviderEnabled(context: Context): Boolean {
  11. val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
  12. return !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) && !locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
  13. }
  14. fun showPositiveDialogWithListener(callingClassContext: Context, title: String, content: String, positiveNegativeListener: IPositiveNegativeListener, positiveText: String, cancelable: Boolean) {
  15. buildDialog(callingClassContext, title, content)
  16. .builder
  17. .positiveText(positiveText)
  18. .positiveColor(getColor(R.color.colorPrimary, callingClassContext))
  19. .onPositive { _, _ -> positiveNegativeListener.onPositive() }
  20. .cancelable(cancelable)
  21. .show()
  22. }
  23. private fun buildDialog(callingClassContext: Context, title: String, content: String): MaterialDialog {
  24. return MaterialDialog.Builder(callingClassContext)
  25. .title(title)
  26. .content(content)
  27. .build()
  28. }
  29. private fun getColor(color: Int, context: Context): Int {
  30. return ContextCompat.getColor(context, color)
  31. }
  32. fun getLocationRequest() : LocationRequest {
  33. val locationRequest = LocationRequest.create()
  34. locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
  35. locationRequest.interval = 3000
  36. return locationRequest
  37. }
  38. }

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

  1. class GoogleMapHelper {
  2. companion object {
  3. private const val ZOOM_LEVEL = 18
  4. private const val TILT_LEVEL = 25
  5. }
  6. /**
  7. * @param latLng in which position to Zoom the camera.
  8. * @return the [CameraUpdate] with Zoom and Tilt level added with the given position.
  9. */
  10. fun buildCameraUpdate(latLng: LatLng): CameraUpdate {
  11. val cameraPosition = CameraPosition.Builder()
  12. .target(latLng)
  13. .tilt(TILT_LEVEL.toFloat())
  14. .zoom(ZOOM_LEVEL.toFloat())
  15. .build()
  16. return CameraUpdateFactory.newCameraPosition(cameraPosition)
  17. }
  18. /**
  19. * @param position where to draw the [com.google.android.gms.maps.model.Marker]
  20. * @return the [MarkerOptions] with given properties added to it.
  21. */
  22. fun getDriverMarkerOptions(position: LatLng): MarkerOptions {
  23. val options = getMarkerOptions(R.drawable.car_icon, position)
  24. options.flat(true)
  25. return options
  26. }
  27. private fun getMarkerOptions(resource: Int, position: LatLng): MarkerOptions {
  28. return MarkerOptions()
  29. .icon(BitmapDescriptorFactory.fromResource(resource))
  30. .position(position)
  31. }
  32. }

MarkerAnimationHelper

  1. class MarkerAnimationHelper {
  2. fun animateMarkerToGB(marker: Marker, finalPosition: LatLng, latLngInterpolator: LatLngInterpolator) {
  3. val startPosition = marker.position
  4. val handler = Handler()
  5. val start = SystemClock.uptimeMillis()
  6. val interpolator = AccelerateDecelerateInterpolator()
  7. val durationInMs = 2000f
  8. handler.post(object : Runnable {
  9. var elapsed: Long = 0
  10. var t: Float = 0.toFloat()
  11. var v: Float = 0.toFloat()
  12. override fun run() {
  13. // Calculate progress using interpolator
  14. elapsed = SystemClock.uptimeMillis() – start
  15. t = elapsed / durationInMs
  16. v = interpolator.getInterpolation(t)
  17. marker.position = latLngInterpolator.interpolate(v, startPosition, finalPosition)
  18. // Repeat till progress is complete.
  19. if (t < 1) {
  20. // Post again 16ms later.
  21. handler.postDelayed(this, 16)
  22. }
  23. }
  24. })
  25. }
  26. }

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

LatLngInterpolator

  1. interface LatLngInterpolator {
  2. fun interpolate(fraction: Float, a: LatLng, b: LatLng): LatLng
  3. class Spherical : LatLngInterpolator {
  4. override fun interpolate(fraction: Float, a: LatLng, b: LatLng): LatLng {
  5. // http://en.wikipedia.org/wiki/Slerp
  6. val fromLat = toRadians(a.latitude)
  7. val fromLng = toRadians(a.longitude)
  8. val toLat = toRadians(b.latitude)
  9. val toLng = toRadians(b.longitude)
  10. val cosFromLat = cos(fromLat)
  11. val cosToLat = cos(toLat)
  12. // Computes Spherical interpolation coefficients.
  13. val angle = computeAngleBetween(fromLat, fromLng, toLat, toLng)
  14. val sinAngle = sin(angle)
  15. if (sinAngle < 1E-6) {
  16. return a
  17. }
  18. val temp1 = sin((1 – fraction) * angle) / sinAngle
  19. val temp2 = sin(fraction * angle) / sinAngle
  20. // Converts from polar to vector and interpolate.
  21. val x = temp1 * cosFromLat * cos(fromLng) + temp2 * cosToLat * cos(toLng)
  22. val y = temp1 * cosFromLat * sin(fromLng) + temp2 * cosToLat * sin(toLng)
  23. val z = temp1 * sin(fromLat) + temp2 * sin(toLat)
  24. // Converts interpolated vector back to polar.
  25. val lat = atan2(z, sqrt(x * x + y * y))
  26. val lng = atan2(y, x)
  27. return LatLng(toDegrees(lat), toDegrees(lng))
  28. }
  29. private fun computeAngleBetween(fromLat: Double, fromLng: Double, toLat: Double, toLng: Double): Double {
  30. val dLat = fromLat – toLat
  31. val dLng = fromLng – toLng
  32. return 2 * asin(sqrt(pow(sin(dLat / 2), 2.0) + cos(fromLat) * cos(toLat) * pow(sin(dLng / 2), 2.0)))
  33. }
  34. }
  35. }

The LatLngInterpolator interface helps to animate the driver car Marker.

FirebaseHelper

  1. class FirebaseHelper constructor(driverId: String) {
  2. companion object {
  3. private const val ONLINE_DRIVERS = “online_drivers”
  4. }
  5. private val onlineDriverDatabaseReference: DatabaseReference = FirebaseDatabase
  6. .getInstance()
  7. .reference
  8. .child(ONLINE_DRIVERS)
  9. .child(driverId)
  10. init {
  11. onlineDriverDatabaseReference
  12. .onDisconnect()
  13. .removeValue()
  14. }
  15. fun updateDriver(driver: Driver) {
  16. onlineDriverDatabaseReference
  17. .setValue(driver)
  18. Log.e(“Driver Info”, ” Updated”)
  19. }
  20. fun deleteDriver() {
  21. onlineDriverDatabaseReference
  22. .removeValue()
  23. }
  24. }

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

  1. 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.

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