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 falseswitches location off for the whole phone, and Android showed its own "No location access" dialog over the app. Switch it back withtrue, and check withadb 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 aMutableStateFlow. It is not put inSavedStateHandle, 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
- Add the coordinates: the two JSON keys,
NetworkMarket's optional fields, themarketscolumns in schema 6 with their migration test,distanceKmandnearestMarketswith tests. - 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. - Add
LocationSourceand 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. - Set the emulator to your own town and check the order against a map.
- Break it on purpose: put the
getLastKnownLocationshortcut back in a scratch copy and see where it says you are. - 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
- Location permissions — Foreground and background, approximate and precise, with the accuracy of each.
- Request location permissions at runtime — The dialogs for coarse and fine, and what each choice grants.
- Location button — Android 17's one-session precise location, and its Jetpack library.
- LocationManagerCompat —
getCurrentLocationandisLocationEnabledon every API level. - Location permissions policy — Play's rules for coarse, fine and the location button, with their dates.
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