Skip to content

Latest commit

 

History

183 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AdManageKit

JitPack API License

AdManageKit is a comprehensive Android library designed to simplify the integration and management of Google AdMob ads, Google Play Billing, and User Messaging Platform (UMP) consent.

Latest Version 4.4.5 is a patch release. No API changed. It fixes a BannerAdView bug that renders a blank banner slot inside Jetpack Compose: swapping the shimmer placeholder for the loaded AdView raises an ordinary requestLayout(), which cannot cross Compose's AndroidView interop boundary once any ancestor already carries a pending layout flag — so Compose never re-measured the subtree and the freshly attached AdView was left at 0×0. The ad loaded, onAdLoaded/onAdImpression fired and the impression was logged and billed, but the slot stayed blank until a rotation or resize forced a full traversal. Both the AdMob and BannerWaterfall success paths now force the measure/layout pass themselves. Also picks up the Next-Gen GMA SDK 1.4.0, AGP 9.3.2 and Firebase BOM 34.18.0. See Release Notes v4.4.5.

Version 4.4.4 is a critical billing hotfix. No API changed. It repairs a regression introduced in 4.4.3, where the Play Billing connection was never started at all: connectToGooglePlayBilling() guarded on billingClient.isReady(), which reports true the instant a client is built, so startConnection(...) was skipped on every fresh client. Setup never ran, no product details or entitlement were ever fetched, and there was no failure callback and no error log to explain it. If you ship the billing module on 4.4.3, upgrade. Also fixes a long-standing bug where a timed-out setup reported itself as initialized — permanently suppressing the host app's if (!initBillingFinish) initBilling() retry — and adds DEBUG logging across the billing connection lifecycle. See Release Notes v4.4.4.

⚠️ 4.4.3 is superseded. Its billing regression makes purchases unusable; use 4.4.4 or newer.

Version 4.4.3 is a bug-fix and dependency release. No API changed. Two silent-failure bugs are fixed, both of which cost money: a rewarded load the manager had given up on could sabotage the load that replaced it — discarding an ad a newer load had just delivered, and clearing the "loading" flag out from under a request still in flight — and billing could stop acknowledging purchases for the rest of the process, which lets Play auto-refund a completed purchase on day 3. Acknowledgment also no longer requires the purchase to match a configured product id. Picks up the Next-Gen GMA SDK 1.3.1, Yandex Mobile Ads 8.3.0, and refreshed AndroidX/Firebase/Compose BOMs. See Release Notes v4.4.3.

Version 4.4.2 is a bug-fix release. No API changed, but several fixes are user-visible and two affect revenue: rewarded ads could crash the app (every show-path callback, including onRewardEarned, was delivered on a background thread), a completed purchase could fail to disable ads until the next launch, blank gaps were left where banner/native slots should have collapsed for premium users and after failed loads (in both XML and Compose), and app open ads could appear over screens excluded via disableAppOpenAdsTemporarily(). Also fixes a BannerAdView leak that kept requesting ads after detach. Two deliberate behavior changes: an account-hold subscription no longer disables ads, and premium users no longer reserve ad space in Compose. See Release Notes v4.4.2.

Version 4.4.1 is a patch release that updates the Google Mobile Ads Next-Gen SDK to 1.3.0 (from 1.2.1) and repairs three documentation-delivery channels that had silently gone stale: the API docs build had failed on every release since the Dokka 2.x upgrade, leaving the published KDoc without any 4.x types; MCP server npm publishing had been broken since January 2026; and the MCP tool schemas hardcoded which versions and classes existed, so they rejected 4.4.0 as an unknown version. No AdManageKit API changed. See Release Notes v4.4.1.

Version 4.4.0 is a minor release focused on the billing module. Subscription offers can now be purchased individuallysubscribe(activity, offer) buys exactly the plan the user tapped, where previously the library always resolved the token itself and could charge for whichever offer Play listed last. It also adds offer lookup by id/base plan/tag, cross-cadence price normalization (BillingPeriod, getSavingsPercent, getFormattedPricePerMonth), trial-eligibility checks, Play Billing 9 one-time product offers (discounts, rentals, pre-orders, limited quantity), client-side account hold detection, Play payment-recovery messaging, and product-details diagnostics. Purely additive, with one behavior change: an on-hold subscription now reports ON_HOLD and is no longer isSubscriptionActive(). See Release Notes v4.4.0.

Version 4.3.5 is a patch release that makes appOpenAdFreshnessThreshold (default 4 hours) actually enforced on every app open show path. Previously only ON_DEMAND consulted it — HYBRID, ONLY_CACHE, and the multi-provider waterfall checked only whether an ad object existed and could show a day-old cached ad. Stale ads are now discarded and replaced instead of shown. It also settles the appOpenAdTimeout default at 10 seconds (the property said 4s while resetToDefaults() said 10s — 4s was too short for the fetch-with-dialog paths). No API changes. See Release Notes v4.3.5.

Version 4.3.4 is a patch release that restores the pre-4.2.0 default adaptive banner height: ADAPTIVE again requests the standard anchored adaptive size (~50-90dp) instead of the taller large anchored adaptive format the Next-Gen migration had switched to. The taller format is now opt-in via BannerAdSize.ADAPTIVE_LARGE (app:bannerAdSize="adaptive_large" in XML). See Release Notes v4.3.4.

Version 4.3.3 is a patch release that improves native ad media quality: every native request now sends a media-aspect-ratio hint matched to each template's MediaView slot shape and carries global VideoOptions (start-muted by default). New AdManageKitConfig fields and a per-view NativeTemplateView.setMediaAspect(...) override expose the behavior; all defaults preserve prior request behavior. See Release Notes v4.3.3.

