According to the Oxford dictionary, geocaching refers to “an activity or pastime in which an item, or a container holding several items, is hidden at a particular location for GPS users to find using coordinates posted on the internet.”
For a geocaching application, we want the app to notify a user when they are within a specific radius of item A. Let’s say that the user, represented by a marker, has an item stored in a coordinate represented by another marker. In this case, the marker for the item is static, while the marker for the user is dynamic.
Using the Fused Location library in Android, we can build a geocaching application that provides a background service notification about the current user’s coordinates. The user will receive a notification if they are within a five-mile radius of the cache and will continue to be updated a distance calculation if they move closer to or further from the item.
At the end of the tutorial, our application will look like this:
To jump ahead:
The reader is required to have the Android Studio code editor and Kotlin on their specific device.
We will start by creating a Google MapFragment
. To do so, create a new Android Studio project. Select Google Maps Activity as your template and fill in our app name and package name. Doing that takes a lot of processes off the board because now we only need to get an API key from the Google Console:
Next, we will go to the Google developer’s console to get the API key.
Then, select Create Credentials and API key to create an API key:
Copy our newly created key, head to the AndroidManifest.xml
file, and paste it into the metadata tag attribute value
with the keyword API key:
After following the steps above, we only have a custom Google Map created automatically by Android Studio. In this section, we want to use the fusedlcation
API to get a continuous location update for the user, even after closing the application. We’ll achieve this via a background notification update.
To begin, head over to the module build.gradle
file and add the dependencies below:
implementation 'com.google.android.gms:play-services-location:20.0.0' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
Next, head back to the AndroidManifest.xml
file and set the following permissions, right above the applications tag:
<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" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
We do not want to open an application and automatically have location access (well, applications don’t work like that). Instead, we want the client to get a notification asking for access to some system settings.
We’ll build an interface file that abstracts the location updates within the root folder and call it ClientInfo.kt
. Within the interface, we’ll create a function with the parameter interval that specifies how often we want our location updated. The function will return a Flow
of type Location
from the coroutines library we added to the dependencies earlier.
We will also create a class to pass a message in case our GPS is turned off:
interface ClientInfo { fun getLocationUpdates(interval: Long): Flow<Location> class LocException(message: String): Exception() }
Now, we need to show the implementation of the ClientInfo
. So, within the same root folder, create a class file called DefaultClientInfo.kt
, which will implement the interface (ClientInfo
) we declared above. The class will then take two constructor parameters: Context
and FusedLocationProviderClient
.
Next, we will override the getLocationUpdates
function, and using the callbackFlow
instance, we’ll first check if the user has accepted the location permission. We’ll do this by creating a utility file in the same root folder called ExtendContext.kt
to write an extension function that returns a Boolean.
This function will check if the COARSE
and FINE_LOCATION
permissions are granted:
fun Context.locationPermission(): Boolean{ return ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_COARSE_LOCATION )== PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED }
If the user has allowed permission, we want to check if they can fetch their location (if the location is enabled) using the SytemService LocationManager
.
Now that we can fetch the user’s location, we need to create a request that will specify how often we want to fetch the user’s location and the accuracy of the data. Also, we will create a callback that will use the onLocationResult
function whenever the FusedLocationProviderClient fetches a new location.
Finally, we will use the fusedlocation.requestLocationUpdates
method to call the callback function, request, and a looper. Here is the implementation:
class DefaultClientInfo( private val context:Context, private val fusedlocation: FusedLocationProviderClient ):ClientInfo{ @SuppressLint("MissingPermission") override fun getLocationUpdates(interval: Long): Flow<Location> { return callbackFlow { if(!context.locationPermission()){ throw ClientInfo.LocException("Missing Permission") } val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager val hasGPS = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) val hasNetwork = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) if(!hasGPS && hasNetwork){ throw ClientInfo.LocException("GPS is unavailable") } val locationRequest = LocationRequest.create().apply { setInterval(interval) fastestInterval = interval priority = Priority.PRIORITY_HIGH_ACCURACY } val locationCallback = object : LocationCallback(){ override fun onLocationResult(result: LocationResult) { super.onLocationResult(result) result.locations.lastOrNull()?.let{ location -> launch { send(location) } } } } fusedlocation.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() ) awaitClose { fusedlocation.removeLocationUpdates(locationCallback) } } } }
To create a foreground service, we will create another class file in our root project called locservices.kt
and make it inherit from the service class. Using a coroutine, we will create a serviceScope
that is bound to the lifetime of the service, call the ClientInfo
abstraction that we created earlier, and a class that stores the coordinate information of our cache.
class LocServices: Service(){ private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private lateinit var clientInfo: ClientInfo }
Next, we will create an onBind
function that will return null
because we are not binding our service to anything. Then, we will use the onCreate
function to call the DefaultClientInfo
class, in which we will provide applicationContext
and
LocationServices.getFusedLocationProviderClient(applicationContext)
as the parameters.
class LocServices: Service(){ // do something override fun onBind(p0: Intent?): IBinder? { return null } override fun onCreate() { super.onCreate() clientInfo = DefaultClientInfo( applicationContext, LocationServices.getFusedLocationProviderClient(applicationContext) ) } }
Now, we will create a companion object
and, within it, create a constant value START
, which we send to the service when we want to start our tracking. Then we will call the onStartCommand()
function for services and provide the constant we created earlier as an intent
that we linked to a start()
function:
class LocServices: Service(){ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when(intent?.action){ START -> start() } return super.onStartCommand(intent, flags, startId) } @SuppressLint("NewApi") private fun start(){ } companion object{ const val START = "Start" } }
The start
function will handle the notification to alert the user that their location is actively being monitored. That means the information we want to provide the user is the distance (in meters) between them and the cache. To do that, we will use the Haversine formula, which computes the distance between two points on a sphere using their coordinates.
Thus, using our callbackflow
, we will call the clientInfo.getLocationUpdates(interval)
method, and using the onEach
method provided by coroutines
, we will be able to get the updated latitude and longitude.
As we said earlier, we want the user to know the distance between them and the cache, but there is a catch. We do not want the user to get a consistent flurry of notifications telling them the distance between them and the cache.
So, we will create a conditional statement that checks if the user is within a thousand-meter radius of the cache. If true, the user will get an ongoing notification informing them if they are getting further or closer to the cache. Once they get within a 50-meter radius, they are notified with a different message, and the service stops:
class LocServices: Service(){ @SuppressLint("NewApi") private fun start(){ val notif = NotificationCompat.Builder(this, "location") .setContentTitle("Geocaching") .setContentText("runnning in the background") .setSmallIcon(R.drawable.ic_launcher_background) .setOngoing(true) val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager clientInfo .getLocationUpdates(1000L) .catch { e -> e.printStackTrace() } .onEach { location -> val lat1 = location.latitude val long1 = location.longitude val radius = 6371 //in km val lat2 = secrets.d val long2 = secrets.d1 val dlat = Math.toRadians(lat2 - lat1) val dlong = Math.toRadians(long2 - long1) val a = sin(dlat / 2) * sin(dlong / 2) + cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) * sin(dlong / 2) * sin(dlong / 2) val c = 2 * asin(sqrt(a)) val valuresult = radius * c val km = valuresult / 1 val meter = km * 1000 val truemeter = String.format("%.2f", meter) if (meter > 100 && meter <= 1000){ val updatednotif = notif .setContentText("You are $truemeter meters away") notificationManager.notify(1, updatednotif.build()) } if (meter < 100){ val getendnotice = notif .setContentText("You are $truemeter meters away, continue with your search") .setOngoing(false) notificationManager.notify(1, getendnotice.build()) stopForeground(STOP_FOREGROUND_DETACH) stopSelf() } } .launchIn(serviceScope) startForeground(1, notif.build()) } }
Finally, we will create an onDestroy
function that cancels the service when we close the application or clear our system cache. Here is the implementation of the code below:
class LocServices: Service(){ override fun onDestroy() { super.onDestroy() serviceScope.cancel() } }
Now that we have the foreground service ready, we will return to the AndroidManifest.xml
file and the tag right above the metadata tag:
<service android:name=".fusedLocation.LocServices" android:foregroundServiceType = "location"/>
If we want to create a notification for our user’s distance to the cache, we need to create a channel to send notifications. Let’s first create a class called LocationApp.kt
that we will make an Application()
.
In the onCreate
function, we’ll create a notification channel from the Android oreo OS upwards. This is how the code looks:
class LocationApp: Application() { override fun onCreate() { super.onCreate() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( "location", "Location", NotificationManager.IMPORTANCE_LOW ) val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } } }
Finally, we will add the attribute to the application tag of the AndroidManifest.xml
file below:
android:name=".fusedLocation.LocationApp"
When we created a Google Maps Activity, we got a MapsActivity.kt
file rather than the regular MainActivity.kt
. This file handles the creation of a map with a marker. We need to make a few changes to that. So, let’s create three private lateinit
variables: LocationCallback
, LocationRequest
and FusedLocationProviderClient
.
Next, we will create three functions; launchintent
, getupdatedlocation
, and startupdate
. We will call them in the onMapReady
callback function.
The launchintent
function handles the location permission request, and the getupdatedlocation
function takes the LocationRequest
and LocationCallback
. The getupdatedlocation
function will also handle calling the start function using its intent.
Finally, within the startupdate
function, we will use the fusedlocation.requestLocationUpdates
method to call the callback function, request, and a looper (set to null).
Here is how the code looks:
class MapsActivity : AppCompatActivity(), OnMapReadyCallback{ companion object{ private var firsttime = true } private lateinit var mMap: GoogleMap private lateinit var binding: ActivityMapsBinding private lateinit var locationCallback: LocationCallback private lateinit var locationRequest: LocationRequest private lateinit var fusedLocationProviderClient: FusedLocationProviderClient private var mMarker: Marker? = null private var secrets = Secretlocation() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMapsBinding.inflate(layoutInflater) setContentView(binding.root) fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this) // 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) } private fun launchintent() { ActivityCompat.requestPermissions( this, arrayOf( Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION ), 0 ) } private fun getupdatedlocation(){ locationRequest = LocationRequest.create().apply { interval = 10000 fastestInterval = 5000 priority = Priority.PRIORITY_HIGH_ACCURACY } locationCallback = object : LocationCallback(){ override fun onLocationResult(result: LocationResult) { if (result.locations.isNotEmpty()){ val location = result.lastLocation if (location != null){ mMarker?.remove() val lat1 = location.latitude val long1 = location.longitude val d = secrets.d val d1 = secrets.d1 val latlong = LatLng(lat1, long1) val stuff = LatLng(d, d1) val stuffoption= MarkerOptions().position(stuff).title("$stuff").icon( BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE)) mMarker = mMap.addMarker(stuffoption) val markerOptions = MarkerOptions().position(latlong).title("$latlong") mMarker = mMap.addMarker(markerOptions) if (firsttime){ mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latlong, 17f )) firsttime = false } } } } } Intent(applicationContext, LocServices::class.java).apply { action = LocServices.START startService(this) } } @SuppressLint("MissingPermission") private fun startupdate(){ fusedLocationProviderClient.requestLocationUpdates( locationRequest, locationCallback, null ) } override fun onMapReady(googleMap: GoogleMap) { mMap = googleMap launchintent() getupdatedlocation() startupdate() mMap.uiSettings.isZoomControlsEnabled = true } }
When we run our application, we should have the result below:
In this tutorial, we created a map using Android’s Fused Location library, which continuously updates the location of a user on a map. We also created a foreground service determining the distance between the user and a specific item. Finally, we created a notification for whenever our user gets closer to the cache.
Thanks for reading, and happy coding!
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your Android apps — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowIt’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.