RizTech Academy logo
RizTech Academy
Shipping to Google PlayLesson 5 of 545 min

Play policy, data safety and staying published

An app on Google Play makes promises in public. Its store listing carries a Data safety section that says what the app collects and shares, written by you, shown to every farmer before she taps Install. If that section is wrong, the app is breaking Play's policy, whether or not anyone meant harm, and an app can be removed after launch as well as rejected before it. This lesson writes Mandi Bhav's privacy policy and Data safety answers from what the code actually does, checks every network call and permission to prove them, gives the app the in-app privacy notice Play asks for, and shows what one crash-reporting library would change.

As in lesson 3, the console's pages are described from Google's Play Console Help and policy pages as they read in November 2026. Policy wording changes; the method in this lesson does not.

Start from the code, not from the form

The Data safety form asks what the app does. The only honest source for that is the release build, so start there, not from memory of what you meant to build.

Every server the app talks to. Mandi Bhav's code has one base URL, in app/build.gradle.kts, and Coil loads pictures whose addresses the same server's JSON gives. To be sure no library or forgotten line adds another, list every web address compiled into the release APK. A short Python script that reads each classes*.dex in the APK and prints anything starting http:// or https:// gave:

1 http://ns.adobe.com/xap/1.0/
2 http://schemas.android.com/apk/res/android
1 https://developer.android.com/training/articles/direct-boot
1 https://github.com/RizTech-Academy/mandi-bhav/blob/main/PRIVACY.md
1 https://github.com/RizTech-Academy/mandi-bhav/blob/main/PRIVACY.md.
1 https://goo.gle/compose-feedback
1 https://issuetracker.google.com/issues/241760537
1 https://issuetracker.google.com/issues/new?component=413107&template=1096568.
2 https://issuetracker.google.com/issues/new?component=907884&template=1466542
1 https://raw.githubusercontent.com/RizTech-Academy/mandi-bhav/main/api/v1/
1 https://youtrack.jetbrains.com/issue/KT-55980

Most are text inside libraries' error messages and XML namespaces, never requested. The rest are Mandi Bhav's: the API, which the app downloads from, and the privacy policy, which it never downloads but hands to the browser when the user asks for it. (It appears twice because R8 copied the constant into the "No browser on this phone can open …." message, full stop included.) That is the whole list. Run it on the release build: R8 (lesson 1) has removed the classes the app does not use, so what is left belongs to code that can run. The same scan of the unshrunk build, from before this module's changes, printed 15 lines where the release build of that same code printed 9, among them an http://localhost/ from a library's unused code, each of which you would have to chase down.

Every permission. The manifest you wrote is not the manifest that ships: libraries add their own. Read the merged one from the APK:

apkanalyzer manifest permissions app/build/outputs/apk/release/app-release-unsigned.apk
com.riztechacademy.mandibhav.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION
android.permission.ACCESS_COARSE_LOCATION
android.permission.INTERNET
android.permission.FOREGROUND_SERVICE
android.permission.ACCESS_NETWORK_STATE
android.permission.WAKE_LOCK
android.permission.POST_NOTIFICATIONS
android.permission.RECEIVE_BOOT_COMPLETED