Version 4.3.2 is a patch release that fixes banner shimmer sizing — the placeholder now reserves the real adaptive-banner height from the first frame and in the layout preview instead of collapsing to ~50dp — and stops the Compose native ad from clipping its call-to-action button. Also bumps the Android Gradle Plugin to 9.3.0. See Release Notes v4.3.2.

Version 4.3.1 is a patch release fixing crashes from Next-Gen SDK callbacks delivered on background threads — interstitial onNextAction, banner/native waterfall chains, and NativeTemplateView.onFailedToLoad are now marshalled to the main thread — plus a load-site guard for app open ads. See Release Notes v4.3.1.

Version 4.3.0 adds all standard AdMob banner sizes (BannerAdSize), fully custom native ad templates on NativeTemplateView, and a redesigned size-adaptive banner shimmer with night-mode support. See What's New in 4.3.0.

Since 4.2.0 the library runs on the Google Mobile Ads Next-Gen SDK (ads-mobile-sdk, stable) and Google Play Billing Library 9.1.0 — see Next-Gen GMA SDK below and Migrating to 4.2.0 if you're upgrading from 3.x/4.1.


Next-Gen GMA SDK

As of v4.2.0, AdManageKit runs on Google's Next-Gen Google Mobile Ads SDK (com.google.android.libraries.ads.mobile.sdk, stable 1.4.0 as of 4.4.5) instead of the legacy com.google.android.gms:play-services-ads. This isn't a branch or an opt-in — it's the only version of AdManageKit going forward.

Why the move

The legacy Google Mobile Ads SDK is in maintenance mode; new AdMob features (Ad Inspector improvements, mediation updates, in-app price-increase messaging, etc.) land on the Next-Gen SDK first, and Google's own guidance is to move new integrations there. AdManageKit had a separate nextgen branch exploring this since it was in beta and diverging from main's waterfall/Yandex/billing work — now that the SDK is stable, that split is gone.

What's different under the hood

  • Threading: Next-Gen SDK callbacks fire on a background thread (the legacy SDK guaranteed main thread). Every ad manager, provider, and view in this library already wraps its callback bodies in Handler(Looper.getMainLooper()).post {} — this is transparent to you as a consumer.
  • Initialization: MobileAds.initialize() must be called explicitly once, before any ad request — the legacy SDK's silent lazy-init on first use no longer exists. AdManageKit doesn't call this for you (it doesn't own your app's consent flow); see the sample app's MyApplication.kt for the recommended pattern.
  • Callback types: Where AdManageKit exposes ad-network error/value types through AdKitError, AdKitLoadError, and AdKitValue (used by AdManagerCallback, AdLoadCallback, AdCallback, and friends), those aliases now resolve to Next-Gen SDK types instead of legacy ones. Your callback implementations (onFailedToLoad(error), onPaidEvent(value), etc.) don't need to change — only code that reads legacy-only members of those objects does. See Migrating to 4.2.0.

What's New in 4.4.0

A billing-focused minor release. Everything is additive — no existing method signature changed — with one deliberate behavior change around account hold.

Buy the Offer the User Actually Tapped

AppPurchase could already describe subscription offers (getOffers, getTrialOffer, getBaseOffer since 3.5.7), but it could not act on them. subscribe(activity, subsId) always resolved the offer token itself — the configured trialId, and failing that whichever offer Play listed last. On a product with three plans, a user tapping "Yearly" could be charged for monthly, purely because of list order.

val offers = AppPurchase.getInstance().getOffers("premium_sub")
// …render them, then buy exactly what the user chose:
AppPurchase.getInstance().subscribe(activity, offers[selectedIndex])

The same applies to one-time products (purchase(activity, offer)) and upgrades (updateSubscription(activity, newSubsId, offerToken, oldToken, mode)). The original no-token overloads are unchanged.

Offer Lookup by Id, Base Plan or Tag

billing.getIntroOffer("premium_sub")                   // first with an intro price
billing.getOfferByBasePlanId("premium_sub", "yearly")
billing.getOfferByTag("premium_sub", "popular")        // Play Console offer tag
billing.getBestValueOffer("premium_sub")               // lowest cost per month
billing.getCheapestFirstCycleOffer("premium_sub")      // cheapest way in

Offer tags are the useful one here: tag offers in the Play Console and select them by tag, and a paywall can be re-targeted without shipping an app update.

Price Comparison Without Parsing ISO-8601

Play returns billing periods as raw strings ("P1M", "P1Y"). The new BillingPeriod parses them and normalizes prices so plans of different cadences compare fairly:

val period = BillingPeriod.parse("P1Y")!!
period.unit          // Unit.YEAR
period.count         // 1 → getQuantityString(R.plurals.years, count, count)
period.totalMonths   // 12.0

billing.getSavingsPercent("premium_monthly", "premium_yearly")  // 50 → "Save 50%"
billing.getFormattedPricePerMonth("premium_yearly")             // "$5.00"

OfferInfo also gained firstCyclePrice (what the user pays today — "Free", "$1.99" or "$9.99"), pricePerMonthMicros, introDiscountPercent, trialDays, introTotalDays, isBaseOffer and hasTag(...).

Trial Eligibility

Google Play filters offers per account — a user who already used a trial simply doesn't receive that offer. So offer presence is the eligibility signal, now named explicitly:

button.text = if (billing.isEligibleForFreeTrial("premium_sub")) "Start free trial" else "Subscribe"

This stops a paywall promising a trial the user cannot claim and then having Play charge them immediately.

