Android
The Ezoic Ads SDK for Android lets you display banner, rewarded, interstitial, native, outstream video, and instream video ads in native Android apps. The SDK initializes Ezoic configuration, Prebid, Google Ad Manager, and consent handling from one integration point.
Requirements
- Android SDK 24 or higher
- Android Gradle Plugin 8.0 or higher
- Kotlin 2.1 or higher if your app compiles against Google Mobile Ads directly (for example to render native ads or call Google Mobile Ads yourself); Java-only apps and apps that only use the Ezoic API are unaffected
- Google Mobile Ads application ID
Installation
The current SDK version is 1.19.1. Add the Ezoic Ads SDK dependency to your app module:
dependencies {
implementation("com.ezoic.sdk:ezoic-ads-sdk:1.19.1")
}
The SDK depends on Google Mobile Ads and Prebid Mobile. Make sure your project resolves dependencies from Google's Maven repository and Maven Central.
Upgrading to 1.19.1: no app changes are needed. Release builds made with Android Gradle Plugin 9 no longer crash at startup with "Failed to create an instance of androidx.work.impl.WorkDatabase". AGP 9 turns on R8 strict full mode, which stops the keep rules of the WorkManager and Room versions Google Mobile Ads depends on from keeping constructors those libraries create by reflection. The SDK now ships rules that keep them.
Upgrading to 1.19.0: one behavior change to check:
- Banner and outstream views no longer destroy themselves when they leave the window. RecyclerView, ViewPager2, Compose lazy lists and tab layouts detach views and attach them again later, and in 1.18 that left the ad blank. Views now pause while detached and resume when reattached. Call
destroy()when you remove a view for good; otherwise it's released when its Activity or Fragment is destroyed.
Also new, with no app changes needed:
setSubjectToCOPPA(true)now also tags Google Ad Manager requests as child-directed, addstfcd=1to instream video tags, and skips Amazon bids.- Consent set with
setGDPRConsentorsetGPPConsentbeforeinitializeis now written where Google and the other ad SDKs read it. - The device ID the SDK sends is kept only while storage consent allows it.
- Calling
initializeagain now reports success instead ofAlreadyInitialized. - Apps that initialize after their first screen appears get a launch pageview, and ad requests no longer wait 2 seconds for one.
trackPageview(screen)without a completion callback sends the pageview again; it hadn't since 1.12.0.
Upgrading to 1.18.1: no app changes are needed. EzoicAds.instance.initialize can now be called from any thread. In 1.17.0 and 1.18.0, calling it off the main thread never ran the callback, so no ads loaded.
Upgrading to 1.18.0: no app changes are needed. New EzoicNativeAdTemplateView is a ready-made native ad layout you can restyle with EzoicNativeAdStyle; see Use the ready-made template. Rewarded ads also report an outcome for every load.
Upgrading to 1.17.0: if you show native ads, call ad.registerView(adView) with the view that displays the ad (see Add a Native Ad); without it native ads are not reported as viewed. No other app changes are needed. Ad measurement now matches Ezoic's web reporting more closely: viewed impressions for every format, an outcome for every ad opportunity including no-fills, and revenue and floor values reported the same way as on the web. Google's full-screen ad screens and returning from the background no longer count as new pageviews. If you label your launch screen, see Labeling screens.
Upgrading to 1.16.0: no app changes are needed. Banners can now refresh when Ezoic enables it for the ad unit (see Banner refresh).
Upgrading to 1.15.0: the SDK now uses Google Mobile Ads 25.2.0 (version 22.x is past Google's sunset date) and, through it, the User Messaging Platform SDK 4.0.0. If your app forces Google Mobile Ads to an older version, remove that constraint.
Initialize the SDK
Initialize the SDK from your Application class or main Activity before loading ads.
import android.app.Application
import android.util.Log
import com.ezoic.ads.sdk.core.EzoicAds
import com.ezoic.ads.sdk.core.EzoicConfiguration
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
val config = EzoicConfiguration(
domain = "example.com",
debugEnabled = false,
testMode = false
)
EzoicAds.instance.initialize(this, config) { result ->
result.onSuccess {
Log.d("Ezoic", "SDK initialized")
}.onFailure { error ->
Log.e("Ezoic", "Initialization failed: $error")
}
}
}
}
domain must match the domain configured for your site in Ezoic.
Add a Banner Ad
Create an EzoicBannerView, add it to your layout, and call loadAd() after the SDK is initialized.
import android.os.Bundle
import android.util.Log
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import com.ezoic.ads.sdk.adunits.EzoicBannerView
import com.ezoic.ads.sdk.adunits.EzoicBannerViewListener
import com.ezoic.ads.sdk.core.EzoicError
class MainActivity : AppCompatActivity(), EzoicBannerViewListener {
private lateinit var bannerView: EzoicBannerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bannerView = EzoicBannerView(this, 12345)
bannerView.listener = this
val container = findViewById<FrameLayout>(R.id.banner_container)
container.addView(bannerView)
bannerView.loadAd("300x250")
}
override fun onBannerLoaded(bannerView: EzoicBannerView) {
Log.d("Ezoic", "Banner loaded")
}
override fun onBannerLoadFailed(bannerView: EzoicBannerView, error: EzoicError) {
Log.e("Ezoic", "Banner failed: ${error.message}")
}
override fun onBannerSizeChanged(bannerView: EzoicBannerView, widthDp: Int, heightDp: Int) {
Log.d("Ezoic", "Banner size: ${widthDp}x${heightDp}")
}
override fun onDestroy() {
bannerView.destroy()
super.onDestroy()
}
}
Replace 12345 with your Ezoic ad unit identifier. The SDK fetches the Google Ad Manager ad unit, Prebid configuration, supported sizes, targeting values, and refresh interval from Ezoic servers. Give the banner container android:layout_height="wrap_content" so collapse can take effect.
Banner refresh
Banner refresh is off unless Ezoic's servers enable it for the ad unit. To control it yourself, set these on the banner before loadAd(); leave them null to follow Ezoic's setting:
bannerView.refreshIntervalSeconds = 60 // 0 = off, minimum 30
bannerView.refreshMaxCount = 10 // refreshes per loadAd(), 0 = no limit, maximum 100
The countdown only runs while at least half of the banner is on screen and visible and your app is in the foreground; it pauses otherwise. If your Google Ad Manager ad unit has its own auto-refresh turned on, turn it off so the banner is not refreshed twice.
Unfilled ads
When no ad fills, EzoicBannerView collapses (View.GONE) so it does not leave a blank gap. This is controlled by collapseOnNoFill (default true). isCollapsed is true while the view is collapsed. If a refresh does not fill, the previous ad stays visible. onBannerSizeChanged reports the displayed size in dp, or 0×0 when the view collapses.
Banner Sizes
You can load ads with an adaptive default size, a typed size, one size string, or a list of size strings.
bannerView.loadAd()
bannerView.loadAd(EzoicBannerSize.MediumRectangle)
bannerView.loadAd("300x250")
bannerView.loadAd(listOf("300x250", "320x50"))
Common sizes include:
EzoicBannerSize.Banner: 320x50EzoicBannerSize.LargeBanner: 320x100EzoicBannerSize.MediumRectangle: 300x250EzoicBannerSize.FullBanner: 468x60EzoicBannerSize.Leaderboard: 728x90EzoicBannerSize.Custom(width, height): custom dimensions
Add a Rewarded Ad
Rewarded ads are full-screen ads that grant an in-app reward when the user finishes watching. They use a load-then-show lifecycle: load the ad ahead of time (for example, at the start of a level), then present it at a natural break and grant the reward when it is earned.
import com.ezoic.ads.sdk.adunits.EzoicRewardedAd
import com.ezoic.ads.sdk.adunits.EzoicRewardedAdListenerAdapter
EzoicRewardedAd.load(this, 12345) { result ->
result.onSuccess { ad ->
// Optional: observe lifecycle events
ad.listener = object : EzoicRewardedAdListenerAdapter() {
override fun onRewardedAdDismissed(rewardedAd: EzoicRewardedAd) {
Log.d("Ezoic", "Rewarded ad closed")
}
}
// Present and grant the reward when earned
ad.show(this) { reward ->
Log.d("Ezoic", "Earned ${reward.amount} ${reward.type}")
grantReward(reward.amount)
}
}.onFailure { error ->
Log.e("Ezoic", "Rewarded load failed: ${error.message}")
}
}
Replace 12345 with your Ezoic ad unit identifier. Call show(activity) only after the ad has loaded; showing before the load completes (or after the ad was already shown) reports EzoicError.AdNotReady. Rewarded ads are single-use — load a new ad for each opportunity.
You can pass a reward name when you show the ad. The name appears in your reports.
ad.show(this, "extra life") { reward ->
grantReward(reward.amount)
}
The reward type and amount come from the reward configured on the Google Ad Manager rewarded ad unit. The EzoicRewardedAdListener also reports onRewardedAdShown, onRewardedAdFailedToShow, onRewardedAdImpression, onRewardedAdClicked, and onUserEarnedReward. Java apps load with an EzoicRewardedAdLoadListener and extend EzoicRewardedAdListenerAdapter.
Add an Interstitial Ad
Interstitial ads are full-screen ads shown at natural transition points (for example, between levels or screens). Unlike rewarded ads, they grant no reward. They use the same load-then-show lifecycle: load the ad ahead of time, then present it at a natural break.
import com.ezoic.ads.sdk.adunits.EzoicInterstitialAd
import com.ezoic.ads.sdk.adunits.EzoicInterstitialAdListenerAdapter
EzoicInterstitialAd.load(this, 12345) { result ->
result.onSuccess { ad ->
// Optional: observe lifecycle events
ad.listener = object : EzoicInterstitialAdListenerAdapter() {
override fun onInterstitialAdDismissed(interstitialAd: EzoicInterstitialAd) {
Log.d("Ezoic", "Interstitial ad closed")
}
}
// Present at a natural transition point
ad.show(this)
}.onFailure { error ->
Log.e("Ezoic", "Interstitial load failed: ${error.message}")
}
}
Replace 12345 with your Ezoic ad unit identifier. Call show(activity) only after the ad has loaded; showing before the load completes (or after the ad was already shown) reports EzoicError.AdNotReady. Interstitial ads are single-use — load a new ad for each opportunity.
The EzoicInterstitialAdListener reports onInterstitialAdShown, onInterstitialAdFailedToShow, onInterstitialAdImpression, onInterstitialAdClicked, and onInterstitialAdDismissed. Java apps load with an EzoicInterstitialAdLoadListener and extend EzoicInterstitialAdListenerAdapter.
Add an Outstream Video Ad
Outstream video ads render inline in your own layout, like a banner, but with a video creative in an SDK-built player. Create an EzoicOutstreamAdView, add it to your layout, and call loadAd().
import com.ezoic.ads.sdk.adunits.EzoicOutstreamAdView
import com.ezoic.ads.sdk.adunits.EzoicOutstreamAdViewListener
import com.ezoic.ads.sdk.core.EzoicError
class MainActivity : AppCompatActivity(), EzoicOutstreamAdViewListener {
private lateinit var outstreamAdView: EzoicOutstreamAdView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
outstreamAdView = EzoicOutstreamAdView(this, 12345)
outstreamAdView.listener = this
val container = findViewById<FrameLayout>(R.id.outstream_container)
container.addView(outstreamAdView)
outstreamAdView.loadAd()
}
override fun onOutstreamLoaded(adView: EzoicOutstreamAdView) {
Log.d("Ezoic", "Outstream ad loaded")
}
override fun onOutstreamLoadFailed(adView: EzoicOutstreamAdView, error: EzoicError) {
Log.e("Ezoic", "Outstream failed: ${error.message}")
}
override fun onDestroy() {
outstreamAdView.destroy()
super.onDestroy()
}
}
Replace 12345 with your Ezoic ad unit identifier. EzoicOutstreamAdView is also XML-inflatable. Unlike EzoicBannerView, loadAd() takes no size — the SDK renders at the size configured on the server (640x360 by default). Call destroy() when the hosting Activity or Fragment is destroyed.
The EzoicOutstreamAdViewListener also reports onOutstreamImpression, onOutstreamClicked, onOutstreamOpened, onOutstreamClosed, and onOutstreamSizeChanged — all optional. EzoicOutstreamAdView collapses on no-fill the same way as EzoicBannerView (collapseOnNoFill, default true). Use a wrap_content container so the collapse takes effect.
Add an Instream Video Ad
Instream video ads play inside your own video content. Your app owns the video player and the Google IMA SDK; the Ezoic SDK's job is to build the Google Ad Manager VAST ad-tag URL you hand to IMA. There is no Ezoic view — the deliverable is a URL string. EzoicInstreamAd is a view-less, multi-use controller: unlike rewarded and interstitial ads it is not single-use, so keep the instance and call load() again for the next video.
import com.ezoic.ads.sdk.adunits.EzoicInstreamAd
import com.ezoic.ads.sdk.adunits.EzoicInstreamAdListener
import com.ezoic.ads.sdk.core.EzoicError
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory
private val instreamAd = EzoicInstreamAd(12345)
instreamAd.load(this, contentUrl = playingVideoUrl, listener = object : EzoicInstreamAdListener {
override fun onAdTagReady(adTagUrl: String) {
// Feed the tag to your IMA ads request.
val request = ImaSdkFactory.getInstance().createAdsRequest()
request.adTagUrl = adTagUrl
adsLoader.requestAds(request)
}
override fun onAdFailedToLoad(error: EzoicError) {
Log.e("Ezoic", "Instream load failed: ${error.message}")
// Play content without a preroll.
}
})
// In your AdErrorEvent.AdErrorListener, walk down the floor waterfall:
instreamAd.getNextAdTagUrl()?.let { nextTag ->
val request = ImaSdkFactory.getInstance().createAdsRequest()
request.adTagUrl = nextTag
adsLoader.requestAds(request)
}
// In your AdEvent.AdEventListener, on AdEventType.STARTED:
instreamAd.reportImpression()
Replace 12345 with your Ezoic ad unit identifier. contentUrl (the URL of the video being played) is optional and, when supplied, is added to the ad tag for contextual targeting. The EzoicInstreamAdListener reports onAdTagReady(adTagUrl) when the tag is ready and onAdFailedToLoad(error) on no fill. On an IMA ad error, call getNextAdTagUrl() to retry the next floor in the waterfall — it returns null once the waterfall is exhausted. On the IMA STARTED event, call reportImpression() (optionally passing a revenueUsd value) to record the Ezoic impression. Unlike rewarded and interstitial ads, EzoicInstreamAd is multi-use — call destroy() only when the ad unit is no longer needed, for example when the player is torn down.
Add a Native Ad
Native ads deliver ad assets (headline, image, body text, call to action) that you render inside your own layout instead of a fixed banner or full-screen format, so the ad matches the look and feel of your content.
import com.ezoic.ads.sdk.adunits.EzoicNativeAd
import com.google.android.gms.ads.nativead.NativeAdView
private var ezoicNativeAd: EzoicNativeAd? = null
EzoicNativeAd.load(this, 12345) { result ->
result.onSuccess { ad ->
ezoicNativeAd = ad
val gmaAd = ad.nativeAd ?: return@onSuccess
// Inflate your own NativeAdView layout and populate its asset views
val adView = layoutInflater.inflate(R.layout.native_ad_view, null) as NativeAdView
val headline = adView.findViewById<TextView>(R.id.ad_headline)
headline.text = gmaAd.headline
adView.headlineView = headline
adView.setNativeAd(gmaAd)
// Lets the SDK report the ad as viewed (half on screen for one second).
ad.registerView(adView)
adContainer.removeAllViews()
adContainer.addView(adView)
}.onFailure { error ->
Log.e("Ezoic", "Native load failed: ${error.message}")
}
}
Replace 12345 with your Ezoic ad unit identifier. The SDK returns the underlying Google Mobile Ads NativeAd on ad.nativeAd; populate a NativeAdView with your own styling and assign it with setNativeAd().
Then call ad.registerView(adView) with the view that shows the ad (SDK 1.17.0 and later). The SDK uses it to measure viewability: the ad is reported as viewed once at least half of it has been on screen for one continuous second. A native ad whose view is never registered is not reported as viewed.
Use the ready-made template
SDK 1.18.0 and later include EzoicNativeAdTemplateView, a complete native ad layout: an "Ad" badge, icon, headline, advertiser, media, body text and a call-to-action button. Restyle it with EzoicNativeAdStyle so it matches your app. The template fills in every asset and calls registerView for you.
import android.graphics.Color
import com.ezoic.ads.sdk.adunits.EzoicNativeAd
import com.ezoic.ads.sdk.adunits.EzoicNativeAdStyle
import com.ezoic.ads.sdk.adunits.EzoicNativeAdTemplateView
val template = EzoicNativeAdTemplateView(this).apply {
style = EzoicNativeAdStyle(
backgroundColor = Color.parseColor("#1E1E2E"),
cornerRadius = 16f,
headlineTextColor = Color.WHITE,
bodyTextColor = Color.parseColor("#CDD6F4"),
callToActionBackgroundColor = Color.parseColor("#89B4FA"),
callToActionTextColor = Color.parseColor("#1E1E2E"),
callToActionCornerRadius = 22f,
adLabelText = "Sponsored",
)
}
adContainer.addView(template)
EzoicNativeAd.load(this, 12345) { result ->
result.onSuccess { ad ->
ezoicNativeAd = ad
template.nativeAd = ad
}
}
Every property is optional. Leave a color out (null) to use the theme default, which follows light and dark mode. Sizes are in dp. Assigning a new style redraws the ad in place without reloading it. From Java, use new EzoicNativeAdStyle.Builder(); EzoicNativeAdStyle.fromMap(map) accepts the same keys with ARGB integers or CSS hex strings such as "#1E1E2E" for colors.
| Property | Default | Notes |
|---|---|---|
backgroundColor |
none (transparent) | Card background |
cornerRadius |
0 |
Card corners; content is clipped to them |
padding |
8 |
Inner padding |
headlineTextColor, headlineTextSize |
theme, 16 |
The headline is bold |
advertiserTextColor, advertiserTextSize |
theme, 12 |
|
bodyTextColor, bodyTextSize |
theme, 14 |
|
callToActionBackgroundColor, callToActionTextColor |
platform button | |
callToActionTextSize, callToActionCornerRadius |
14, 4 |
The radius applies when a background color is set |
mediaHeight |
175 |
At least 120, which Google requires for native media |
adLabelText |
"Ad" |
The ad attribution badge; a blank value falls back to "Ad" |
adLabelTextColor, adLabelBackgroundColor |
#202124, #FBBC04 |
Build your own layout
For full control over the layout, draw the ad yourself: the layout, fonts, colors, spacing, and light and dark themes are whatever you build in your NativeAdView. Style it like any other view in your app.
A complete layout, saved as res/layout/native_ad_view.xml:
<com.google.android.gms.ads.nativead.NativeAdView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/ad_attribution"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ad"
android:textColor="@android:color/white"
android:background="#E8A317"
android:paddingHorizontal="6dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<ImageView
android:id="@+id/ad_icon"
android:layout_width="40dp"
android:layout_height="40dp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/ad_headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="16sp" />
<TextView
android:id="@+id/ad_advertiser"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<com.google.android.gms.ads.nativead.MediaView
android:id="@+id/ad_media"
android:layout_width="match_parent"
android:layout_height="180dp"
android:layout_marginTop="8dp" />
<TextView
android:id="@+id/ad_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="14sp" />
<Button
android:id="@+id/ad_call_to_action"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
</LinearLayout>
</com.google.android.gms.ads.nativead.NativeAdView>
Populate it from the loaded ad. Any asset other than the headline can be missing, so hide the view for a missing asset and leave it unregistered:
fun bindNativeAd(adView: NativeAdView, gmaAd: NativeAd) {
val headline = adView.findViewById<TextView>(R.id.ad_headline)
headline.text = gmaAd.headline
adView.headlineView = headline
val media = adView.findViewById<MediaView>(R.id.ad_media)
adView.mediaView = media
val body = adView.findViewById<TextView>(R.id.ad_body)
body.isVisible = gmaAd.body != null
body.text = gmaAd.body
adView.bodyView = body.takeIf { it.isVisible }
val advertiser = adView.findViewById<TextView>(R.id.ad_advertiser)
advertiser.isVisible = gmaAd.advertiser != null
advertiser.text = gmaAd.advertiser
adView.advertiserView = advertiser.takeIf { it.isVisible }
val icon = adView.findViewById<ImageView>(R.id.ad_icon)
icon.isVisible = gmaAd.icon != null
icon.setImageDrawable(gmaAd.icon?.drawable)
adView.iconView = icon.takeIf { it.isVisible }
val cta = adView.findViewById<Button>(R.id.ad_call_to_action)
cta.isVisible = gmaAd.callToAction != null
cta.text = gmaAd.callToAction
adView.callToActionView = cta.takeIf { it.isVisible }
// Call last, after every asset view is assigned.
adView.setNativeAd(gmaAd)
}
Each ad asset comes from a NativeAd field and is shown in the view you assign to the matching NativeAdView property:
| Asset | NativeAd field |
NativeAdView property |
View |
|---|---|---|---|
| Headline | headline |
headlineView |
TextView |
| Main image or video | mediaContent |
mediaView |
MediaView |
| Body text | body |
bodyView |
TextView |
| Call to action | callToAction |
callToActionView |
Button |
| Icon | icon |
iconView |
ImageView |
| Advertiser | advertiser |
advertiserView |
TextView |
| Star rating | starRating |
starRatingView |
RatingBar |
| Store | store |
storeView |
TextView |
| Price | price |
priceView |
TextView |
Google requires the following of every native ad layout:
- Label the ad. Show an ad attribution such as "Ad" or "Sponsored", like the badge at the top of the layout above.
- Keep assets inside the ad view. Every asset view must sit inside the
NativeAdView, and each one you show must be assigned to its property. Unassigned views don't respond to taps. - Show the media in a
MediaView. Give it at least 120x120dp so images and video display. - Leave the AdChoices icon visible. Google Mobile Ads adds it to a corner of the
NativeAdView; don't cover it.
NativeAdView. Clear any issues it lists before you release. Google's native ads guide for Ad Manager covers the layout rules in full.
Release the ad when the hosting Activity or Fragment is destroyed:
override fun onDestroy() {
ezoicNativeAd?.destroy()
ezoicNativeAd = null
super.onDestroy()
}
The EzoicNativeAdListener reports onNativeAdImpression, onNativeAdClicked, onNativeAdOpened, and onNativeAdClosed. Java apps load with an EzoicNativeAdLoadListener and can extend EzoicNativeAdListenerAdapter.
AndroidManifest.xml
Add your Google Mobile Ads application ID to AndroidManifest.xml:
In most Ezoic mobile app integrations, this app ID is provided by Ezoic and can be found in your Ezoic dashboard. Use the value assigned to your app unless your Ezoic representative gives you a different Google Mobile Ads application ID.
<manifest>
<application>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX" />
</application>
</manifest>
If your app targets Android 12 or higher and uses the advertising ID, add the advertising ID permission:
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
app-ads.txt
Host an app-ads.txt file at the root of the developer website listed on your app's Play Store page (for example, https://example.com/app-ads.txt). It authorizes the buyers and exchanges that may sell your inventory; a missing or incomplete file causes most programmatic demand to be filtered out. Ezoic provides the required entries — confirm the file is published and current.
Unlike iOS, Android has no App Tracking Transparency or SKAdNetwork requirements. The advertising ID permission above is the main identifier-related step, and Google Mobile Ads merges its own required entries through the manifest automatically.
Privacy and Consent
Built-in consent dialog
Starting with SDK 1.13.0, the SDK includes an IAB TCF 2.4 consent management platform (CMP ID 299). It only acts for users in GDPR regions; elsewhere no dialog is shown and ads load as before.
The dialog is used only when both of these are true:
- Ezoic has enabled the consent dialog for your domain. This is a server-side setting.
cmpEnabledistrueinEzoicConfiguration. This is the default.
cmpEnabled = false (Java: .cmpEnabled(false)). See Using your own CMP.
Call presentConsentIfRequired once per process from your first Activity's onCreate, right after EzoicAds.initialize. It is safe to call before initialization finishes: it waits for the init response. If that Activity is recreated or finishes while the dialog loads, the dialog opens on whichever Activity is in front (waiting up to 10 seconds for one to resume). Calling it again is harmless: you get AlreadyPresenting while a dialog is in flight and AlreadyDecided once a valid decision is stored.
You must also add a "Privacy settings" button or menu item that is always reachable and calls presentConsentSettings. TCF policy requires users to be able to reopen the dialog and withdraw consent at any time.
import com.ezoic.ads.sdk.privacy.cmp.ConsentOutcome
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EzoicAds.instance.presentConsentIfRequired(this) { outcome ->
when (outcome) {
is ConsentOutcome.Decided -> Log.d("Ezoic", "User chose ${outcome.decision}")
is ConsentOutcome.Failed -> Log.w("Ezoic", "Consent UI failed: ${outcome.error.message}")
else -> Unit // NotRequired, AlreadyDecided, Dismissed, AlreadyPresenting
}
}
}
// "Privacy settings" button (required): reopen the dialog with the user's stored choices
privacyButton.setOnClickListener {
EzoicAds.instance.presentConsentSettings(this) { outcome -> /* ... */ }
}
Java apps use the …WithListener overloads, which take a ConsentOutcomeListener:
EzoicAds.getInstance().presentConsentIfRequiredWithListener(this, outcome -> {
if (outcome instanceof ConsentOutcome.Decided) {
Log.d("Ezoic", "User chose " + ((ConsentOutcome.Decided) outcome).getDecision());
}
});
privacyButton.setOnClickListener(v ->
EzoicAds.getInstance().presentConsentSettingsWithListener(this, outcome -> { /* ... */ }));
cmpEnabled = true and never call presentConsentIfRequired, ad loads for GDPR-region users will fail once Ezoic enables the consent dialog for your domain. Either add the call above or set cmpEnabled = false if you run your own CMP.
Consent outcomes
Callbacks run on the main thread. ConsentOutcome tells you what happened:
| Outcome | When |
|---|---|
NotRequired |
GDPR doesn't apply, the built-in CMP is disabled (cmpEnabled = false or by Ezoic), another CMP owns consent, or consent is managed by the app (see Setting consent manually) |
AlreadyDecided |
A still-valid decision is stored; no dialog shown |
Decided(decision) |
The user chose ACCEPT_ALL, REJECT_ALL or CUSTOM; the choice is saved |
Dismissed |
The dialog closed without a choice; ads stay gated for this session. This can happen without user action (the system finished the screen, it failed to start, or resetConsent() ran meanwhile) |
AlreadyPresenting |
A consent dialog is already on screen, or one is being prepared with nothing on screen yet |
Failed(error) |
The dialog couldn't be shown (for example a network error, or no Activity in front within 10 seconds). Failed(NotInitialized) means it was called before initialize, or init failed |
Other consent calls:
presentConsentSettingsalways reopens the dialog in GDPR regions, even if the user already decided. It returnsNotRequiredoutside GDPR regions, whencmpEnabledisfalse, when another CMP is present, or when consent is managed by the app. If the dialog can't be loaded (for example offline) and a still-valid decision is stored, it returnsAlreadyDecided.isConsentRequired(Boolean?) isnulluntil the init request completes, or when the server sent no consent information. It istruewhenever GDPR applies and the built-in CMP is in charge, including after the user has decided, andfalseotherwise.resetConsent()deletes the stored decision so the dialog shows again. Ads are gated again until the user decides. Keys written by another CMP are left alone.
Decisions are written as standard IABTCF_* keys to the app's default SharedPreferences, with the Google Additional Consent string in IABTCF_AddtlConsent, so Prebid, Google Ad Manager, and other IAB-aware SDKs read them as usual. A stored decision is asked for again after the publisher's consent interval (at most 390 days), when the TCF policy version changes, or when the vendor list moves more than 2 versions ahead.
How ad loads wait for consent
In GDPR regions, while the built-in CMP is in charge and no decision is stored, ad loads wait for the user's decision:
- While the consent dialog is loading or on screen, ad loads wait up to 5 minutes in total per dialog, counted from
presentConsentIfRequiredand shared by all waiting ad loads. - While no dialog is in progress, the open dialog is covered by another Activity, or the app is in the background, ad loads wait up to 10 seconds.
If the wait runs out, the ad load fails with EzoicError.ConsentRequired.
If the dialog can't be shown (Failed), the SDK fails open: ads proceed with IABTCF_gdprApplies=1 and no TC string, which Google treats as limited ads and many Prebid bidders skip. An earlier TC string, if any, is cleared when the stored decision was stale. Failed(NotInitialized) writes nothing.
Using your own CMP
Set cmpEnabled = false (Kotlin) or .cmpEnabled(false) (Java Builder). The SDK then reads your CMP's IABTCF_* keys as before. The built-in CMP also stays out of the way automatically if it finds IABTCF_CmpSdkID set to another CMP's ID.
val config = EzoicConfiguration(domain = "example.com", cmpEnabled = false)
Whichever CMP writes them, the SDK automatically reads consent signals from SharedPreferences:
- TCF v2 consent is read from
IABTCF_*keys. - GPP consent is read from
IABGPP_*keys. - US Privacy consent is read from the standard IAB privacy key.
Setting consent manually
You can also set consent in code. Manual consent and the built-in CMP are mutually exclusive: once setGDPRConsent is called, or when autoReadConsent is false, the built-in CMP doesn't gate ad loads, show its dialog, or write IABTCF_* keys, and resetConsent() leaves the keys alone.
The setGDPRConsent value lasts for the current process only. Call setGDPRConsent before EzoicAds.initialize on every launch, or set cmpEnabled = false. Otherwise each cold start begins with the built-in CMP in charge until your call lands, and it can gate ad loads, show its dialog, and write IABTCF_* keys. setGDPRConsent doesn't write your consent to IABTCF_* keys in SharedPreferences for Google Mobile Ads or other SDKs in the app, so your own CMP must write those keys.
EzoicAds.instance.setGDPRConsent(
applies = true,
consentString = "TCF_CONSENT_STRING"
)
EzoicAds.instance.setGPPConsent(
gppString = "GPP_STRING",
sectionIds = "7"
)
EzoicAds.instance.setSubjectToCOPPA(true)
// Then initialize
EzoicAds.instance.initialize(this, config) { result -> /* ... */ }
Pageview Tracking
Ezoic reports app traffic per screen, the way it reports a website per URL. Apps have no URLs, so the SDK gives each screen one:
https://<your domain>/<application id>/<screen label>
That URL is what per-page reporting and placement optimization key on, so the screen label is what you will see in the Ezoic dashboard.
Automatic tracking
The SDK automatically tracks a pageview for every Activity resume, Fragment resume, and view-hosted Navigation destination change after initialization. Without any code from you, the screen label defaults to the Activity or Fragment class name (or the navigation route), for example https://example.com/com.example.app/SettingsFragment.
If your app uses Compose Navigation, opt in with your NavController:
val navController = rememberNavController()
EzoicAds.instance.TrackNavigation(navController)
For WebView-heavy apps, use EzoicWebViewClient so page loads are tracked as pageviews, labeled with the page's URL path:
webView.webViewClient = EzoicWebViewClient()
Labeling screens
To name screens the way you want to see them in reporting, call trackPageview(screen) when the user lands on a screen, typically from onResume(). Available in SDK 1.12.0 and later.
override fun onResume() {
super.onResume()
EzoicAds.instance.trackPageview("members area")
// reported as https://example.com/com.example.app/members-area
}
EzoicAds.getInstance().trackPageview("members/profile");
// reported as https://example.com/com.example.app/members/profile
Labels are free text. Use / for hierarchy (members/profile); spaces become -, punctuation is dropped, and case is kept. The label is also attached to every ad request on that screen, so impressions and pageviews report under the same page.
A labeled trackPageview(screen) takes precedence over the automatic pageview for the same navigation, so you can label only the screens you care about without double counting. Calling trackPageview() with no label still records a pageview under the current screen label.
Label your launch screen early. If you label the first screen, call trackPageview(screen) before EzoicAds.initialize or from its completion callback. A launch-screen label that arrives more than about 1 second after initialization finishes (outside a consent dialog) records a second launch pageview.
Manual tracking only
If you would rather the SDK never guess, disable automatic tracking and label every screen yourself:
val config = EzoicConfiguration(
domain = "example.com",
autoTrackPageviews = false
)
Java Compatibility
The Android SDK is written in Kotlin, but the public API is designed for Java interoperability. Java apps can use the EzoicConfiguration.Builder, callback interfaces, Java-friendly banner size constants, and EzoicBannerViewAdapter.
Troubleshooting
SDK Not Initializing
- Confirm the configured domain matches your Ezoic dashboard.
- Confirm the app has network access.
- Enable
debugEnabled = trueand check Logcat forEzoicAdslogs.
Ads Not Loading
- Initialize the SDK before calling
loadAd(). - Confirm the Ezoic ad unit identifier is configured in Ezoic.
- Confirm the Google Mobile Ads application ID is present in
AndroidManifest.xml. - Check consent configuration if traffic is subject to privacy regulations.
- If ad loads fail with
EzoicError.ConsentRequired, the built-in consent dialog is waiting for a decision. CallpresentConsentIfRequiredafter initializing, or setcmpEnabled = falseif you run your own CMP. See Built-in consent dialog.