Android SDK: Complete Kotlin SDK documentation # API reference > Complete public API of the Qartvelo Ads Android SDK 0.3.0 (package com.qartvelo.sdk). Package `com.qartvelo.sdk`, version `0.3.0` (`QartveloAds.SDK_VERSION`). Every method is safe to call from any thread, never throws and delivers callbacks on the main thread. `@JvmStatic` and `@JvmOverloads` make the API callable from Java as `QartveloAds.initialize(...)`. ## QartveloAds [Section titled “QartveloAds”](#qartveloads) ```kotlin object QartveloAds { const val SDK_VERSION: String = "0.3.0" fun initialize( context: Context, appKey: String, options: QartveloAdsOptions = QartveloAdsOptions(), listener: QartveloAdsInitListener? = null, ) fun isInitialized(): Boolean fun loadInterstitial(placementId: String, listener: QartveloAdsListener? = null) fun showInterstitial(activity: Activity, placementId: String, listener: QartveloAdsListener? = null) fun isInterstitialReady(placementId: String): Boolean fun loadRewarded(placementId: String, listener: QartveloAdsListener? = null) fun showRewarded(activity: Activity, placementId: String, listener: QartveloAdsListener? = null) fun isRewardedReady(placementId: String): Boolean fun addEventListener(listener: QartveloAdsListener) fun removeEventListener(listener: QartveloAdsListener) fun setLogLevel(level: QartveloAdsLogLevel) fun setPrivacy(privacy: QartveloAdsPrivacy) fun registerFallbackAdapter(adapter: FallbackAdapter) } ``` | Method | Description | | ------------------------------------------ | ---------------------------------------------------------------------------------------------- | | `initialize` | Starts the SDK once per process. See [Initialization](/android/initialization/) | | `isInitialized` | `true` once the first initialization attempt finished (also offline) | | `loadInterstitial` / `loadRewarded` | Loads an ad for a placement code; ends with `onLoaded` or `onLoadFailed` | | `showInterstitial` / `showRewarded` | Shows the best ready ad; ends with `onDismissed`, `onNoAdAvailable` or `onLoadFailed` | | `isInterstitialReady` / `isRewardedReady` | Whether a show would display an ad now | | `addEventListener` / `removeEventListener` | Global observers for every placement, banners included | | `setLogLevel` | Overrides `options.logLevel` at runtime | | `setPrivacy` | Privacy signals forwarded to the fallback adapter. See [Privacy](/guides/privacy/) | | `registerFallbackAdapter` | Use a custom fallback network. See [Custom fallback adapter](/guides/custom-fallback-adapter/) | ## QartveloAdsOptions [Section titled “QartveloAdsOptions”](#qartveloadsoptions) ```kotlin data class QartveloAdsOptions( val admobFallback: Boolean = true, val requestTimeoutMs: Long = 800, val testMode: Boolean = false, val testForceNoFill: Boolean = false, val logLevel: QartveloAdsLogLevel = QartveloAdsLogLevel.ERROR, val baseUrl: String = DEFAULT_BASE_URL, // "https://ads.qartvelo.com/" val admobAdUnits: Map = emptyMap(), ) ``` ## QartveloAdsBannerView [Section titled “QartveloAdsBannerView”](#qartveloadsbannerview) ```kotlin class QartveloAdsBannerView(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout { var placementId: String? // XML: app:qartvelo_placementId var listener: QartveloAdsListener? fun load() // idempotent fun destroy() // detaches; the loaded banner stays cached } ``` ## Listeners [Section titled “Listeners”](#listeners) ```kotlin interface QartveloAdsListener { fun onLoaded(info: QartveloAdsAdInfo) {} fun onLoadFailed(placementId: String, error: QartveloAdsError) {} fun onShown(info: QartveloAdsAdInfo) {} fun onImpression(info: QartveloAdsAdInfo) {} fun onClicked(info: QartveloAdsAdInfo) {} fun onDismissed(info: QartveloAdsAdInfo) {} fun onReward(info: QartveloAdsAdInfo, reward: QartveloAdsReward) {} fun onFallbackStarted(placementId: String, format: AdFormat, reason: String) {} fun onNoAdAvailable(placementId: String, format: AdFormat) {} } fun interface QartveloAdsInitListener { fun onInitialized(success: Boolean, error: QartveloAdsError?) } ``` ## Models [Section titled “Models”](#models) ```kotlin enum class QartveloAdsLogLevel { NONE, ERROR, INFO, DEBUG } enum class AdFormat { BANNER, INTERSTITIAL, REWARDED } enum class AdSource { QARTVELO, ADMOB } data class QartveloAdsAdInfo( val placementId: String, val format: AdFormat, val source: AdSource, val campaignId: String? = null, val creativeId: String? = null, ) data class QartveloAdsReward(val type: String = "reward", val amount: Int = 1) enum class QartveloAdsErrorCode { NOT_INITIALIZED, INVALID_PLACEMENT, NETWORK_ERROR, TIMEOUT, NO_FILL, CREATIVE_FAILED, AD_EXPIRED, SHOW_FAILED, ALREADY_SHOWING, INTERNAL_ERROR, } data class QartveloAdsError(val code: QartveloAdsErrorCode, val message: String) data class QartveloAdsPrivacy( val consentGiven: Boolean? = null, // null = unknown val childDirected: Boolean? = null, val underAgeOfConsent: Boolean? = null, ) ``` ## Fallback seam [Section titled “Fallback seam”](#fallback-seam) Package `com.qartvelo.sdk.fallback`: `FallbackAdapter`, `FallbackSettings`, `FallbackLoadCallback`, `FallbackShowCallback`, `FallbackBannerCallback`, `FallbackBanner`. Documented in [Custom fallback adapter](/guides/custom-fallback-adapter/). ## Java [Section titled “Java”](#java) ```java QartveloAds.initialize(this, "app_xxxxxxxxxxxxxxxxxxxxxxxx", new QartveloAdsOptions(true, 800L, BuildConfig.DEBUG, false, QartveloAdsLogLevel.ERROR, "https://ads.qartvelo.com/", Collections.emptyMap())); QartveloAds.loadInterstitial("game_end"); QartveloAds.showInterstitial(activity, "game_end"); ``` `QartveloAdsOptions` has `@JvmOverloads`, so leading arguments can be passed and the rest default (for example `new QartveloAdsOptions(true, 800L, BuildConfig.DEBUG)`). # Banner > Add a QartveloAdsBannerView in XML or code, listen to its events and manage its lifecycle. ## In XML [Section titled “In XML”](#in-xml) res/layout/activity_main.xml ```xml ``` MainActivity.kt ```kotlin class MainActivity : AppCompatActivity() { private lateinit var banner: QartveloAdsBannerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) banner = findViewById(R.id.banner) banner.listener = object : QartveloAdsListener { override fun onLoaded(info: QartveloAdsAdInfo) { // info.source is AdSource.QARTVELO or AdSource.ADMOB } override fun onNoAdAvailable(placementId: String, format: AdFormat) { banner.visibility = View.GONE } } banner.load() // idempotent } override fun onDestroy() { banner.destroy() // the loaded banner stays cached for the next view super.onDestroy() } } ``` ## In code [Section titled “In code”](#in-code) ```kotlin val banner = QartveloAdsBannerView(this).apply { placementId = "home_banner" layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT) } container.addView(banner) banner.load() ``` ## In Jetpack Compose [Section titled “In Jetpack Compose”](#in-jetpack-compose) ```kotlin @Composable fun QartveloBanner(placementId: String, modifier: Modifier = Modifier) { AndroidView( modifier = modifier.fillMaxWidth(), factory = { context -> QartveloAdsBannerView(context).apply { this.placementId = placementId load() } }, onRelease = { it.destroy() }, ) } ``` ## Behaviour [Section titled “Behaviour”](#behaviour) * **One controller per placement code.** A re-created view (rotation, list recycling, Compose recomposition, React Native re-render) that calls `load()` again re-attaches to the loaded banner; no new request is made. * **Refresh** happens no faster than the placement’s `banner_refresh_seconds` (at least 30 s), and only while the view is attached and visible. It pauses when the Activity stops or the view is hidden or detached. * **Fallback**: on Qartvelo Ads no-fill, timeout, error or creative failure the view renders your AdMob anchored adaptive banner instead. A visible AdMob banner refreshes itself (AdMob settings apply); a later Qartvelo Ads fill replaces it. * **One visible banner per placement code.** Use distinct placement codes for banners that are on screen at the same time. * **No leaks.** The view never holds an Activity after it is detached; AdMob views are re-parented through a context wrapper. * Qartvelo Ads banner creatives keep their aspect ratio within the view width. Supported creative sizes are 320x50, 320x100, 300x250, 468x60 and 728x90. ## Banner events [Section titled “Banner events”](#banner-events) Banners use the same [`QartveloAdsListener`](/android/events/): `onLoaded`, `onLoadFailed`, `onShown`, `onImpression`, `onClicked`, `onFallbackStarted` and `onNoAdAvailable`. `onDismissed` and `onReward` do not apply. ## Errors [Section titled “Errors”](#errors) | Situation | Callback | | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `placementId` not set | `onLoadFailed(INVALID_PLACEMENT)` | | `load()` before `QartveloAds.initialize` | `onLoadFailed(NOT_INITIALIZED)` | | The code belongs to a non-banner placement, or does not exist | `onLoadFailed(INVALID_PLACEMENT)` | | No source has an ad | `onNoAdAvailable`, then `onLoadFailed` with the Qartvelo Ads failure (`NO_FILL`, `TIMEOUT`, `NETWORK_ERROR` or `CREATIVE_FAILED`) | After a failure the banner keeps its refresh schedule and tries again; you do not need to call `load()` again. # Events and errors > QartveloAdsListener callbacks, global observers, event ordering and error codes. All callbacks run on the **main thread**. Every method of `QartveloAdsListener` has an empty default body, so override only what you need. ```kotlin interface QartveloAdsListener { fun onLoaded(info: QartveloAdsAdInfo) {} fun onLoadFailed(placementId: String, error: QartveloAdsError) {} fun onShown(info: QartveloAdsAdInfo) {} fun onImpression(info: QartveloAdsAdInfo) {} fun onClicked(info: QartveloAdsAdInfo) {} fun onDismissed(info: QartveloAdsAdInfo) {} fun onReward(info: QartveloAdsAdInfo, reward: QartveloAdsReward) {} fun onFallbackStarted(placementId: String, format: AdFormat, reason: String) {} fun onNoAdAvailable(placementId: String, format: AdFormat) {} } ``` ## Callbacks [Section titled “Callbacks”](#callbacks) | Callback | When | | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | `onLoaded(info)` | A load finished and a source is ready. `info.source` is the one that will be shown | | `onLoadFailed(placementId, error)` | A load ended without an ad, or a show-time error (`ALREADY_SHOWING`, `SHOW_FAILED`) | | `onFallbackStarted(placementId, format, reason)` | Qartvelo Ads could not serve. `reason`: `no_fill`, `timeout`, `error`, `creative_failed`, `disabled` | | `onNoAdAvailable(placementId, format)` | No source can serve. Before `onLoadFailed` on loads; alone on shows | | `onShown(info)` | The creative is on screen | | `onImpression(info)` | The impression was counted (one per ad) | | `onClicked(info)` | First tap on the ad, recorded before the browser opens | | `onReward(info, reward)` | Rewarded completion, at most once per show | | `onDismissed(info)` | The full-screen ad closed | ## Ordering guarantees [Section titled “Ordering guarantees”](#ordering-guarantees) * A **load** ends with exactly one of `onLoaded` or `onLoadFailed`. `onFallbackStarted` may precede either. * A **show** produces `onShown` and `onImpression` when the creative is on screen, `onClicked` at most once, `onReward` at most once (rewarded only), and ends with exactly one of `onDismissed`, `onNoAdAvailable` or `onLoadFailed`. ## Global observers [Section titled “Global observers”](#global-observers) Per-call listeners receive the events of that call. Global observers receive every event of every placement and format, including banners. Use them for analytics: ```kotlin val analytics = object : QartveloAdsListener { override fun onImpression(info: QartveloAdsAdInfo) { log("ad_impression", info.placementId, info.source.name) } override fun onFallbackStarted(placementId: String, format: AdFormat, reason: String) { log("ad_fallback", placementId, reason) } } QartveloAds.addEventListener(analytics) // later QartveloAds.removeEventListener(analytics) ``` ## Ad info [Section titled “Ad info”](#ad-info) ```kotlin data class QartveloAdsAdInfo( val placementId: String, val format: AdFormat, // BANNER, INTERSTITIAL, REWARDED val source: AdSource, // QARTVELO or ADMOB val campaignId: String? = null, // "cmp_12" for Qartvelo Ads ads, null for AdMob val creativeId: String? = null, // "cr_34" for Qartvelo Ads ads, null for AdMob ) ``` ## Error codes [Section titled “Error codes”](#error-codes) `QartveloAdsError(code: QartveloAdsErrorCode, message: String)` | Code | Meaning | | ------------------- | ------------------------------------------------------------------------------------------ | | `NOT_INITIALIZED` | `initialize` was not called, the app key is empty, or the backend rejected the key/package | | `INVALID_PLACEMENT` | Empty or unknown placement code, or a placement used with the wrong format | | `NETWORK_ERROR` | The backend could not be reached and no fallback was available | | `TIMEOUT` | The backend did not answer in time and no fallback was available | | `NO_FILL` | Neither Qartvelo Ads nor the fallback had an ad | | `CREATIVE_FAILED` | The creative could not be downloaded or decoded | | `AD_EXPIRED` | The ad passed its expiry before it was shown | | `SHOW_FAILED` | The ad could not be displayed (for example the Activity was finishing) | | `ALREADY_SHOWING` | Another full-screen ad is on screen | | `INTERNAL_ERROR` | Unexpected failure inside the SDK. The SDK never throws into your code | The `message` is for logs only; branch on `code`. # Initialization > Initialize the SDK once, configure options, and pass privacy signals. Initialize once, as early as possible, normally in `Application.onCreate`. The call returns immediately; networking happens in the background. MyApp.kt ```kotlin import com.qartvelo.sdk.QartveloAds import com.qartvelo.sdk.QartveloAdsLogLevel import com.qartvelo.sdk.QartveloAdsOptions import com.qartvelo.sdk.QartveloAdsPrivacy class MyApp : Application() { override fun onCreate() { super.onCreate() // Optional, any time: privacy signals from your consent flow (see Privacy guide) QartveloAds.setPrivacy(QartveloAdsPrivacy(consentGiven = consentOrNull())) QartveloAds.initialize( context = this, appKey = "app_xxxxxxxxxxxxxxxxxxxxxxxx", options = QartveloAdsOptions( testMode = BuildConfig.DEBUG, logLevel = if (BuildConfig.DEBUG) QartveloAdsLogLevel.DEBUG else QartveloAdsLogLevel.ERROR, ), ) { success, error -> // Main thread. success == false still leaves the SDK usable. } } } ``` ## Options [Section titled “Options”](#options) `QartveloAdsOptions` is a data class; every field has a default. | Option | Type | Default | Meaning | | ------------------ | --------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `admobFallback` | `Boolean` | `true` | Allow the AdMob adapter to serve your units when Qartvelo Ads cannot | | `requestTimeoutMs` | `Long` | `800` | Qartvelo Ads time budget before falling back. A per-placement value from the dashboard wins. Clamped to 100..10000 | | `testMode` | `Boolean` | `false` | Non-billable test ads; AdMob uses Google’s test units. See [Test mode](/get-started/test-mode/) | | `testForceNoFill` | `Boolean` | `false` | Force Qartvelo Ads `no_fill` to exercise the fallback | | `logLevel` | `QartveloAdsLogLevel` | `ERROR` | `NONE`, `ERROR`, `INFO`, `DEBUG`. Logcat tag `QartveloAds` | | `baseUrl` | `String` | `https://ads.qartvelo.com/` | API origin. Change only for a self-hosted or local backend | | `admobAdUnits` | `Map` | empty | Placement code to your AdMob ad unit id. Wins over the dashboard value | ## Behaviour [Section titled “Behaviour”](#behaviour) * **Idempotent.** Only the first call counts. Later calls are ignored (a different app key or options are logged at `INFO`), but their listener still receives the first result. Restart the process to change options. * **Only the application context is kept.** Passing an Activity is safe. * **Never blocks loads on a slow backend.** With a cached remote config, loads start immediately and the Qartvelo Ads request obtains its session within its own timeout. Only the very first start (no cache yet) holds loads for up to `requestTimeoutMs` while the first configuration arrives. * **Failure is not fatal.** If initialization fails (offline, backend down, key rejected), the listener gets `success = false`, the SDK runs on its cached config and can still fall back to AdMob. The session is retried lazily and throttled, so an outage never blocks your UI. * `QartveloAds.isInitialized()` becomes `true` when the first attempt has finished, successfully or not. * Loads before `initialize` was called at all fail with `NOT_INITIALIZED`. ## Initialization listener [Section titled “Initialization listener”](#initialization-listener) ```kotlin fun interface QartveloAdsInitListener { fun onInitialized(success: Boolean, error: QartveloAdsError?) } ``` Typical errors: `NETWORK_ERROR` and `TIMEOUT` (backend unreachable), `NOT_INITIALIZED` (empty or rejected app key, wrong package name, app not approved outside test mode). ## Runtime settings [Section titled “Runtime settings”](#runtime-settings) ```kotlin QartveloAds.setLogLevel(QartveloAdsLogLevel.DEBUG) // overrides options.logLevel QartveloAds.setPrivacy(QartveloAdsPrivacy(childDirected = true)) ``` Both can be called before or after `initialize`. See the [Privacy guide](/guides/privacy/) for what the privacy signals do. ## App key safety [Section titled “App key safety”](#app-key-safety) The app key is public and identifies your app; the backend accepts it only together with the package name registered in the dashboard. Do not put the SDK secret in your app. # Installation > Add the Qartvelo Ads Android SDK and the optional AdMob adapter to your Gradle build. | Artifact | Contents | Required | | ------------------------------ | ------------------------------------------------------------------- | -------- | | `com.qartvelo.ads:core:0.3.0` | API client, config cache, rendering, event delivery, banner view | Yes | | `com.qartvelo.ads:admob:0.3.0` | Fallback adapter for Google Mobile Ads (`play-services-ads` 25.4.0) | Optional | ## Requirements [Section titled “Requirements”](#requirements) * `minSdk` 23 or higher, `compileSdk` 35 or higher * Java 17 toolchain, AndroidX * Core brings OkHttp 4.12, Media3 ExoPlayer 1.8 and AndroidX core. It does not depend on Google Mobile Ads; only the adapter does, as a normal (never shaded) dependency, so a newer `play-services-ads` in your app wins. ## Add the repository and dependencies [Section titled “Add the repository and dependencies”](#add-the-repository-and-dependencies) The SDK is published on JitPack (built from the [Qartvelo-com/ads](https://github.com/Qartvelo-com/ads) tags). No account or token is needed. * Kotlin DSL settings.gradle.kts ```kotlin dependencyResolutionManagement { repositories { google() mavenCentral() maven("https://jitpack.io") { content { includeGroup("com.qartvelo.ads") } } } } ``` app/build.gradle.kts ```kotlin dependencies { implementation("com.qartvelo.ads:core:0.3.0") // Optional: fall back to your own AdMob ad units. Leave out to run Qartvelo Ads only. implementation("com.qartvelo.ads:admob:0.3.0") } ``` * Groovy settings.gradle ```groovy dependencyResolutionManagement { repositories { google() mavenCentral() maven { url "https://jitpack.io" content { includeGroup("com.qartvelo.ads") } } } } ``` app/build.gradle ```groovy dependencies { implementation "com.qartvelo.ads:core:0.3.0" implementation "com.qartvelo.ads:admob:0.3.0" // optional } ``` * Version catalog gradle/libs.versions.toml ```toml [versions] qartvelo-ads = "0.3.0" [libraries] qartvelo-ads-core = { module = "com.qartvelo.ads:core", version.ref = "qartvelo-ads" } qartvelo-ads-admob = { module = "com.qartvelo.ads:admob", version.ref = "qartvelo-ads" } ``` app/build.gradle.kts ```kotlin dependencies { implementation(libs.qartvelo.ads.core) implementation(libs.qartvelo.ads.admob) } ``` The adapter is discovered at runtime by class name, so adding or removing `com.qartvelo.ads:admob` needs no code change. Without it, the SDK works normally and reports `onNoAdAvailable` when Qartvelo Ads has no ad. ## Manifest [Section titled “Manifest”](#manifest) `core` merges the `INTERNET` permission and its full-screen ad Activity automatically. Nothing else is needed for Qartvelo Ads. If you include the AdMob adapter, Google requires **your own** AdMob App ID, or the Google Mobile Ads SDK crashes the app at start-up: app/src/main/AndroidManifest.xml ```xml ``` For development you can use Google’s sample App ID `ca-app-pub-3940256099942544~3347511713`. ## R8 / ProGuard [Section titled “R8 / ProGuard”](#r8--proguard) Both libraries ship consumer rules. Minified release builds need no extra configuration. ## Local backend (optional) [Section titled “Local backend (optional)”](#local-backend-optional) If you run your own Qartvelo Ads backend on your machine for development, the emulator reaches it at `http://10.0.2.2:8000/`. Allow cleartext HTTP for that host in **debug builds only**: app/src/debug/res/xml/network_security_config.xml ```xml 10.0.2.2 localhost ``` app/src/debug/AndroidManifest.xml ```xml ``` Production always uses HTTPS (`https://ads.qartvelo.com/`, the default). Next: [Initialization](/android/initialization/). # Interstitial > Preload and show full-screen interstitial ads at natural breaks. Interstitials are full-screen image or video ads shown at natural pauses: between levels, after finishing an article, when leaving a screen. ## Load early, show later [Section titled “Load early, show later”](#load-early-show-later) ```kotlin // When the level starts QartveloAds.loadInterstitial("game_end", object : QartveloAdsListener { override fun onLoaded(info: QartveloAdsAdInfo) { // ready; info.source is QARTVELO or ADMOB } override fun onLoadFailed(placementId: String, error: QartveloAdsError) { // nothing ready; error.code tells you why } }) // When the level ends if (QartveloAds.isInterstitialReady("game_end")) { QartveloAds.showInterstitial(activity, "game_end", object : QartveloAdsListener { override fun onDismissed(info: QartveloAdsAdInfo) = continueGame() override fun onNoAdAvailable(placementId: String, format: AdFormat) = continueGame() override fun onLoadFailed(placementId: String, error: QartveloAdsError) = continueGame() }) } else { continueGame() } ``` Always continue your flow from **every** terminal callback of the show: `onDismissed`, `onNoAdAvailable` and `onLoadFailed`. Exactly one of them ends each show. ## Load behaviour [Section titled “Load behaviour”](#load-behaviour) 1. A still-valid cached Qartvelo Ads ad completes the load immediately. 2. Otherwise the SDK requests Qartvelo Ads and, when a fallback is possible, **preloads AdMob in parallel**. 3. On a fill, the creative is downloaded and validated, then `onLoaded(source = QARTVELO)`. 4. On no-fill, timeout, network error, creative failure or kill switch: `onFallbackStarted(reason)`, then `onLoaded(source = ADMOB)` when AdMob is ready, or `onNoAdAvailable` followed by `onLoadFailed` if AdMob has nothing either. 5. If the Qartvelo Ads creative is still downloading 1.5 s after the request timeout, a ready AdMob ad completes the load; the Qartvelo Ads download continues and is used by the next show. Concurrent loads of the same placement share one request. ## Show behaviour [Section titled “Show behaviour”](#show-behaviour) * Show prefers a valid Qartvelo Ads ad, then a ready AdMob ad, else `onNoAdAvailable`. * If a Qartvelo Ads creative fails to render before anything was displayed, a ready AdMob ad is shown instead. * Only one full-screen ad can be on screen; a second show fails with `onLoadFailed(ALREADY_SHOWING)`. * A Qartvelo Ads ad is shown at most once and never after its expiry (30 minutes after the request). * Rotation and Activity re-creation do not restart the ad or repeat events. * Tapping the ad records the click, then opens the advertiser’s URL in the browser. ## Listeners [Section titled “Listeners”](#listeners) Pass a listener to `showInterstitial`. Without one, show events go to the last load listener of that placement while your code still references it (load listeners are held weakly so a destroyed Activity is never retained), plus any global observers registered with `QartveloAds.addEventListener`. ## Good practice [Section titled “Good practice”](#good-practice) * Preload as soon as you know an ad might be shown, not right before showing. * Do not show interstitials on app launch, in the middle of gameplay, or right after another full-screen ad. * Use a frequency cap on the placement (for example 1 per `hour`) rather than counting in code. * After a show, load again for the next opportunity. # Release checklist > Everything to verify before shipping a build with Qartvelo Ads to production. * [ ] The app and its placements are **approved** in the dashboard and placements are `active`. * [ ] The release `applicationId` equals the package name registered for the app. * [ ] `testMode = false` and `testForceNoFill = false` in release builds (for example `testMode = BuildConfig.DEBUG`). * [ ] `baseUrl` is the default `https://ads.qartvelo.com/` (or your production HTTPS origin); no cleartext network config in the release manifest. * [ ] Your **own** AdMob App ID is in the manifest and your own ad unit ids are set per placement (dashboard or `admobAdUnits`); no Google test ids in production. See [AdMob fallback](/guides/admob-fallback/). * [ ] Consent is collected by your CMP where required and passed with `QartveloAds.setPrivacy`. See [Privacy](/guides/privacy/). * [ ] `logLevel` is `ERROR` or `NONE`. * [ ] A minified (R8) release build was tested once on a real device. Consumer rules are bundled. * [ ] `banner.destroy()` is called when banner screens are destroyed. * [ ] Your game or app flow continues from every terminal show callback (`onDismissed`, `onNoAdAvailable`, `onLoadFailed`). * [ ] Rewards are granted only from `onReward` (or `result.rewarded` in React Native), never from `onDismissed`. * [ ] Your Google Play Data safety form reflects what the SDK sends. See [Privacy](/guides/privacy/#google-play-data-safety). After release, watch the **Fill rate** and **Fallbacks** columns in your reports. Many fallbacks with a healthy fill rate usually mean requests time out: check a debug build’s `onFallbackStarted` reasons and consider raising the placement’s request timeout. # Rewarded > Show rewarded videos and grant rewards exactly once, only after confirmed completion. Rewarded placements play a video the user opts into in exchange for an in-app reward (coins, an extra life, a hint). ```kotlin // Preload, for example when the shop screen opens QartveloAds.loadRewarded("reward_coins") // The user taps "Watch a video for 50 coins" QartveloAds.showRewarded(activity, "reward_coins", object : QartveloAdsListener { override fun onReward(info: QartveloAdsAdInfo, reward: QartveloAdsReward) { grantCoins(50) // exactly once, only after completion } override fun onDismissed(info: QartveloAdsAdInfo) = resumeGame() override fun onNoAdAvailable(placementId: String, format: AdFormat) = showNoVideoMessage() override fun onLoadFailed(placementId: String, error: QartveloAdsError) = showNoVideoMessage() }) ``` Use `QartveloAds.isRewardedReady("reward_coins")` to enable or hide the “watch a video” button. ## Reward guarantees [Section titled “Reward guarantees”](#reward-guarantees) * `onReward` fires **at most once per show** and **only after confirmed completion**, whether Qartvelo Ads or the AdMob fallback served the ad. * Qartvelo Ads rewards are `QartveloAdsReward(type = "reward", amount = 1)`. AdMob rewards carry the type and amount configured on your AdMob unit (`onUserEarnedReward` is mapped to `onReward`). Most apps ignore the amount and grant their own fixed reward. * Closing a Qartvelo Ads video early asks the user for confirmation and forfeits the reward. * Rotation and Activity re-creation neither restart the video nor repeat events. * The SDK reports the completion to the backend (`POST /events/reward`) for auditing; your app should grant the reward from the callback, not wait for the server. ## Video playback [Section titled “Video playback”](#video-playback) Qartvelo Ads rewarded videos are MP4 (H.264), 5 to 60 seconds long, played full-screen with Media3 ExoPlayer from a file downloaded before `onLoaded`. Playback never starts streaming over a slow network. # AdMob fallback > How the SDK falls back to your own AdMob ad units, and how to set it up. Qartvelo Ads is the primary ad source. When it has no eligible campaign, fails, is switched off or exceeds its time budget, the SDK can show an ad from **your own** Google AdMob account instead, so a placement is never left empty. ## Your AdMob account, your ad units, your revenue [Section titled “Your AdMob account, your ad units, your revenue”](#your-admob-account-your-ad-units-your-revenue) * You create your own AdMob account, your own AdMob app and your own ad units (one per placement and format) and give those ids to Qartvelo Ads. * Qartvelo Ads never owns, shares, proxies or pools AdMob inventory and never routes several publishers through one AdMob account. * Google pays AdMob revenue directly to you. Qartvelo Ads does not receive, report, take a share of, or have access to your AdMob revenue or account. * The adapter uses Google’s official `com.google.android.gms:play-services-ads` artifact as a normal Gradle dependency, never a modified copy. * You remain responsible for complying with AdMob policies (placement, invalid traffic, consent). ## Setup [Section titled “Setup”](#setup) 1. In AdMob, create an app for your package and ad units that match your placements: banner units for banner placements, interstitial units for interstitial placements, rewarded units for rewarded placements. 2. Add the AdMob **App ID** (contains `~`) to `AndroidManifest.xml` as `com.google.android.gms.ads.APPLICATION_ID` meta-data. 3. Add the adapter: `implementation("com.qartvelo.ads:admob:0.3.0")`, or `QartveloAds_admobEnabled=true` in React Native. 4. Map each placement to its ad unit **ID** (contains `/`), either on the placement in the dashboard (fallback provider **AdMob**) or in code: ```kotlin QartveloAdsOptions( admobAdUnits = mapOf( "home_banner" to "ca-app-pub-XXX/111", "game_end" to "ca-app-pub-XXX/222", "reward_coins" to "ca-app-pub-XXX/333", ), ) ``` The in-code mapping wins over the dashboard value for the same placement code. No other code is needed. The SDK discovers the adapter automatically, initializes Google Mobile Ads once on a background thread and reports AdMob ads through the same callbacks with `info.source == AdSource.ADMOB` (`'admob'` in React Native). ## When a fallback is possible [Section titled “When a fallback is possible”](#when-a-fallback-is-possible) All of these must hold for a placement: * the adapter is present (or a custom adapter was registered with `QartveloAds.registerFallbackAdapter`); * `admobFallback` is `true` in the options and the remote config allows fallback; * the placement’s fallback provider is `admob` (and the ad response did not say `fallback: none`); * an ad unit id is known for the placement (in code or in the dashboard), or test mode is on. ## Decision flow [Section titled “Decision flow”](#decision-flow) **Interstitial and rewarded loads** 1. A still-valid cached Qartvelo Ads ad completes the load immediately. 2. Otherwise the SDK requests Qartvelo Ads and, if a fallback is possible, **preloads AdMob in parallel**. 3. Qartvelo Ads fill: the creative is downloaded and validated, then `onLoaded(source = QARTVELO)`. 4. No fill, timeout, network error, creative failure or kill switch: `onFallbackStarted(reason)`, then `onLoaded(source = ADMOB)` as soon as AdMob is ready, or `onNoAdAvailable` + `onLoadFailed` if AdMob also has nothing. 5. Show prefers a valid Qartvelo Ads ad, then a ready AdMob ad, else `onNoAdAvailable`. **Banners** request Qartvelo Ads first. On failure, the AdMob anchored adaptive banner is created in the same view and refreshes itself according to your AdMob settings. The next successful Qartvelo Ads refresh replaces it. ## Fallback reasons [Section titled “Fallback reasons”](#fallback-reasons) | Reason | Meaning | | ----------------- | ------------------------------------------------------------------ | | `no_fill` | The backend had no eligible campaign (or test mode forced it) | | `timeout` | No answer within the request timeout, or the creative was too slow | | `error` | Network or server error | | `creative_failed` | The creative could not be downloaded, decoded or rendered | | `disabled` | Qartvelo Ads is switched off for this placement, app or publisher | The SDK also sends a fire-and-forget `POST /api/v1/events/fallback` with the placement code and the reason (never any AdMob data), so your reports show fallback counts. ## Timeouts and remote configuration [Section titled “Timeouts and remote configuration”](#timeouts-and-remote-configuration) * The Qartvelo Ads request budget (session plus ad request) defaults to 800 ms. Users never wait for it: loads are asynchronous and AdMob is already preloading. * Effective timeout: the placement’s remote value, else `requestTimeoutMs`, clamped to 100..10000 ms. * Creative downloads have separate limits (images 10 s, video 45 s), because a Qartvelo Ads ad is only reported as loaded once its creative is on the device. * Without an app update, the dashboard can switch Qartvelo Ads off per placement (the SDK goes straight to AdMob), change the fallback provider, change the timeout and change the AdMob unit. The last configuration is cached for offline starts. * If the API is unreachable at start-up, the SDK runs on the cached configuration and falls back quickly; repeated session attempts are throttled. ## Test mode [Section titled “Test mode”](#test-mode) With `testMode = true` the adapter replaces every unit with Google’s public test units and Qartvelo Ads serves only non-billable test creatives. Add `testForceNoFill = true` to see the fallback every time. Never ship a release with test mode or Google’s test ids. ## Consent [Section titled “Consent”](#consent) The adapter does not show consent forms and does not set consent. It respects your Google UMP / CMP configuration and only adds the restrictions you pass through `QartveloAds.setPrivacy`. See [Privacy](/guides/privacy/).