RizTech Academy logo
RizTech Academy
Permissions and Device FeaturesLesson 3 of 430 min

Location, and its privacy expectations

Arjun farms near Niphad, between Lasalgaon and Nashik. When his tomatoes are ready, the question before the price is which market he can reach before the morning auction. The Markets tab lists four markets in alphabetical order, which helps nobody standing in a field. This lesson sorts them by distance from where he is, and the whole of the work is in asking for as little as possible: an approximate location, once, on the phone, never saved and never sent anywhere. That is also what Google Play now expects, and what a farmer who has heard stories about apps tracking people is right to insist on.

Where the markets are

A distance needs two ends. The markets' end comes from the prices API, which module 9 made a rule of: add fields to v1/, never rename or remove them, because released builds read those files. Each market in prices.json gains two keys:

{
  "name": "Lasalgaon APMC",
  "district": "Nashik",
  "state": "Maharashtra",
  "latitude": 20.1500,
  "longitude": 74.2330
}

roughly where the market yard is, to about a kilometre. Mandi Bhav's parser has ignoreUnknownKeys on, so builds already installed skip them; to be sure rather than hopeful, the module 10 checkpoint's own RetrofitPriceNetworkDataSourceTest was run against the new files, and all 13 tests passed. The new parser reads them as optional (val latitude: Double? = null), so the old files still parse too, and a market with half a location, or a latitude of 200, gets no location rather than a wrong one. Both cases have a test.

The coordinates go into Room as two nullable columns on markets, part of schema 6 with the sale slips, and reach the app as a GeoPoint on Market. The distance is a pure function, domain/NearestMarkets.kt:

fun distanceKm(from: GeoPoint, to: GeoPoint): Double {
    val lat1 = Math.toRadians(from.latitude)
    val lat2 = Math.toRadians(to.latitude)
    val dLat = lat2 - lat1
    val dLon = Math.toRadians(to.longitude - from.longitude)
    val a = sin(dLat / 2).pow(2) + cos(lat1) * cos(lat2) * sin(dLon / 2).pow(2)
    return 2 * EARTH_RADIUS_KM * asin(sqrt(a))
}

the haversine formula: the distance over the Earth's surface, in a straight line. Lasalgaon to Nashik comes out at 48.8 km, and one degree of latitude at 111.2 km, which is what the tests check. It is not the road distance. A market 30 km away across a river may be 50 by road, which is why the card says "in a straight line" and the market screen has a button that opens a maps app for the real route (lesson 4).

Approximate, not precise

Android has two location permissions, and the user decides which one an app gets.

ACCESS_COARSE_LOCATION ACCESS_FINE_LOCATION
Accuracy About 3 square kilometres Usually within about 50 metres, sometimes a few
The dialog "approximate location" only A choice of precise or approximate (Android 12+)
Enough for Which town, which market, the weather Directions, delivery to a door, running routes
Google Play Preferred whenever it does the job Only when neither approximate location nor the location button can

"Which APMC is closest" is a question about towns. Precise location would tell Mandi Bhav which field Arjun is standing in, which it has no use for, and a user shown a precise-location request from a price app is right to say no. Requesting only coarse location also changes the dialog. On the emulator, "Use my location" showed:

Allow Mandi Bhav to access this device's approximate location?
While using the app
Only this time
Don't allow

with no precise option at all.

How approximate is approximate? With the emulator set to Niphad (20.08 N, 74.11 E), the fix a coarse-only app received was:

fix=Location[gps 20.072072,74.107631 hAcc=2000.0 et=+19h9m17s441ms]

0.9 km from the real position, with an accuracy of 2,000 metres. Android moved the point, and said so. For Mandi Bhav that changed Lasalgaon from 15 km to 16.

One more thing is coming that you should know about. Android 17 adds a location button, a system-drawn button that grants precise location for one session, from an experimental Jetpack library. Google Play's policy, effective 27 January 2027, requires it for apps targeting Android 17 or later that need precise location only for one-time, user-started actions. It is about precise location: an app that needs only approximate location, like this one, is told to request coarse location, as Mandi Bhav does. Mandi Bhav targets API 36 today; when the course moves it to 37, that policy is the thing to read again.

LocationManager or Play services

There are two APIs for asking where the phone is:

Framework LocationManager (with LocationManagerCompat) FusedLocationProviderClient (play-services-location 21.4.0)
Dependency None; core-ktx is already in the app Google Play services library
Works on Every Android phone Phones with Google Play services
Battery and accuracy Good, using Android's fused provider from Android 12 Very good, on every version, with geofencing and activity recognition
APK size, unminified release 10,945,935 bytes 11,406,794 bytes (+450 KB)

Both built with AGP 9.5.1 and Kotlin 2.4.21 in a scratch copy; the size is from that. For one fix when the user taps a button, the framework is enough, works on phones sold without Google's apps, and costs nothing. If Mandi Bhav one day needs geofences ("tell me when I reach the market"), Play services earns its place.

One fix, and how it fails

data/LocationSource.kt answers one question with one of four results:

sealed interface LocationResult {
    data class Found(val point: GeoPoint, val accuracyMetres: Float?) : LocationResult
    data object NoPermission : LocationResult
    data object LocationOff : LocationResult
    data object NoFix : LocationResult
}

and the Android implementation checks them in order: the permission (it may have been revoked a second ago), whether location is switched on for the whole phone, then a fix:

override suspend fun currentLocation(): LocationResult {
    val coarse = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
    if (coarse != PackageManager.PERMISSION_GRANTED) return LocationResult.NoPermission
    if (!LocationManagerCompat.isLocationEnabled(manager)) return LocationResult.LocationOff

    val location = try {
        currentFix()
    } catch (e: SecurityException) {
        // Revoked between the check and the call.
        return LocationResult.NoPermission
    }
    return location?.let { LocationResult.Found(GeoPoint(it.latitude, it.longitude), it.accuracyOrNull()) }
        ?: LocationResult.NoFix
}

A fix is one call to LocationManagerCompat.getCurrentLocation, wrapped in suspendCancellableCoroutine so that leaving the screen cancels it, and in withTimeoutOrNull so it cannot wait for ever. Never requestLocationUpdates: Mandi Bhav wants to know where Arjun is once, not to follow him.

The provider is tried in order. First the fused provider (Android 12 and later), or the network provider before that: Wi-Fi and mobile towers, cheap on the battery, for 10 seconds. Then GPS, for 20. Before Android 12, GPS needs precise location, so a coarse app skips it there.

Trap one: the last known location. The first version asked getLastKnownLocation first and used anything under an hour old, which sounds thrifty. On the emulator it answered instantly:

Nearest: Lasalgaon APMC
About 13389 km away in a straight line, from your approximate location.

The fused provider's cached fix was the emulator's default position, in California, seven minutes old. On a phone it would be wherever the phone last worked out it was, which after a bus ride from Pune is the wrong district. getCurrentLocation already returns a cached fix when one is recent enough for the provider, and a fresh one otherwise, so the shortcut was removed.

Trap two: the emulator has no Wi-Fi or towers to locate by. The fused provider gave nothing in 20 seconds. dumpsys location showed the request and its end:

fused provider +registration 10229/com.riztechacademy.mandibhav/… -> Request[@0 BALANCED, duration=+30s0ms, …]
fused provider -registration 10229/com.riztechacademy.mandibhav/…

That is why GPS comes second. With the fallback: the fused provider gave up at 01:32:15.666, GPS delivered at 01:32:19.548, under 4 seconds, and the card said "Nearest: Lasalgaon APMC. About 16 km away". A phone in a field with no Wi-Fi is closer to the emulator than to your desk.

Testing location on the emulator

adb emu geo fix 74.1100 20.0800

sets the emulator's GPS position. Longitude first, then latitude, the reverse of how people say them; swapped, Niphad lands in the Arctic Ocean. A fix only reaches an app while it is asking, so send the command, or repeat it, during the request. Two more things the emulator did that you will meet:

  • After one fix, asking again within about ten minutes produced nothing, however the position was changed; thirteen minutes after the first, a fix from Kharadi arrived and Pune (Gultekdi) APMC came out nearest at 11 km. The cause was not established. Treat it as a free test of the "Could not find where you are" path.
  • adb shell cmd location set-location-enabled false switches location off for the whole phone, and Android showed its own "No location access" dialog over the app. Switch it back with true, and check with adb shell cmd location is-location-enabled.

Every state the card can be in

