- 26th Mar, 2021
- Jay R.
17th May, 2021 | Kinjal D.
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.
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.
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.
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.
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
A rider app will broadcast the live location of the Rider.
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" />
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?
) {
}
})
}
}
}
private fun addDataToDatabase() {
//setValue method is used to add value to RTFD
databaseReference.setValue(
CurrentLocation(latitude, longitude)
)
}
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)
}
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))
}
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) {
}
})
}
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))
}
}
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:
Get insights on the latest trends in technology and industry, delivered straight to your inbox.