Four of these are yours, from module 9, 10 and 11. WAKE_LOCK, RECEIVE_BOOT_COMPLETED and FOREGROUND_SERVICE come from WorkManager, which keeps the processor awake while a worker runs, reschedules work after a restart, and can run long work in a foreground service (Mandi Bhav's worker never asks it to). The first line comes from AndroidX Core: a permission only this app holds, used to protect broadcast receivers it registers at run time. You do not have to guess which library added what: the manifest merger writes a report, app/build/intermediates/manifest_merge_blame_file/release/processReleaseMainManifest/manifest-merger-blame-release-report.txt, with the source of every line, and for these it names androidx.work:work-runtime:2.12.0 and androidx.core:core:1.19.1. None of the library ones gives access to anything personal, but you should know why each is there before Google, or a user, asks.

From those two lists and the code, here is everything Mandi Bhav touches:

What Where in the code Leaves the phone?
Prices, history, pictures PricesApi, Coil Requests go out; nothing about the user is in them. The server sees the phone's IP address, as every web server does
Approximate location LocationSource, Markets tab No: used once to sort markets, never saved
Notifications SystemAlertNotifier No: posted by the app after a refresh
Sale slip photos SaleSlipRepository, files/slips/ No
Alerts, home market Room, DataStore Only the home market, in the phone's own backup if the user has it on (data_extraction_rules.xml)
Share, Show on map Intents.kt Only when the user taps, to the app the user chooses
Crash and ANR reports Android itself (module 13) Android vitals, not the app; module 13's LastExit log is debug-only

What the Data safety form means by "collect" and "share"

Google's definitions decide the answers, and they are narrower than everyday speech:

Term Google's meaning Not counted
Collect Transmitting data from your app off the user's device Data processed only on the device; data used only in memory to serve a request (ephemeral processing) is listed in the form but not shown on the listing
Share Transferring user data to a third party A service provider processing it for you; a transfer the user starts and expects, such as sharing a price; anonymised data
Third-party code Data your libraries send counts as your app's collection, whoever receives it Nothing: you answer for your SDKs

Two Mandi Bhav cases worth working through:

  • Location. Approximate location is a data type in the form, but it is only collected if it leaves the phone. Mandi Bhav's never does: nearestMarkets runs on the phone. So it is not declared. Had the app sent the location to a server to find the nearest market, it would be, even though the user would see the same screen.
  • The IP address. The page addresses it in one place: location inferred from an IP address must be declared as location. Mandi Bhav's server is a set of static files on GitHub; the app does not use the address and has no server code that could. The privacy policy names it anyway, honestly, because GitHub's servers see it.

So Mandi Bhav's answer to the form's first question, whether the app collects or shares any of the required user data types, is no. The rest of the form asks, for each data type collected, whether it is shared, optional, encrypted in transit, deletable on request, and why it is used; with nothing collected, those answers are empty. Even so, you must complete the form and give a privacy policy: the Help page says every app on a closed, open or production track must, including apps that collect nothing. Only an app that lives on the internal testing track alone is exempt.

The trap: a library you add for something else

Module 13 left one promise for here: what if Mandi Bhav used Firebase Crashlytics? Its own data disclosure page lists what it collects by default: stack traces, relevant app state and device metadata at the time of a crash, and an installation identifier to count affected users. In Play's terms that is Crash logs and Diagnostics (under App info and performance) and Device or other IDs, all collected, because they are sent to Firebase. Firebase processes them for you, which is the service-provider case, so not shared. The form would gain those three types with a purpose for each, the privacy policy would need a new row naming Google as the processor, and both would have to change in the same release as the library.

The trap is the order people do it in: the SDK goes in with a version bump, the release goes out, and the form still says "no data collected". The listing is now false. Make the Data safety form part of the change that adds the dependency, the way a database migration is part of the change that alters an entity, and read the release APK's URL list and permissions again before each release, as above.

The privacy policy

Google's User Data policy, as it reads now:

  • Every app must have one, including apps that collect nothing, with the link in the Play Console's privacy policy field.
  • Inside the app too: the policy asks for a privacy policy link or text in the app itself.
  • On an active, publicly accessible, non-geofenced URL, not a PDF, and not editable by visitors (a shared document anyone can edit does not count).
  • It must give the developer's details and a privacy contact, the kinds of personal data accessed, collected, used and shared and with whom, how data is handled securely, how long it is kept and how to have it deleted, and be clearly labelled as a privacy policy.

Mandi Bhav's is PRIVACY.md at the root of its repository, so its address is a public GitHub page: a table of what the app uses, why, and where it goes, the same rows as the table above; how to delete everything (uninstall, or clear storage); and an issues page for questions. A real product's policy names a real person or company and an email address, and may need a lawyer; a course app's can be this short because the app is this simple.

Inside the app, the notice is on the Alerts tab, the tab that holds the user's own data and asks for the notification permission. A Privacy button in its top bar opens a dialog:

const val PRIVACY_POLICY_URL = "https://github.com/RizTech-Academy/mandi-bhav/blob/main/PRIVACY.md"

/**
 * What the app does with data, in plain words. It must say the same as
 * PRIVACY.md and the Data safety form in the Play Console: change all three
 * together, or none.
 */
val privacySummary = listOf(
    "Mandi Bhav collects no personal data and shares none. There are no accounts, ads, analytics or crash reports.",
    "Prices and pictures are downloaded from Mandi Bhav's server on GitHub. The app sends nothing about you; " +
        "like any website, the server sees your phone's internet address.",
    "If you allow it, your approximate location is used once, on this phone, to find the nearest market. " +
        "It is not saved or sent anywhere.",
    "Your price alerts and sale slip photos stay on this phone. Only your home market is included in your " +
        "phone's own backup, if backup is on.",
)
var showPrivacy by rememberSaveable { mutableStateOf(false) }
if (showPrivacy) {
    PrivacyDialog(onOpenPolicy = onOpenPrivacyPolicy, onDismiss = { showPrivacy = false })
}
Scaffold(
    topBar = {
        TopAppBar(
            title = { Text("Alerts") },
            actions = { TextButton(onClick = { showPrivacy = true }) { Text("Privacy") } },
        )
    },
)

PrivacyDialog shows the four paragraphs, with Close and Full policy; the route opens the URL with ACTION_VIEW, catching ActivityNotFoundException as openOnMap does in module 11. The summary is text in the app, so it works offline, on the farm, where the full policy would not load. PrivacyNoticeTest, in sharedTest, taps Privacy, checks the first paragraph is shown, taps Full policy and checks the route was called; it runs on the JVM and on the emulator.

Permissions that need a declaration

Some permissions need more than a manifest line: Google asks for a declaration in the console, explaining why the app needs them, and may refuse. From Google's policy on permissions that access sensitive information:

Permission or API Declaration needed
SMS and Call Log Yes, and only for default handler apps and narrow exceptions
Background location Yes
All files access (MANAGE_EXTERNAL_STORAGE) Yes
Photos and videos (READ_MEDIA_IMAGES, READ_MEDIA_VIDEO) Yes, and only if the system photo picker is not enough
Exact alarms (USE_EXACT_ALARM), full-screen intents Yes
Accessibility services, QUERY_ALL_PACKAGES, REQUEST_INSTALL_PACKAGES Yes
Precise location (ACCESS_FINE_LOCATION) Announced: for apps targeting API 37 or higher, from 27 January 2027

Mandi Bhav needs none of them, and that was decided in module 11, not here: the photo picker and the camera app instead of storage and camera permissions, coarse location instead of fine, a location used while the app is open instead of in the background. The cheapest declaration is the one the design never needed.

Staying published

Getting on Play is not the end of the obligations:

  • The target API level moves every year (lesson 3); an app that stops updating stops reaching new users on new phones.
  • The forms describe the current release. Content rating, Data safety, privacy policy and the in-app notice change with the code that changes them.
  • Read Google's policy emails. Policy updates come with dates, like the precise-location declaration above. Put each date in the calendar the day it arrives.

Check your work

Why is Mandi Bhav's approximate location not declared as collected? Because it never leaves the phone; Google's definition of collect is transmitting off the device. Sending it to a server, even to compute the same answer, would make it collected.

Which three permissions in the merged manifest came from WorkManager, and why did the lesson check them? WAKE_LOCK, RECEIVE_BOOT_COMPLETED and FOREGROUND_SERVICE. Libraries add permissions you never wrote, and you answer for the merged manifest, not the one in your source.

Does an app that collects nothing need a Data safety form and a privacy policy? Yes, both, on any closed, open or production track; only an app on internal testing alone is exempt.

What would adding Crashlytics add to the form? Crash logs and diagnostics, and a device or other ID, as collected (not shared, since Firebase processes them for you), each with a purpose; and a matching change to the privacy policy and the in-app notice, in the same release.

Why does the URL scan work better on the release build? R8 has removed unused code, so the strings left belong to code that can run; the unshrunk build printed 15 lines to the shrunk build's 9, including an http://localhost/ from unused library code.

Practice

  1. Write the Python scan and run it on your release APK. Account for every URL it prints: which are requested, which are only text?
  2. Run apkanalyzer manifest permissions on your debug APK and compare it with the release one. Which extra permissions does the debug build carry, and which library adds each? (Module 13 added one library to debug builds only.)
  3. Write Mandi Bhav's Data safety answers on paper, one line per data type the form lists that the app touches in any way, with "collected: no" or "yes" and the reason.
  4. Break it on purpose: in a scratch copy, send the user's coordinates to the prices server as query parameters on the prices.json request (the static server ignores them). Run the URL scan and the app, and decide what the Data safety form and the privacy policy must now say. Then remove the change.
  5. Harder: plan a Crashlytics change for Mandi Bhav without making it: the dependencies and Gradle plugin, the Firebase project, every change to PRIVACY.md, privacySummary and the Data safety form, and what a farmer on a ₹9,000 phone pays for it in APK size and start-up. Check your list against Firebase's data disclosure page.

Official documentation

Next: module 15, Android Best Practices.


That is module 14. You can now:

  • turn on R8 for a release build, read the rules your libraries already ship, add a keep rule only when a minified build has shown you need one, and measure what shrinking saved with apkanalyzer;
  • keep every release's mapping file, match its pg_map_id to the r8-map-id in a crash, and know that an app bundle carries it to Play for you;
  • sign a release with an upload key whose passwords never enter the repository, and know why losing it is recoverable under Play App Signing when losing an app signing key once was not;
  • walk through the Play Console's setup with real answers: the listing, content rating, target audience, the target API requirement, the pre-launch report and Android vitals;
  • plan internal, closed and open testing, meet the 12-tester requirement for a new personal account, number releases so they always go forwards, and halt a staged rollout;
  • write a Data safety declaration and a privacy policy from what the release build actually does, and keep them true as dependencies change.

Mandi Bhav now has a shrunk, signable release build that keeps its signing secrets out of git, an archived mapping per release, a regenerated Baseline Profile, a privacy policy and an in-app privacy notice, and 206 unit tests. Module 15, Android Best Practices, steps back from shipping to the code itself: how to name things from table to screen, where logic belongs, and how to review an Android change, using fourteen modules of Mandi Bhav as the example and the evidence.

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