State The card says Its button
Not asked Sort the markets by how far they are from you. Mandi Bhav uses your approximate location, once, on this phone. Use my location
Finding Finding where you are… This can take up to 30 seconds. None
Found Nearest: Lasalgaon APMC. About 16 km away in a straight line, from your approximate location. Update
Refused once Without your location, Mandi Bhav cannot tell which market is nearest. You can still choose one from the list. Use my location (explains first)
Blocked Location is not allowed... Open Settings
Location off Your phone's location is off Turn on location
No fix Could not find where you are Try again
No market locations Market locations not downloaded yet. Refresh the prices on the Prices tab. Try again

Two of those rows were added because the emulator showed them missing.

No market locations. When this module's build was first installed, its saved markets had come from the live API on GitHub, which does not have the coordinates until the files are pushed. A fix with nothing to measure to would have said "Could not find where you are", blaming Arjun's phone for the server's gap. So marketsUiState says what is actually missing.

Location off, then on. "Turn on location" opens the system's location settings. Arjun switched it on, came back, and the card still said his location was off: the answer was the last one the app had, from before. The route now asks again when the screen resumes in that state:

LifecycleResumeEffect(uiState.nearest) {
    if (uiState.nearest == NearestMarket.LocationOff && location.status == PermissionStatus.Granted) {
        viewModel.findNearest()
    }
    onPauseOrDispose { }
}

Measured: location off, "Use my location", "Turn on location", the switch in Settings, Back; at 01:45:58 the card said "Finding where you are…" and by 01:46:18 "Nearest: Pune (Gultekdi) APMC. About 11 km away", with the list re-sorted: Pune 11, Vashi 116, Nashik 164, Lasalgaon 181.

The privacy promise, in code

The card promises "once, on this phone". The code keeps it:

  • The fix lives in MarketsViewModel, in memory, in a MutableStateFlow. It is not put in SavedStateHandle, Room or DataStore, so after process death the list is back in name order until Arjun asks again. That costs him one tap.
  • Nothing logs it. The scratch builds in this lesson printed fixes with a tag; the real app does not, because logcat is readable by anyone with the phone and a cable.
  • Nothing sends it. The distances are computed on the phone, from coordinates every user downloads anyway. The prices server never learns where anyone is, which also means there is nothing to declare and nothing to leak.

That last point is a design choice with a cost: the server cannot say "farmers near Niphad are looking at tomato prices", which a business might want to know. Mandi Bhav decides that it does not.

Check your work

Why coarse location, not fine. Choosing a market needs a town, not a field; approximate location is accurate to about 3 square kilometres, the dialog then offers nothing more, and Play allows fine location only when approximate location cannot do the job.

What the coarse fix looked like. 20.072072, 74.107631 with an accuracy of 2,000 m, for a real position of 20.08, 74.11: 0.9 km off.

Why not getLastKnownLocation. It returned a seven-minute-old cached fix in California, 13,389 km from every market; getCurrentLocation uses a cached fix only when it is recent enough, and gets a fresh one otherwise.

Why GPS is tried after the fused provider. On the emulator, and on a phone with no Wi-Fi or towers to locate by, the fused provider found nothing in its 10 seconds; GPS answered in under 4.

Practice 2. adb emu geo fix 20.08 74.11 puts the phone at latitude 74.11 north, longitude 20.08 east, in the Arctic Ocean north of Norway. By the same formula every market is between 6,800 and 7,000 km away, Lasalgaon nearest.

Practice

  1. Add the coordinates: the two JSON keys, NetworkMarket's optional fields, the markets columns in schema 6 with their migration test, distanceKm and nearestMarkets with tests.
  2. Run adb emu geo fix 20.08 74.11, with the numbers the wrong way round, and ask for the nearest market. Predict the distances first.
  3. Add LocationSource and its fake, the nearest-market card and its states. Then go through every row of the table above on the emulator, including location off and a blocked permission.
  4. Set the emulator to your own town and check the order against a map.
  5. Break it on purpose: put the getLastKnownLocation shortcut back in a scratch copy and see where it says you are.
  6. Harder: Kavita trades at two markets 40 km apart. Add "Show distances from my home market" as a second way to sort, using the home market's coordinates and no location permission at all. Decide which the card should offer first, and why.

Official documentation

Next: intents, sharing and deep links, where Mandi Bhav talks to other apps and other apps talk to it.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship