Realtime Location Sharing with Firebase and Android Background Service

  • 17th May, 2021
  • Kinjal D.
Share
  • LinkedIn-icon
  • WhatsApp-icon

Realtime Location Sharing with Firebase and Android Background Service

17th May, 2021 | Kinjal D.

  • Software Development
Live Location Tracker

What are You Going to Learn?

Build an application which stores the current location of the rider in firebase real-time database and display the live location of the rider to the customer on the map using Google Map SDK and Android background service.

Services in Android

  1. Background Service

A background service performs an operation that isn't directly noticed by the user. For example, if an app used a service to compact its storage, that would usually be a background service.

  1. Foreground Service

A foreground service performs some operation that is noticeable to the user. For example, an audio app would use a foreground service to play an audio track. Foreground services must display a Notification. Foreground services continue running even when the user isn't interacting with the app.

When you use a foreground service, you must display a notification so that users are actively aware that the service is running. This notification cannot be dismissed unless the service is either stopped or removed from the foreground.

Why to use the Foreground Service for Fetching the Current Location in Android?

Whenever an app runs in the background, it consumes some of the device's limited resources, like RAM. This can result in an impaired user experience, especially if the user is using a resource-intensive app, such as playing a game or watching a video. Android 8.0 (API level 26) imposes limitations on what apps can do while running in the background to improve the user experience. Hence to prevent the Service from being killed by the system we need to use foreground service.

Android Implementation

We will build this app using Android LocationManager API, Google Maps API, and Firebase Realtime Database. We will track the user's real-time location with Android’s LocationManager API and push the location update via Firebase realtime database. We will stream the location updates on Google Maps using Google Maps API.

Project setup

  1. Create a new firebase app.
  2. Create a new project in Google Maps Platform and generate a new key.
  3. Connect Android project to Firebase.
  4. Create a real-time database instance in Firebase and update security rules.
  5. Create Android App project (Rider/Customer).
  6. Add Google play service and Firebase database dependencies in app’s gradle file.
  7. Add Internet and Location permissions in AndroidManifest.xml ACCESS_COARSE_LOCATION ACCESS_FINE_LOCATION

Rider App

A rider app will broadcast the live location of the Rider.

1. Location Service

We will create a foreground service and initialize the firebase database and display the foreground notification. Then onStart command we will start location updates and save the lat/long to the firebase database.

class LocationService : Service() {
  
    private val CHANNEL_ID = "LocationService"
    var latitude = 0.0
    var longitude = 0.0
    lateinit var firebaseDatabase: FirebaseDatabase
    lateinit var databaseReference: DatabaseReference
  
    override fun onCreate() {
          super.onCreate()

          //To get instance of our firebase database
          firebaseDatabase = FirebaseDatabase.getInstance()

          //Location is the root node in the realtime database
          //To get reference of our database
          databaseReference = firebaseDatabase.getReference("Location")

          //Function to show notification with latitude and longitude of user
          showNotification(latitude, longitude)
      }

      override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

          //Function to get current location using LocationManager Api
          getCurrentLocation(this)

          //Function to show live location on map
          startMapActivity()

          return START_STICKY
      }

Note: Starting in Android 8.0 (API level 26), all notifications must be assigned to a channel.

private fun showNotification(latitude: Double, longitude: Double) {

        //It requires to create notification channel when android version is more than or equal to oreo
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel()
        }

        val notificationIntent = Intent(this, MapActivity::class.java)
       
        val pendingIntent = PendingIntent.getActivity(
            this,
            0, notificationIntent, 0
        )
  
        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("Location Service")
            .setContentText("Latitude = $latitude Longitude = $longitude")
            .setSmallIcon(R.drawable.ic_baseline_notifications_active_24)
            .setContentIntent(pendingIntent)
            .setOngoing(true)
            .build()
        startForeground(1, notification)
    }

    private fun createNotificationChannel() {
        val serviceChannel = NotificationChannel(
            CHANNEL_ID, "Location Service Channel",
            NotificationManager.IMPORTANCE_DEFAULT
        )
        val manager = getSystemService(NotificationManager::class.java)
        manager!!.createNotificationChannel(serviceChannel)

    }

