A minimal Kotlin Android app that picks a photo, applies one of three filters (Sketch, B&W, Bubble), saves to the gallery, and monetizes via banner + interstitial AdMob ads — wired up to pass Google's policy review.
Doodlepop = doodle (sketch) + pop (bubble), the two signature effects.
- 3 filters in pure-Kotlin (
ImageFilters.kt) — no OpenCV, ~10 MB APK - Material 3 UI with toolbar + bottom-anchored banner
- AdMob banner (always-on at the bottom)
- AdMob interstitial on Save, with 60-second cooldown
- UMP / GDPR consent gathered before any ad loads
- About / Privacy screen with policy link and "Reset Ad Consent" action
- Debug builds use Google's universal test ad IDs; release builds use your real ones
./gradlew :app:assembleDebug
./gradlew :app:installDebugOr open in Android Studio → Run.
Saved images land in Pictures/Doodlepop/ on the device.
Google reviews new AdMob publishers when their first app starts serving ads. These are the items they actually check. Everything marked ✓ done is wired in code; everything marked TODO needs an action from you outside the IDE.
- ✓ UMP consent gathered before
MobileAds.initialize—ConsentManager.ktruns first, ads load only aftercanRequestAds()returns true. - ✓ Reset-consent control — required by Google so users can change their mind. Lives in About → Reset Ad Consent.
- ✓ Test IDs in debug, real IDs in release — automatic via
buildConfigFieldinapp/build.gradle.kts. Clicking ads in debug is safe; clicking your own production ads will get the account banned. - ✓ Interstitial at a natural break point — fires on Save, never on app launch, with a 60-second cooldown. Loading ads during app launch or on every action is the #1 cause of policy rejection.
- ✓ Banner doesn't overlap content — image preview is constrained above the banner.
- ✓ App ID in
AndroidManifest.xml—ca-app-pub-7343868790309663~1386077355. - ✓ Interstitial unit ID in
app/build.gradle.kts(release) —ca-app-pub-7343868790309663/2507587335.
- Generate a privacy policy (free):
- https://app-privacy-policy-generator.firebaseapp.com/ → tick "AdMob" and "Google Analytics for Firebase" as third-party services.
- Host it publicly. Easiest free option: paste into a Notion page (Share → Publish to web) or a GitHub Pages repo.
- Update
app/src/main/res/values/strings.xml→privacy_policy_urlwith that URL.
- Create a real AdMob banner unit (you only have an interstitial right now):
- https://apps.admob.com/ → your app → Ad units → Add ad unit → Banner.
- Replace
admob_banner_unit_idinapp/src/main/res/values/strings.xmlwith the unit ID it gives you (the one with a/not~).
- Pick a unique applicationId.
com.doodlepop.appis fine if it isn't taken on Play; otherwise change it inapp/build.gradle.ktstocom.<yourbrand>.doodlepop. - Replace the launcher icon. The default vector icon is functional but not distinctive — use Android Studio's File → New → Image Asset to generate a branded one.
- Sign the release build — see Signing below.
- Create the app: https://play.google.com/console → Create app. Category: Photography. Free.
- Upload
app-release.aabunder Release → Production → Create new release. - Data Safety form (under App content):
- Personal info: None collected.
- Device or other IDs: Collected, shared → reason: Advertising or marketing. (AdMob uses GAID.)
- Approximate location: Collected, shared → reason: Advertising or marketing.
- Mark data as collected via third-party SDK (Google AdMob).
- Encryption in transit: Yes.
- Privacy policy URL: paste the URL from step 1 above.
- Content rating: take the IARC questionnaire → answer honestly (it's a photo editor → "Everyone"). Be sure to mark that the app displays ads.
- Target audience and content: age 13+ (AdMob ToS minimum).
- Ads declaration: tick "Yes, my app contains ads."
- Link your AdMob account in Play Console → Monetize with AdMob → Link.
- App access: if anything needs login (Doodlepop doesn't), declare it. If not, mark "All functionality is available without restrictions."
- Submit for review. First review takes 1-7 days. After approval, you can promote to Production.
- Verify the app: AdMob auto-links to the Play listing once both use the same package name.
- Add
app-ads.txtto your website root (e.g.https://yourbrand.com/app-ads.txt):- AdMob shows you the exact line to add under Apps → app-ads.txt → Set up.
- Add the same domain to your Play Listing under Developer page → Website.
- AdMob crawls this within 24 hours; verified = improved fill rate.
- Payment & tax info — Payments → Settings. Required before Google releases your earnings (threshold is $100).
- Clicking your own live ads. Always test with debug builds (test IDs). After publishing, only check that ads load — never click them on a device tied to your AdMob account.
- Putting real unit IDs in debug builds. Other people's clicks on debug-channel ads count as invalid traffic.
- Showing ads in screens with no user-generated content. Doodlepop only shows ads after the user is interacting with a photo — compliant.
- Stacking ads. One banner + one interstitial-on-save is fine. Two banners on one screen is not.
- Modifying ad views. Don't wrap the AdView in a container that obscures or resizes the ad creative.
Already wired. app/build.gradle.kts selects:
| Build type | Interstitial unit ID | Banner unit ID |
|---|---|---|
debug |
Google test (/1033173712) |
Google test (/6300978111) |
release |
Your real (/2507587335) |
Test until you create one — TODO above |
To swap the banner once you create it, change admob_banner_unit_id in
strings.xml (it's read identically in debug and release; if you want to keep
testing in debug, move it to a buildConfigField like the interstitial).
keytool -genkey -v -keystore release.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias doodlepopCreate keystore.properties next to app/:
storeFile=/absolute/path/to/release.jks
storePassword=...
keyAlias=doodlepop
keyPassword=...
Wire into app/build.gradle.kts:
import java.util.Properties
import java.io.FileInputStream
val ksProps = Properties().apply {
val f = rootProject.file("keystore.properties")
if (f.exists()) load(FileInputStream(f))
}
android {
signingConfigs {
create("release") {
storeFile = file(ksProps.getProperty("storeFile") ?: "/dev/null")
storePassword = ksProps.getProperty("storePassword")
keyAlias = ksProps.getProperty("keyAlias")
keyPassword = ksProps.getProperty("keyPassword")
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
}Then build the Play upload bundle:
./gradlew :app:bundleRelease
# app/build/outputs/bundle/release/app-release.aabUMP only shows the form for users in GDPR regions. To force-test it from a US
device, in AdMob Console: Privacy & messaging → GDPR → Add test device IDs
(get yours from logcat: adb logcat | grep "Use RequestConfiguration").
To reset consent locally: open Doodlepop → About → Reset Ad Consent → restart the app.
app/src/main/
AndroidManifest.xml – Activities, AdMob App ID
java/com/doodlepop/app/
MainActivity.kt – UI, picker, save, toolbar
AboutActivity.kt – Privacy + reset consent
ImageFilters.kt – Sketch / B&W / Bubble
InterstitialAdManager.kt – Load/show/cooldown logic
ConsentManager.kt – UMP GDPR/CCPA flow
res/
layout/activity_main.xml – Toolbar + preview + buttons + AdView
layout/activity_about.xml
menu/main_menu.xml
values/strings.xml – Banner unit ID + policy URL
values/themes.xml