According to the Oxford Dictionary, geocaching is "an activity or pastime in which an item or container containing several items is hidden in a specific location for a GPS user to find using coordinates published on the Internet.
For a geocaching application, we want the application to notify the user when the user is within a certain radius of item A. Suppose the user (represented by a token) stores an item at the coordinates represented by another token. How to reset network settings in Windows 11? It only takes 6 easy steps. In this case, the item's tag is static, while the user's tag is dynamic.
Using the Fused Location library in Android, we can build a geocaching application that provides background service notifications about the current user's coordinates. How to turn off Search the Web results in Windows 11 ? Disabling the search window tutorial will continue to update the distance calculation.
prerequisites
Reader requires the Android Studio code editor and Kotlin to be installed on its specific device.
start
We'll start by creating a Google. To do this, create a new Android Studio project. Choose Google Maps Activity as a template and fill in our app name and package name. Doing this would be a lot of work because now we just need to get an API key from the Google console: MapFragment
Next, we'll go to the Google developer's console to get an API key.
Then, select Create Credentials and API Key to create an API key:
Copy our newly created key, go to the file, and paste it in the metadata tag attribute with the keyword API key: AndroidManifest.xml``value
create function
After following the steps above, we only have a custom Google Map automatically created by Android Studio. In this section, we want to use the API to get continuous location updates for the user, even after closing the app. We will do this through background notification updates. fused lcation
First, go to the module file and add the following dependencies: build.gradle
implementation 'com.google.android.gms:play-services-location:20.0.0' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
Next, go back to the file and set the following permissions right above the application tag: AndroidManifest.xml
<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" />
Create abstractions and permission settings
We don't want to open the app and automatically have location access (well, apps don't work that way). Instead, we want the client to be notified asking to access certain system settings.
We'll build an interface file that abstracts location updates in the root folder and call it. In the interface, we'll create a function with an argument interval that specifies how often we want the position to be updated. This function will return the of type from the coroutine library we added to our dependencies earlier. ClientInfo.kt Flow
Location
We'll 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. So, in the same root folder, create a class file named class that will implement the interface we declared above() . The class will then take two constructor parameters: and . ClientInfo DefaultClientInfo.kt
ClientInfo Context
FusedLocationProviderClient
Next, we'll override that function, and using the callbackFlow instance, we first check if the user has accepted the location permission. To do this, we'll create a utility file in the same root folder that calls to write extension functions that return boolean values. getLocationUpdates``ExtendContext.kt
This function will check if and permission is granted: COARSE``FINE_LOCATION
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 permission, we want to check if they can use the .SytemService LocationManager
Now that we can get the user's location, we need to create a request that will specify how often we get the user's location and the accuracy of the data. Additionally, we'll create a callback that will use this function whenever the FusedLocationProviderClient gets a new location . onLocationResult
Finally, we'll use this method to call callback functions, requests, and loopers. Here is the implementation: fusedlocation.requestLocationUpdates
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) } } } }
Create a foreground service
In order to create the foreground service, we will create another class file in the root project called and make it inherit from the service class. Using coroutines, we'll create a class that binds to the service lifetime, calls the abstraction we created earlier, and a class that stores cached coordinate information. locservices.kt serviceScope
ClientInfo
class LocServices: Service(){ private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private lateinit var clientInfo: ClientInfo }
Next, we'll create a function that will return, since we haven't bound our service to anything. We'll then use that function to call the class, where we'll provide and as parameters. onBind null
onCreate DefaultClientInfo
applicationContext ``LocationServices.getFusedLocationProviderClient( applicationContext )
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're going to create one, and inside of it, a constant value that we'll send to the service when we want to start tracking. We will then call the function for the service, providing the constant we created earlier as the constant we link to the function: companion object START
onStartCommand() intent
start()
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" } }
This function will handle notifications to alert the user that their location is being actively monitored. This means that the information we want to provide the user is the distance in meters between them and the cache. To do this, we'll use the Haversine formula, which uses the coordinates between two points on a sphere to calculate the distance between them. start
So using ours, we'll call that method, and using the provided method, we'll be able to get the updated latitude and longitude. callbackflow clientInfo.getLocationUpdates(interval)
onEach``coroutines
As we said before, we want users to know how far they are from the cache, but there's a catch. We don't want users to receive a barrage of consistent notifications telling them how close they are to the cache.
So we'll create a conditional statement that checks if the user is within the cache's kilometer radius. If true, the user will receive continuous notifications if they are getting further or closer to the cache. Once they come within a 50 meter radius, they will be notified with a different message and the service will stop:
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 valueresult = radius * c val km = valueresult / 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'll create a function that cancels the service when the app is closed or the system cache is cleared. The following is the implementation of the code: onDestroy
class LocServices: Service(){ override fun onDestroy() { super.onDestroy() serviceScope.cancel() } }
Now that we have our foreground service ready, we'll go back to the file and markup just above the metadata markup: AndroidManifest.xml
<service android:name=".fusedLocation.LocServices" android:foregroundServiceType = "location"/>
notification channel
If we want to create a notification for the user's distance from the cache, we need to create a channel to send the notification. Let's start by creating an application called .LocationApp.kt``Application()
In this function we will create a notification channel up from the Android oreo OS. Here's what the code looks like: onCreate
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 add the attribute to the application tag in the following file: AndroidManifest.xml
android:name=".fusedLocation.LocationApp"
MapsActivity.kt
When we create a Google Maps activity, we get a file instead of a regular file. This file handles the creation of the map with markers. We need to make some changes to this. So let's create three variables: and . MapsActivity.kt MainActivity.kt
private lateinit LocationCallback
LocationRequest``FusedLocationProviderClient
Next, we'll create three functions; and . We will call them in the callback function. launchintent getupdatedlocation
startupdate``onMapReady
The function handles the location permission request, the function takes and . The function will also call the start function with its intent processing. launchintent getupdatedlocation
LocationRequest LocationCallback
getupdatedlocation
Finally, in the function, we will use the method to call the callback function, request and looper (set to null). startupdate``fusedlocation.requestLocationUpdates
The code looks like this:
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 get the following result:
in conclusion
In this tutorial, we created a map using Android's Fusion Location library, which constantly updates the user's location on the map. We also created a foreground service that determines the distance between the user and a particular item. Finally, we create a notification for the user to be near the cache.