You must need to register your service in AndroidManifest file and add the permission for foreground service for Android Pie.

<service
            android:name=".service.LocationService"
            android:enabled="true"
            android:exported="false" />

<!--
             The API key for Google Maps-based APIs is defined as a string resource.
             (See the file "res/values/google_maps_api.xml").
             Note that the API key is linked to the encryption key used to sign the APK.
             You need a different API key for each encryption key, including the release key that is used to
             sign the APK for publishing.
             You can define the keys for the debug and release targets in src/debug/ and src/release/.
        -->
<meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="GOOGLE_MAP_KEY" />

2. How to Get Live Location Updates?

Before requesting location updates you must need to check location permission.

private fun checkLocationPermission() {
  
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
            if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
                checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
            ) {
              
                val permissionList =
                    arrayOf(
                        Manifest.permission.ACCESS_FINE_LOCATION,
                        Manifest.permission.ACCESS_COARSE_LOCATION
                    )
                requestPermissions(
                    permissionList,
                    REQUEST_CODE_LOCATION_PERMISSION
                )
              
            } else {
              
                //This function checks gps is enabled or not in the device
                showGpsEnablePopup()
            
            }
        }
    }


override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
  
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_LOCATION_PERMISSION && grantResults.size >= 0) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
              
                //This function checks gps is enabled or not in the device
                showGpsEnablePopup()
              
            } else {
              
                Toast.makeText(this, "Permission Denied !", Toast.LENGTH_SHORT).show()
            
            }
        }
  
    }

We will use the best location provider such as GPS for live location, hence we need to make sure GPS is enabled in the user’s device. If GPS is not enabled, show a popup dialog to enable it.

private fun showGpsEnablePopup() {
  
        val locationRequest = LocationRequest.create()
        locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
  
        val builder = LocationSettingsRequest.Builder()
            .addLocationRequest(locationRequest)

        builder.setAlwaysShow(true) //this displays dialog box like Google Maps with two buttons - OK and NO,THANKS

        val task =
            LocationServices.getSettingsClient(this).checkLocationSettings(builder.build())

        task.addOnCompleteListener {
            try {
                val response = task.getResult(ApiException::class.java)
                // All location settings are satisfied. The client can initialize location
                // requests here.
                
                //Gps enabled
              
            } catch (exception: ApiException) {
                when (exception.statusCode) {
                    LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> // Location settings are not satisfied. But could be fixed by showing the
                        // user dialog.
                        try {
                            // Cast to a resolvable exception.
                            val resolvable = exception as ResolvableApiException
                            // Show the dialog by calling startResolutionForResult(),
                            // and check the result in onActivityResult().
                            resolvable.startResolutionForResult(
                                this,
                                REQUEST_CHECK_SETTINGS
                            )
                        } catch (e: SendIntentException) {
                            // Ignore the error.
                        }
                    LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE -> {
                    }
                }
            }
        }

    }

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CHECK_SETTINGS) {
            when (resultCode) {
                Activity.RESULT_OK -> {
                    //Gps enabled
                }
                Activity.RESULT_CANCELED -> {
                    Toast.makeText(this, "Gps is required, please turn it on", Toast.LENGTH_SHORT)
                        .show()
                }
            }
        }
    }

LocationManager

This class provides access to the system location services. These services allow applications to obtain periodic updates of the device's geographical location, or to be notified when the device enters the proximity of a given geographical location.

Criteria

Criteria class is used for selecting the best location provider. Providers may be chosen according to accuracy, power usage, ability to report altitude, speed, bearing, and monetary cost.

Request Location Update

locationManager.requestLocationUpdate (String provider, long minTime, float minDistance, LocationListener listener)

Parameters

provider: The name of the provider with which to register

minTime: Minimum time interval between location updates, in milliseconds

minDistance: Minimum distance between location updated, in meters

listener: A LocationListener whose OnLocationChanged method will be called for each location update

private fun getCurrentLocation(context: Context?) {
  
        //Check all location permission granted or not
        if (ActivityCompat.checkSelfPermission(
                context!!,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED
        ) {
          
            //Criteria class indicating the application criteria for selecting a location provider
            val criteria = Criteria()
            criteria.accuracy = Criteria.ACCURACY_FINE
            criteria.isSpeedRequired = true

            val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
            val provider = locationManager.getBestProvider(criteria, true)
          
            if (provider != null) {
                locationManager.requestLocationUpdates(
                    provider, 1, 0.1f, object : LocationListener {
                        override fun onLocationChanged(location: Location) {
                          
                            //Location changed
                          
                            latitude = location.latitude
                            longitude = location.longitude
                          
                            //Function to add data to firebase realtime database
                            addDataToDatabase()
                          
                            //Update notification with latest latitude and longitude
                            showNotification(latitude, longitude)
                           
                        }

                        override fun onProviderDisabled(provider: String) {
                            //Provider disabled
                        }

                        override fun onProviderEnabled(provider: String) {
                            //Provider enabled
                        }

                        override fun onStatusChanged(
                            provider: String?,
                            status: Int,
                            extras: Bundle?
                        ) {
                          
                        }

                    })
            }
        }
    }

3. How to Save Data to the Firebase Realtime Database?

private fun addDataToDatabase() {
        
        //setValue method is used to add value to RTFD
        databaseReference.setValue(
            CurrentLocation(latitude, longitude)
        )
    }

4. How to Start Service from Activity?

To start our location service we can use startService() or startForegroundService() methods from our Activity

fun startService(context: Context) {
            val startServiceIntent = Intent(context, LocationService::class.java)
          
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
                context.startForegroundService(startServiceIntent)
            } else {
                // Pre-O behavior.
                context.startService(startServiceIntent)
            }

        }

Similarly, we will use method stopService(Intent service) to stop the location service

fun stopService(context: Context) {
            val stopServiceIntent = Intent(context, LocationService::class.java)
            context.stopService(stopServiceIntent)
        }

Customer App

Now we will create a customer App which will display the live location of the Rider on the Map. onMapReady(GoogleMap) method is triggered when MapActivity opens which contains an instance of GoogleMap and a method to fetch updated latitude and longitude from the firebase realtime database.

class MapActivity : AppCompatActivity(), OnMapReadyCallback {

    private lateinit var mMap: GoogleMap
    lateinit var databaseReference: DatabaseReference
    var previousLatLng: LatLng? = null
    var currentLatLng: LatLng? = null
    private var polyline1: Polyline? = null

    private val polylinePoints: ArrayList<LatLng> = ArrayList()
    private var mCurrLocationMarker: Marker? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_map)
      
        // Obtain the SupportMapFragment and get notified when the map is ready to be used.
        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }

    /**
     * Manipulates the map once available.
     * This callback is triggered when the map is ready to be used.P
     * This is where we can add markers or lines, add listeners or move the camera. In this case,
     * we just add a marker near Sydney, Australia.
     * If Google Play services is not installed on the device, the user will be prompted to install
     * it inside the SupportMapFragment. This method will only be triggered once the user has
     * installed Google Play services and returned to the app.
     */
    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap

        //Function to draw line between list of consecutive points
        setPolylines()
      
        //Function to fetch location from firebase realtime database
        fetchUpdatedLocation()
      
    }
  
      private fun setPolylines() {
        val polylineOptions = PolylineOptions()
        polylineOptions.color(resources.getColor(R.color.design_default_color_primary))
        polylineOptions.geodesic(true)

        polyline1 = mMap.addPolyline(polylineOptions.addAll(polylinePoints))
    }

A polyline is a list of points, where line segments are drawn between consecutive points.

We will use it to draw the path of the rider.

private fun setPolylines() {
  
        val polylineOptions = PolylineOptions()
        polylineOptions.color(resources.getColor(R.color.design_default_color_primary))
        polylineOptions.geodesic(true)

        polyline1 = mMap.addPolyline(polylineOptions.addAll(polylinePoints))
  
    }

1. Receiving Location Updated from Firebase Database?

Whenever there is a change in the realtime database it will call onDataChange(dataSnapshot : DataSnapshot) of addValueEventListener(ValueEventListener) listener which is of type ValueEventListener.

private fun fetchUpdatedLocation() {
      
        databaseReference = FirebaseDatabase.getInstance().getReference("Location")
        databaseReference.addValueEventListener(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                updateMap(snapshot)
            }

            override fun onCancelled(error: DatabaseError) {

            }

        })
      
    }

2. Update Map with the Latest Current Location

After receiving a location update, we will update the marker on the Map.

private fun updateMap(dataSnapshot: DataSnapshot) {
        var latitude = 0.0
        var longitude = 0.0
        val data = dataSnapshot.childrenCount
  
        for (d in 0 until data) {
            latitude = dataSnapshot.child("latitude").getValue(Double::class.java)!!.toDouble()
            longitude = dataSnapshot.child("longitude").getValue(Double::class.java)!!.toDouble()
        }
  
        currentLatLng = LatLng(latitude, longitude)
  
        if (previousLatLng == null || previousLatLng !== currentLatLng) {
            // add marker line
            previousLatLng = currentLatLng
            polylinePoints.add(currentLatLng!!)
            polyline1!!.points = polylinePoints

            if (mCurrLocationMarker != null) {
                mCurrLocationMarker!!.position = currentLatLng!!
            } else {
                mCurrLocationMarker = mMap.addMarker(
                    MarkerOptions()
                        .position(currentLatLng!!)
                        .icon(bitmapFromVector(applicationContext, R.drawable.ic_bike))
                )
            }
            mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(currentLatLng!!, 16f))
        }
    }

3. How to Convert a Vector Image to a BitmapDescriptor?

We used the custom image as a marker icon in the Google Map and it is a vector image but the marker icon method needs BitmapDescriptor so the image needs to be converted to a BitmapDescriptor.

private fun bitmapFromVector(context: Context, vectorResId: Int): BitmapDescriptor? {
        // below line is use to generate a drawable.
        val vectorDrawable = ContextCompat.getDrawable(context, vectorResId)

        // below line is use to set bounds to our vector drawable.
        vectorDrawable!!.setBounds(
            0,
            0,
            vectorDrawable.intrinsicWidth,
            vectorDrawable.intrinsicHeight
        )

        // below line is use to create a bitmap for our
        // drawable which we have added.
        val bitmap = Bitmap.createBitmap(
            vectorDrawable.intrinsicWidth,
            vectorDrawable.intrinsicHeight,
            Bitmap.Config.ARGB_8888
        )

        // below line is use to add bitmap in our canvas.
        val canvas = Canvas(bitmap)

        // below line is use to draw our
        // vector drawable in canvas.
        vectorDrawable.draw(canvas)

        // after generating our bitmap we are returning our bitmap.
        return BitmapDescriptorFactory.fromBitmap(bitmap)
    }

Hurray !, it’s completed now. That’s how you can use the live location tracking feature in your android application using firebase, google play service, and service in android.

References:

1. Android Developers

2. Firebase Realtime

3. Database Google Play services

More blogs in "Software Development"

Redux
  • Software Development
  • 28th Jan, 2021
  • Farhan S.

Redux Approach in Android Using MutableLiveData

You must have heard about the word “Redux” while working with frontend developers or somewhere else, No ? Let me explain. Redux is a pattern and...
Keep Reading
Object Detection
  • Software Development
  • 3rd Jan, 2024
  • Arjun S.

Object Detection in 2024: The Ultimate Guide

Image Source: Object Detection Ever wondered how your smartphone recognizes faces in photos or how self-driving cars navigate through traffic seamlessly? The answer lies in a...
Keep Reading
Unit Tests
  • Software Development
  • 31st Mar, 2024
  • Dhanashree K.

Beginner’s Guide on Unit Tests: What, Why and How?

Have you ever made a small change in form validation, tested it, deployed it, and then broken the next few pages or end points of...
Keep Reading