Play Billing 9 Features That Were Unused

  • One-time product offers — a single INAPP product can carry several offers (a full price plus a launch discount, a rental, a pre-order, a limited-quantity drop). New OneTimeOfferInfo + getOneTimeOffers() / getBestOneTimeOffer(); the legacy getOneTimePurchaseOfferDetails() exposes only one of them
  • Payment recoveryshowInAppMessages(activity, listener) shows Play's fix-your-payment flow for declined subscriptions
  • Product-details diagnosticssetProductDetailsListener(...) and getUnfetchedProducts() report which ids Play declined and why, so an empty paywall is no longer silent
  • Installment plansOfferInfo.isInstallmentPlan and commitment payment counts
  • Fraud prevention & EU disclosuresetObfuscatedAccountId(), setObfuscatedProfileId(), setOfferPersonalized(), applied to every flow the library launches

Account Hold Detected Client-Side (behavior change)

When Play cannot charge a subscriber, the subscription enters account hold. This previously required server-side verification; Play Billing 9 reports it client-side, so PurchaseResult.isSuspended() is now populated and getSubscriptionState() returns ON_HOLD.

isSubscriptionActive() therefore returns false during account hold. That matches Google's requirement — the user's payment was declined and they must not keep premium access. If your app deliberately keeps serving on-hold users, check isSuspended() explicitly.

if (billing.hasSubscriptionOnHold()) {
    billing.showInAppMessages(activity, object : InAppMessageListener {
        override fun onSubscriptionRecovered(purchaseToken: String) { refreshPremiumUi() }
        override fun onNoActionNeeded() { }
    })
}

See Release Notes v4.4.0 and the Subscription Offers wiki page for full details.

What's New in 4.3.0

All Standard Banner Sizes

BannerAdView and BannerAdCompose now support every standard AdMob banner size via the new BannerAdSize enum — ADAPTIVE (default, unchanged behavior), BANNER (320x50), LARGE_BANNER (320x100), MEDIUM_RECTANGLE (300x250), FULL_BANNER (468x60), LEADERBOARD (728x90). Select programmatically (loadBanner(activity, adUnitId, BannerAdSize.MEDIUM_RECTANGLE)), in XML (app:bannerAdSize="medium_rectangle"), or in Compose (adSize = BannerAdSize.MEDIUM_RECTANGLE). The size carries through retries, auto-refresh, and the multi-provider waterfall. See the Banner Ad Guide.

Custom Native Ad Templates

If none of the 38 built-in NativeTemplateView presets fit your design, supply your own layout: setCustomTemplate(layoutResId, shimmerResId, sizeHint) programmatically, app:customAdLayout/app:customAdShimmerLayout in XML, or customLayoutResId on NativeTemplateCompose. The layout's root must be a Next-Gen SDK NativeAdView reusing the standard asset ids. See the NativeTemplateView guide.

Redesigned Banner Shimmer + Night Mode

The banner loading placeholder now adapts to the requested size (a 300x250 request shows a proper media-block placeholder, not a lone 50dp row), reserves the exact ad size on every path including waterfalls (no layout jump), centers to match the loaded ad, and follows the system dark theme instead of rendering a hardcoded white card.

App Open Ads: Late MobileAds Initialization Support

AppOpenManager no longer crashes if constructed before MobileAds.initialize() completes: every load path is guarded, callback-driven flows (splash screens) wait for the SDK within their existing timeout and always receive a terminal callback, and the new isMobileAdsReady() confirms the SDK can accept requests. See the App Open Ads guide.

Compatibility: source-compatible; ADAPTIVE keeps pre-4.3.0 behavior everywhere. Recompile against 4.3.0 rather than hot-swapping the AAR (Compose composables gained default parameters).

See Release Notes v4.3.0 for full details.

What's New in 4.2.0

Google Mobile Ads Next-Gen SDK

com.google.android.gms:play-services-ads is replaced by the stable Next-Gen SDK (ads-mobile-sdk:1.2.1) across every ad manager, provider, native view, and the Compose module. This is the change described in Next-Gen GMA SDK above — read that section first if you're upgrading.

Google Play Billing Library 9.1.0

Upgraded from 8.3.0. No code changes were needed in AppPurchase — the wrapper was already on v8+ patterns. Source- and behavior-compatible.

compileSdk 37

Required transitively by androidx-core-ktx 1.19.0+. Bump your app's compileSdk to 37+.

