How to Build a Geocaching App Using Android's Fused Location

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 FlowLocation

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.ktClientInfo ContextFusedLocationProviderClient

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 serviceScopeClientInfo

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 nullonCreate DefaultClientInfoapplicationContext ``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 STARTonStartCommand() intentstart()

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.ktprivate lateinit LocationCallbackLocationRequest``FusedLocationProviderClient

Next, we'll create three functions; and . We will call them in the callback function. launchintent getupdatedlocationstartupdate``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 getupdatedlocationLocationRequest LocationCallbackgetupdatedlocation

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.

Guess you like

Origin blog.csdn.net/weixin_47967031/article/details/132642413