Fixes

  • Native ad MediaView rendering blank in NativeTemplateView and the programmatic native provider (a leftover legacy-SDK pattern was interfering with the Next-Gen SDK's automatic media rendering)
  • NativeAdView.mediaView returning null for dynamically-inflated templates (the getter's auto-discovery needs the view to be window-attached; affected views now use findViewById() directly)
  • A Native Validator false-positive risk on deeply-nested templates like CARD_MODERN, caused by registering the ad before the view had been through a layout pass

Compatibility: not fully source-compatible — see Migrating to 4.2.0. Most consumers (anyone who only calls methods on AdManageKit's own callback types, without custom native XML layouts) are unaffected.

See Release Notes v4.2.0 for full details.

For the 3.6.0 release (Yandex template matching, waterfall-aware programmatic loader), see Release Notes v3.6.0.

For previous versions, see the Changelog or individual release notes.

Screenshots

NativeBannerSmall Ad Interstitial Ad App Open Ad UMP Consent Form
NativeBannerSmall ad displayed in app Interstitial ad with loading dialog App open ad on app launch UMP consent form

Demo Video

Watch a short demo of AdManageKit in action:

Watch on YouTube

Getting Started

Installation

Step 1: Add JitPack to your root build.gradle:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}

Step 2: Add dependencies to your app's build.gradle:

implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit:v4.4.5'
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-billing:v4.4.5'
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-core:v4.4.5'

// For Jetpack Compose support
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-compose:v4.4.5'

// For Yandex Ads multi-provider support
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-yandex:v4.4.5'

Step 3: Ensure your app's compileSdk is 37 or higher (required transitively as of 4.2.0).

Step 4: Sync your project with Gradle.

Features

NativeTemplateView (v2.6.0+)

  • 38 Template Styles: card_modern, material3, app_store, social_feed, gradient_card, pill_banner, medium_horizontal, flexible, icon_left, top_icon_media, spotlight, plus the video_* and flat_* families
  • XML & Programmatic: Set templates via app:adTemplate or setTemplate()
  • Material 3 Theming: Automatic dark/light mode support
  • AdChoices Control: Configure placement position (v2.9.0+)
  • Video-Ready: All templates support video ads (120dp+ MediaView)
  • View Documentation

Ad Loading Strategies (v2.6.0+)

  • ON_DEMAND: Fetch fresh ads with loading dialog
  • ONLY_CACHE: Instant display from cache
  • HYBRID: Cache-first with fallback fetch (recommended)
  • View Documentation

Jetpack Compose Integration

  • BannerAdCompose, NativeAdCompose, InterstitialAdCompose
  • Programmatic native ads without predefined layouts
  • ConditionalAd, CacheWarmingEffect utilities

AdMob Ads Management

  • Banner Ads: All standard sizes (adaptive, 320x50, 320x100, 300x250, 468x60, 728x90), auto-refresh, collapsible banners, smart retry
  • Native Ads: Small, Medium, Large formats with caching
  • Interstitial Ads: Time/count-based triggers, dialog support
  • App Open Ads: Lifecycle-aware with activity exclusion

Centralized Configuration

  • AdManageKitConfig: Single configuration point
  • Environment-specific settings (debug vs production)
  • Runtime configuration changes

Intelligent Native Ad Caching

  • Screen-aware caching prevents collisions
  • Smart preloading with usage patterns
  • LRU cache with configurable expiration

Reliability & Performance

  • Smart retry with exponential backoff
  • Circuit breaker for failing ad units
  • Memory leak prevention with WeakReference

Billing & Monetization

  • Subscriptions & One-Time Products: Categories (consumable, feature unlock, remove ads, lifetime premium) with automatic acknowledgment
  • Offers: Enumerate every trial / introductory / base offer and purchase a specific one (v4.4.0+)
  • Price Comparison: ISO-8601 period parsing, per-month normalization, "Save 40%" badges (v4.4.0+)
  • Trial Eligibility: Ask whether this account can still claim a trial before promising one (v4.4.0+)
  • Payment Recovery: Client-side account-hold detection plus Play's in-app recovery flow (v4.4.0+)
  • In-App Purchase Guide

Privacy & Compliance

  • UMP consent management (GDPR/CCPA)
  • Automatic ad hiding for purchased users
  • Obfuscated account/profile identifiers and EU personalized-pricing disclosure (v4.4.0+)

Multi-Provider Waterfall (New)

  • Multiple Ad Networks: Load ads from AdMob, Yandex, and more with automatic fallback
  • Zero Code Changes: Configure provider chains once; all existing API calls use waterfall automatically
  • Per-Ad-Type Chains: Configure different provider orders for each ad format
  • Region-Based: Prioritize providers by user locale (e.g., Yandex first for Russia)
  • View Waterfall Documentation
  • Yandex Integration Guide

Multi-Module Architecture

  • Core Module: Shared interfaces and configuration
  • Compose Module: Jetpack Compose integration
  • Billing Module: Google Play Billing Library v9
  • Yandex Module: Yandex Ads SDK provider

Usage Guide

Quick Configuration

Configure AdManageKit in your Application class:

class MyApp : Application() {
    private lateinit var appOpenManager: AppOpenManager

    override fun onCreate() {
        super.onCreate()

        // Configure AdManageKit
        AdManageKitConfig.apply {
            debugMode = BuildConfig.DEBUG
            enableSmartPreloading = true
            autoRetryFailedAds = true

            // Ad Loading Strategies (v2.6.0+)
            interstitialLoadingStrategy = AdLoadingStrategy.HYBRID
            appOpenLoadingStrategy = AdLoadingStrategy.HYBRID
            nativeLoadingStrategy = AdLoadingStrategy.HYBRID

            // Auto-reload ads after showing (v2.7.0+)
            interstitialAutoReload = true  // default: true
            appOpenAutoReload = true       // default: true
            rewardedAutoReload = true      // default: true
        }

        // Set up billing
        BillingConfig.setPurchaseProvider(BillingPurchaseProvider())

        // Initialize app open ads
        appOpenManager = AppOpenManager(this, "your-app-open-ad-unit-id")
    }
}

Multi-Provider Waterfall (Optional)

Add Yandex (or other providers) as fallback ad networks with zero changes to your existing ad loading code:

// Add Yandex module
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-yandex:v3.6.0'
// In Application.onCreate(), after AdManageKitConfig setup:
YandexProviderRegistration.initialize(this)
val admob = AdMobProviderRegistration.create()
val yandex = YandexProviderRegistration.create()

// Map your AdMob ad unit IDs to Yandex equivalents
AdUnitMapping.register("ca-app-pub-xxx/your-interstitial", mapOf("yandex" to "R-M-XXXXXX-Y"))
AdUnitMapping.register("ca-app-pub-xxx/your-native", mapOf("yandex" to "R-M-XXXXXX-Y"))

// Set provider chains (order = priority)
AdProviderConfig.setInterstitialChain(listOf(admob.interstitialProvider, yandex.interstitialProvider))
AdProviderConfig.setNativeChain(listOf(admob.nativeProvider, yandex.nativeProvider))
// ... same for banner, app open, rewarded

See Multi-Provider Waterfall and Yandex Integration for the full guide.

NativeTemplateView (v2.6.0+)

XML Usage

<com.i2hammad.admanagekit.admob.NativeTemplateView
    android:id="@+id/nativeTemplateView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:adTemplate="material3" />

Available Templates

Template Best For
card_modern General use
material3 M3 apps
minimal Content-focused
compact_horizontal Lists
list_item RecyclerView items
magazine News/blog apps
app_store App promotion (v2.9.0+)
social_feed Feed integration (v2.9.0+)
gradient_card Premium feel (v2.9.0+)
pill_banner Inline placement (v2.9.0+)
medium_horizontal 55/45 media-content split (v3.0.0+)
spotlight High visibility (v2.9.0+)
media_content_split Balanced display (v2.9.0+)
flexible Adaptive layout (v3.3.2+)
icon_left Icon on left, GridView optimized (v3.3.2+)
top_icon_media Icon at top, MediaView center (v3.3.2+)
video_small/medium/large Video content
video_square/vertical/fullscreen Social feeds

Programmatic Usage

// Load with default template
nativeTemplateView.loadNativeAd(activity, "ca-app-pub-xxx/yyy")

// Change template
nativeTemplateView.setTemplate(NativeAdTemplate.MAGAZINE)
nativeTemplateView.loadNativeAd(activity, "ca-app-pub-xxx/yyy")

// With callback
nativeTemplateView.loadNativeAd(activity, adUnitId, object : AdLoadCallback() {
    override fun onAdLoaded() { /* success */ }
    override fun onFailedToLoad(error: AdError?) { /* error */ }
})

// With strategy override
nativeTemplateView.loadNativeAd(activity, adUnitId, callback, AdLoadingStrategy.ONLY_CACHE)

Banner Ads

<com.i2hammad.admanagekit.admob.BannerAdView
    android:id="@+id/bannerAdView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:bannerAdSize="adaptive" />
bannerAdView.loadBanner(this, "ca-app-pub-xxx/yyy")
// Fixed size (BANNER, LARGE_BANNER, MEDIUM_RECTANGLE, FULL_BANNER, LEADERBOARD)
bannerAdView.loadBanner(this, "ca-app-pub-xxx/yyy", BannerAdSize.MEDIUM_RECTANGLE)
// Collapsible banner (adaptive size only)
bannerAdView.loadCollapsibleBanner(this, "ca-app-pub-xxx/yyy", true)

All standard AdMob banner sizes are supported via BannerAdSize (v4.3.0); the default ADAPTIVE is the Google-recommended full-width anchored adaptive banner (~50-90dp), and ADAPTIVE_LARGE (v4.3.4) opts into the taller Next-Gen large anchored adaptive format. See Banner Ad Guide for the full size table.

Native Ads (Traditional Views)

<com.i2hammad.admanagekit.admob.NativeBannerSmall
    android:id="@+id/nativeBannerSmall"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
nativeBannerSmall.loadNativeBannerAd(this, "ca-app-pub-xxx/yyy")
// With caching
nativeBannerSmall.loadNativeBannerAd(activity, adUnitId, useCachedAd = true)

Interstitial Ads

// Load
AdManager.getInstance().loadInterstitialAd(this, "ca-app-pub-xxx/yyy")

// Show immediately
AdManager.getInstance().forceShowInterstitial(this, object : AdManagerCallback() {
    override fun onNextAction() { navigateNext() }
})

// Show with dialog
AdManager.getInstance().forceShowInterstitialWithDialog(this, callback)

// Time-based (every 15 seconds)
AdManager.getInstance().showInterstitialAdByTime(this, callback)

// Count-based
AdManager.getInstance().showInterstitialAdByCount(this, callback, maxDisplayCount = 3)

Auto-Reload Configuration (v2.7.0+)

Control whether interstitial ads automatically reload after being shown:

// Global config (applies to all AdManager methods)
AdManageKitConfig.interstitialAutoReload = false  // Disable auto-reload

// Per-call override via InterstitialAdBuilder
InterstitialAdBuilder.with(activity)
    .adUnit(adUnitId)
    .autoReload(true)  // Override global setting for this call
    .show { navigateNext() }

// Per-call override via AdManager
AdManager.getInstance().showInterstitialIfReady(activity, callback, reloadAd = false)

Priority: InterstitialAdBuilder.autoReload() > AdManageKitConfig.interstitialAutoReload

Rewarded Ads

// Initialize once (e.g., in Application.onCreate())
RewardedAdManager.initialize(context, "ca-app-pub-xxx/yyy")

// Show when ready
if (RewardedAdManager.isAdLoaded()) {
    RewardedAdManager.showAd(activity, object : RewardedAdManager.RewardedAdCallback {
        override fun onRewardEarned(rewardType: String, rewardAmount: Int) {
            grantReward(rewardType, rewardAmount)
        }
        override fun onAdDismissed() {
            continueGameFlow()
        }
    })
}

// Load with timeout (splash screens)
RewardedAdManager.loadRewardedAdWithTimeout(context, 5000, callback)

// Preload during natural pauses
RewardedAdManager.preload(context)

// Control auto-reload
AdManageKitConfig.rewardedAutoReload = false  // Disable globally
RewardedAdManager.showAd(activity, callback, autoReload = false)  // Per-call override

App Open Ads

// In Application class
appOpenManager = AppOpenManager(this, "ca-app-pub-xxx/yyy")

// Exclude activities
appOpenManager.disableAppOpenWithActivity(MainActivity::class.java)

// Force show
appOpenManager.forceShowAdIfAvailable(activity, callback)

// Skip next ad
appOpenManager.skipNextAd()

Single-Activity App Support (v3.2.0+)

For apps with one activity and multiple fragments:

// Set current screen when navigating
navController.addOnDestinationChangedListener { _, destination, _ ->
    appOpenManager.setCurrentScreenTag(destination.label?.toString())
}

// Exclude specific screens
appOpenManager.excludeScreenTags("Payment", "Onboarding", "Checkout")

// Or use fragment tag provider
appOpenManager.setFragmentTagProvider {
    supportFragmentManager.fragments.lastOrNull()?.tag
}
appOpenManager.excludeFragmentTags("PaymentFragment", "OnboardingFragment")

// Temporarily disable during critical flows
appOpenManager.disableAppOpenAdsTemporarily()
// ... perform operation ...
appOpenManager.enableAppOpenAds()

Jetpack Compose

@Composable
fun MyScreen() {
    // Banner
    BannerAdCompose(adUnitId = "ca-app-pub-xxx/yyy")

    // NativeTemplateView with any template (v2.6.0+)
    NativeTemplateCompose(
        adUnitId = "ca-app-pub-xxx/yyy",
        template = NativeAdTemplate.MATERIAL3,
        loadingStrategy = AdLoadingStrategy.HYBRID
    )

    // Native with loading strategy (ON_DEMAND or HYBRID only)
    NativeBannerMediumCompose(
        adUnitId = "ca-app-pub-xxx/yyy",
        loadingStrategy = AdLoadingStrategy.HYBRID
    )

    // Interstitial
    val showInterstitial = rememberInterstitialAd(
        adUnitId = "ca-app-pub-xxx/yyy",
        preloadAd = true
    )
    Button(onClick = { showInterstitial() }) {
        Text("Show Ad")
    }

    // Conditional (hides for purchased users)
    ConditionalAd {
        ProgrammaticNativeBannerMediumCompose(adUnitId = "ca-app-pub-xxx/yyy")
    }
}

UMP Consent

AdsConsentManager.getInstance(this).requestUMP(
    activity = this,
    isDebug = true,
    testDeviceId = "TEST_DEVICE_ID",
    resetConsent = false,
    listener = object : UMPResultListener {
        override fun onCheckUMPSuccess(isConsentGiven: Boolean) {
            if (isConsentGiven) {
                // Initialize and load ads here
                AdManager.getInstance().loadInterstitialAd(activity, adUnitId)
            }
        }
    }
)

In-App Purchases (v2.9.0+)

// Define products with categories
val products = listOf(
    PurchaseItem("coins_100", TYPE_IAP.PURCHASE, PurchaseCategory.CONSUMABLE),
    PurchaseItem("remove_ads", TYPE_IAP.PURCHASE, PurchaseCategory.REMOVE_ADS),
    PurchaseItem("lifetime", TYPE_IAP.PURCHASE, PurchaseCategory.LIFETIME_PREMIUM),
    PurchaseItem("premium_monthly", "free_trial", TYPE_IAP.SUBSCRIPTION)
)

// Initialize
AppPurchase.getInstance().initBilling(application, products)

// Purchase
AppPurchase.getInstance().purchase(activity, "remove_ads")

// Subscribe
AppPurchase.getInstance().subscribe(activity, "premium_monthly")

// Check status
if (AppPurchase.getInstance().isPurchased()) {
    // User has premium (subscription, lifetime, or remove_ads)
}

// Track purchases and handle consumables
AppPurchase.getInstance().setPurchaseHistoryListener(object : PurchaseHistoryListener {
    override fun onNewPurchase(productId: String, purchase: PurchaseResult) {
        if (productId == "coins_100") {
            addCoins(100 * purchase.quantity)
            AppPurchase.getInstance().consumePurchase(productId) // Manual consume
        }
    }
    override fun onPurchaseConsumed(productId: String, purchase: PurchaseResult) { }
})

Subscription Offers & Paywalls (v4.4.0+)

A single subscription product usually carries several offers — a base plan, a free trial, an introductory discount. Play returns only the ones this account is eligible for.

val billing = AppPurchase.getInstance()

// Enumerate every offer, then buy exactly the one the user tapped.
// (subscribe(activity, subsId) picks for you, and may not pick what they chose.)
val offers = billing.getOffers("premium_sub")
billing.subscribe(activity, offers[selectedIndex])

// Or find a specific one
billing.getIntroOffer("premium_sub")                   // first with an intro price
billing.getOfferByBasePlanId("premium_sub", "yearly")
billing.getOfferByTag("premium_sub", "popular")        // Play Console offer tag
billing.getBestValueOffer("premium_sub")               // lowest cost per month

Render an offer without hand-parsing ProductDetails or ISO-8601 periods:

val offer = billing.getBaseOffer("premium_yearly") ?: return

priceLabel.text    = offer.basePrice                   // "$59.99"
cycleLabel.text    = BillingPeriod.formatOf(offer.billingPeriod)   // "1 year"
perMonthLabel.text = AppPurchase.formatPrice(offer.pricePerMonthMicros, offer.currencyCode)
todayLabel.text    = offer.firstCyclePrice             // "Free", "$1.99" or "$59.99"

// "Save 50%" badge — compares base offers, so trials don't distort it
val savings = billing.getSavingsPercent("premium_monthly", "premium_yearly")
savingsBadge.isVisible = savings > 0
savingsBadge.text = "Save $savings%"

// Only promise a trial the user can actually claim
subscribeButton.text = if (billing.isEligibleForFreeTrial("premium_sub")) {
    "Start free trial"
} else {
    "Subscribe"
}

Localization: BillingPeriod.formatOf() is an English convenience. For shipped UI use BillingPeriod.parse(iso) and pair unit + count with your own plurals resources.

One-time products can carry multiple offers too (discounts, rentals, pre-orders, limited quantity):

billing.getBestOneTimeOffer("remove_ads")?.let { offer ->
    price.text = offer.formattedPrice
    badge.isVisible = offer.isDiscounted
    badge.text = "-${offer.effectiveDiscountPercent}%"
    buyButton.isEnabled = !offer.isSoldOut && offer.isValidAt()
    buyButton.setOnClickListener { billing.purchase(activity, offer) }
}

Subscription Management (v2.9.0+)

// Check subscription state
val state = AppPurchase.getInstance().getSubscriptionState("premium_monthly")
when (state) {
    SubscriptionState.ACTIVE -> showPremiumUI()
    SubscriptionState.CANCELLED -> showRenewalPrompt()   // Still has access
    SubscriptionState.ON_HOLD -> showFixPaymentPrompt()  // Payment declined — no access (v4.4.0+)
    SubscriptionState.EXPIRED -> showSubscribeButton()
}

// Upgrade subscription
AppPurchase.getInstance().upgradeSubscription(activity, "premium_yearly")

// Downgrade subscription
AppPurchase.getInstance().downgradeSubscription(activity, "premium_basic")

// Full control with proration mode
AppPurchase.getInstance().changeSubscription(
    activity,
    "premium_monthly",
    "premium_yearly",
    SubscriptionReplacementMode.CHARGE_PRORATED_PRICE
)

Account Hold & Payment Recovery (v4.4.0+)

When Play cannot charge a subscriber, the subscription enters account hold. As of 4.4.0 this is detected client-side — earlier versions needed a server round-trip.

if (billing.hasSubscriptionOnHold()) {
    // Let Play walk the user through fixing their payment method.
    billing.showInAppMessages(activity, object : InAppMessageListener {
        override fun onSubscriptionRecovered(purchaseToken: String) {
            refreshPremiumUi()   // isPurchased() is already up to date here
        }
        override fun onNoActionNeeded() { }
    })
}

Behavior change: isSubscriptionActive() returns false during account hold, matching Google's requirement. If your app deliberately keeps serving these users, check isSuspended() explicitly.

Debugging an Empty Paywall (v4.4.0+)

billing.setProductDetailsListener(object : ProductDetailsListener {
    override fun onProductDetailsLoaded(
        productType: String,
        loaded: List<ProductDetails>,
        unfetched: List<UnfetchedProduct>,
    ) {
        unfetched.forEach { Log.e("Billing", "${it.productId}: status ${it.statusCode}") }
    }
    override fun onProductDetailsFailed(productType: String, responseCode: Int, debugMessage: String?) { }
})

Register it before initBilling. Products land in unfetched when the id is misspelled, the product is inactive in Play Console, or the account can't see the release track.


Documentation

Wiki

API Documentation (Dokka)

Online: https://i2hammad.github.io/AdManageKit/

Generate locally:

./gradlew dokkaGenerateHtml

Output: build/dokka/html/index.html

MCP Server (AI Integration)

npm

AdManageKit provides an MCP (Model Context Protocol) server that integrates with AI tools like Claude Code, Claude Desktop, Cursor, and other MCP-compatible clients. It provides 10 tools for documentation lookup and code generation.

Tools

Category Tools
Documentation search_docs, get_doc_by_topic, get_api_reference, get_release_notes, get_migration_guide, list_documentation
Code Generation generate_config, generate_ad_integration, generate_billing_code, generate_compose_code

Setup

Claude Desktop / Cursor (claude_desktop_config.json):

{
  "mcpServers": {
    "admanagekit": {
      "command": "npx",
      "args": ["-y", "admanagekit-mcp-server"]
    }
  }
}

Claude Code (auto-configured via .mcp.json when working in this repo):

{
  "mcpServers": {
    "admanagekit": {
      "command": "node",
      "args": ["mcp-server/dist/index.js"]
    }
  }
}

What It Does

  • Search documentation across all docs, wiki pages, and API references
  • Look up API references for any class (AdManager, AppOpenManager, NativeAdManager, etc.)
  • Generate integration code for any ad type with display modes, loading strategies, and callbacks
  • Generate billing code for purchases, subscriptions, consumables, and expiry verification
  • Generate Compose code for all Compose ad components
  • Supports both Kotlin and Java output

See mcp-server/ for more details.


Migration Guide

Migrating to 4.2.0

The Next-Gen GMA SDK swap (see Next-Gen GMA SDK) is source-compatible for most consumers. It only affects you if your code does one of the following:

1. You read specific members of an error/value object, not just call the callback method.

AdKitError/AdKitLoadError now resolve to the Next-Gen SDK's LoadAdError, which drops the old domain/cause fields and replaces the free-form Int error code with a closed LoadAdError.ErrorCode enum:

// Before (4.1.x and earlier)
override fun onFailedToLoad(error: AdKitError?) {
    Log.e(TAG, "Failed: ${error?.code} / ${error?.domain}")  // .domain no longer exists
}

// After (4.2.0)
override fun onFailedToLoad(error: AdKitError?) {
    Log.e(TAG, "Failed: ${error?.code}")  // .code is now a LoadAdError.ErrorCode enum, still fine to log
}

2. You use the raw-callback overload of AdManager.loadInterstitialAd.

// Before
AdManager.getInstance().loadInterstitialAd(context, adUnitId,
    object : com.google.android.gms.ads.interstitial.InterstitialAdLoadCallback() {
        override fun onAdLoaded(ad: com.google.android.gms.ads.interstitial.InterstitialAd) { }
        override fun onAdFailedToLoad(error: com.google.android.gms.ads.LoadAdError) { }
    })

// After
AdManager.getInstance().loadInterstitialAd(context, adUnitId,
    object : com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback<
        com.google.android.libraries.ads.mobile.sdk.interstitial.InterstitialAd> {
        override fun onAdLoaded(ad: InterstitialAd) { }
        override fun onAdFailedToLoad(adError: LoadAdError) { }
    })

3. You supply a custom native ad layout XML.

<!-- Before -->
<com.google.android.gms.ads.nativead.NativeAdView ...>
    <com.google.android.gms.ads.nativead.MediaView android:id="@+id/ad_media" ... />
    <com.google.android.gms.ads.nativead.AdChoicesView android:id="@+id/ad_choices_view" ... />
</com.google.android.gms.ads.nativead.NativeAdView>

<!-- After -->
<com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView ...>
    <com.google.android.libraries.ads.mobile.sdk.nativead.MediaView android:id="@+id/ad_media" ... />
    <com.google.android.libraries.ads.mobile.sdk.common.AdChoicesView android:id="@+id/ad_choices_view" ... />
</com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView>

4. Your Compose callback lambdas' parameter types are explicitly typed.

If you wrote onAdFailedToLoad: (com.google.android.gms.ads.LoadAdError?) -> Unit = { ... } explicitly rather than letting Kotlin infer the type from the composable's signature, update the import to com.google.android.libraries.ads.mobile.sdk.common.LoadAdError.

Everything else:

  • Call MobileAds.initialize() once at app startup if you haven't already — see Next-Gen GMA SDK. AdManageKit doesn't do this for you.
  • Bump compileSdk to 37+.
  • If you relied on BannerAdView/BannerAdProvider's pause()/resume() actually pausing ad refresh, that's now a no-op — the Next-Gen SDK's AdView doesn't expose it.
  • Google Play Billing 9.1.0 needs no changes if you only use AppPurchase/PurchaseItem.

See Release Notes v4.2.0 for the full technical writeup.

Migrating to 3.0.0

Version 3.0.0 is fully backward compatible. Optionally adopt new features:

Smart Splash Ads (Recommended)

Replace separate load + show calls with single showOrWaitForAd():

// Before (v2.9.0) - Two-step approach
AdManager.getInstance().loadInterstitialAdForSplash(this, adUnitId, 10_000, object : AdManagerCallback() {
    override fun onNextAction() {
        AdManager.getInstance().forceShowInterstitial(this@SplashActivity, callback)
    }
})

// After (v3.0.0) - Single smart call
AdManager.getInstance().showOrWaitForAd(
    activity = this,
    callback = object : AdManagerCallback() {
        override fun onNextAction() { navigateNext() }
    },
    timeoutMillis = 10_000
)

Ad Pool for Higher Show Rate

// Before (single ad unit)
AdManager.getInstance().loadInterstitialAd(context, "single_unit")

// After (multiple ad units for redundancy)
AdManager.getInstance().loadMultipleAdUnits(context, "high_ecpm", "medium_ecpm", "fallback")

App Open Ad Prefetching

// Prefetch before external intents
appOpenManager.prefetchNextAd()
startActivityForResult(cameraIntent, REQUEST_CODE)

Migrating to 2.9.0

Version 2.9.0 has one breaking change for consumable products:

Consumables are no longer auto-consumed. You must manually call consumePurchase():

// Before v2.9.0 (auto-consume)
AppPurchase.getInstance().setConsumePurchase(true)  // Deprecated

// After v2.9.0 (manual consume)
AppPurchase.getInstance().setPurchaseHistoryListener(object : PurchaseHistoryListener {
    override fun onNewPurchase(productId: String, purchase: PurchaseResult) {
        grantItems(productId, purchase.quantity)
        AppPurchase.getInstance().consumePurchase(productId)  // Manual!
    }
    override fun onPurchaseConsumed(productId: String, purchase: PurchaseResult) { }
})

Use Purchase Categories for better product classification:

// Before
PurchaseItem("coins", TYPE_IAP.PURCHASE, true)  // isConsumable

// After (explicit categories)
PurchaseItem("coins", TYPE_IAP.PURCHASE, PurchaseCategory.CONSUMABLE)
PurchaseItem("remove_ads", TYPE_IAP.PURCHASE, PurchaseCategory.REMOVE_ADS)

Migrating to 2.8.0

Version 2.8.0 is fully backward compatible with one behavioral change:

forceShowInterstitial() now respects loading strategy:

// If you need old behavior (always force fetch), use:
AdManager.getInstance().forceShowInterstitialAlways(activity, callback)

// Or set strategy to ON_DEMAND globally:
AdManageKitConfig.interstitialLoadingStrategy = AdLoadingStrategy.ON_DEMAND

Migrating to 2.7.0

Version 2.7.0 is fully backward compatible. Optionally adopt new features:

// Old way (still works)
val nativeBannerMedium = NativeBannerMedium(context)
nativeBannerMedium.loadNativeBannerAd(activity, adUnitId)

// New unified approach
val nativeTemplateView = NativeTemplateView(context)
nativeTemplateView.setTemplate(NativeAdTemplate.CARD_MODERN)
nativeTemplateView.loadNativeAd(activity, adUnitId)

Sample Project

The app module demonstrates all features. To run:

  1. Clone: git clone https://github.com/i2hammad/AdManageKit.git
  2. Open in Android Studio
  3. Replace placeholder AdMob IDs
  4. Run on device or emulator

Contributing

  1. Fork the repository
  2. Create a branch (git checkout -b feature/YourFeature)
  3. Commit changes (git commit -m 'Add YourFeature')
  4. Push (git push origin feature/YourFeature)
  5. Open a Pull Request

License

Licensed under the MIT License. See LICENSE.

Support

Buy me a coffee

For issues: GitHub Issues or hammadmughal0001@gmail.com

About

AdManageKit is an open-source Android library designed to simplify the management of Google AdMob ads,tROI firebase tracking, in-app billing, and User Messaging Platform (UMP) consent in Android applications.

Resources

Contributing

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages