Language / Ngôn ngữ / भाषा: English | Tiếng Việt | हिन्दी
This document is the mandatory reference standard for partner teams integrating ads into Infinity products. Any ad-related change must follow the architecture, load/show flow, and gating rules defined in this base project.
Example-AdLogic-Partner is designed as the template/base for all Android apps in the ecosystem. Partners should fork or clone this base to keep:
- The same Ads package structure (
AdRemoteConfig,RemoteConfigUtils,AdsManager,AdExtension). - The same config source flow (assets + Firebase Remote Config).
- The same LiveData observation pattern for native ad rendering.
- The same QA entry through DevSetting on Language screen.
Goal: reduce app-to-app drift, improve maintainability, simplify audits, and centralize technical support.
The current base flow — early init in GlobalApp, config sync in Splash, next-screen preload, centralized gating in AdsManager, and organic handling via ERainAd.getShouldDisplay*(enableUaCheck) — is the result of iterative optimization for:
- Correct load timing
- Reduced UI jank
- Safe fallback when offline / purchased
- Cohort-based display control
Partners must not change the core flow (for example: calling SDK directly and bypassing AdsManager, removing organic gating, or changing load/show order) unless approved by Infinity technical team.
The following screens are already implemented and must preserve load/show behavior, preload points, and gating conditions:
| Screen | Placements / behavior |
|---|---|
| Splash | inter_splash, preload native_language, open_resume config |
| Language | Native language/click, preload onboarding page 1, DevSetting (tvTitle) |
| Onboarding | Native page 1 & 4, native full, inter_onboarding, uninstall widget |
| Welcome / Resume | native_welcome, inter_welcome, ResumeAdsEntryRule |
Banner (Home + screens extending BaseActivityWithBanner) |
Normal / collapsible banner, reload by config |
When customizing UI, only adjust layout/container. Do not remove isEnable, purchase, network, or getShouldDisplay*(config.enableUaCheck) checks.
For any new custom screen (not already present in base), partners must follow the same rule set:
- Add placement keys to
ad_config.json/ad_config_debug.jsonand add matching properties inAdRemoteConfig. - Add load methods in
AdsManager(native vialoadNativeInternal, interstitial viaload+showpattern). - In Activity/Fragment: load in
initViews(optional shortpostDelayed), observe LiveData, callpopulateNativeAdViewwhen ad exists, hide container onnull. - For sensitive placements (onboarding-like, welcome, home, permission, widget...): 100% mandatory to attach
ERainAd.getInstance().getShouldDisplay*(config.enableUaCheck)using the mapping in section 4. - For banners: extend
BaseActivityWithBanner, configureBannerConfig, do not load banner outsideAdsManager.loadBanner.
Detailed UI/Ads reference (CTA size, Done button delay, native placement by page): Infinity UI Documentation — Language & Onboarding.
- Debug: read
ad_config_debug.json. - Release: read
ad_config.json, then optionally override from Firebase Remote Config (ad_remote_config).
Order in GlobalApp.onCreate() (mandatory):
| Step | Call | Purpose |
|---|---|---|
| 1 | MobileAds.initialize(this) |
Initialize Google Mobile Ads SDK |
| 2 | DevConfig.init(...) |
DevConfig UI — ads lib versions (see §1.3) |
| 3 | initAdRemoteConfig() |
AdRemoteConfig.initializeFromAssets(this) |
| 4 | initAds() |
ERainAd + resume/inter rules (see §1.5) |
| 5 | ResumeAdsEntryRule.shouldShowWelcomeOnResume() |
Register AppLifecycleObserver when welcome flow applies |
SplashActivity.checkRemoteConfigResult():AdRemoteConfig.initialize(this, RemoteConfigUtils.getAdRemoteConfig())to apply latest remote config.
Call early in onCreate(), before initAdRemoteConfig() and initAds(). Version parameters come from BuildConfig (must be declared in app/build.gradle — see §1.4):
DevConfig.init(
context = this,
nkhStudioVersion = BuildConfig.ERAIN_STUDIO_VERSION,
playServicesAdsVersion = BuildConfig.PLAY_SERVICES_ADS_VERSION,
gdprModuleVersion = BuildConfig.GDPR_MODULE_VERSION
)| Parameter | BuildConfig field |
Shown on DevConfig UI |
|---|---|---|
nkhStudioVersion |
ERAIN_STUDIO_VERSION |
ERain Studio / ads module version |
playServicesAdsVersion |
PLAY_SERVICES_ADS_VERSION |
Google Play Services Ads version |
gdprModuleVersion |
GDPR_MODULE_VERSION |
GDPR module version |
LanguageActivity:mBinding.tvTitle.setOnAdminAdToggleListener()- QA can check: sdk versions, mediation, config id, ad id, reset organic.
Mandatory in
app/build.gradle: to make DevConfig UI show version info correctly, partners must declare all 3buildConfigFieldlines below (in bothdebugandrelease):buildConfigField "String", "ERAIN_STUDIO_VERSION", "\"$erain_studio_version\"" buildConfigField "String", "PLAY_SERVICES_ADS_VERSION", "\"$play_services_ads_version\"" buildConfigField "String", "GDPR_MODULE_VERSION", "\"$module_update_gdpr_version\""
DevConfig testing guide (PO / Tester): DevConfig Testing Guide
In the current base, the core integration is implemented in GlobalApp.initAds(). Partners should keep this pattern when creating new apps:
- Select
environmentby build type (ERainAdConfig.ENVIRONMENT_DEVELOP/ERainAdConfig.ENVIRONMENT_PRODUCTION). - Create
mERainAdConfig = ERainAdConfig(this, environment). - Set required config fields before calling
ERainAd.init(...):adjustConfigfacebookClientTokenadjustTokenTiktokintervalInterstitialAdidAdResume
- Call
ERainAd.getInstance().init(this, mERainAdConfig). - Apply extra resume/inter rules:
Admob.getInstance().setDisableAdResumeWhenClickAds(true)Admob.getInstance().setOpenActivityAfterShowInterAds(true)AppOpenManager.getInstance().disableAppResumeWithActivity(...)for excluded screens.
Reference snippet:
private fun initAds() {
val environment =
if (BuildConfig.DEBUG) ERainAdConfig.ENVIRONMENT_DEVELOP else ERainAdConfig.ENVIRONMENT_PRODUCTION
mERainAdConfig = ERainAdConfig(this, environment)
mERainAdConfig.adjustConfig = AdjustConfig(true, resources.getString(R.string.adjust_token))
mERainAdConfig.facebookClientToken = resources.getString(R.string.facebook_client_token)
mERainAdConfig.adjustTokenTiktok = resources.getString(R.string.event_token)
mERainAdConfig.intervalInterstitialAd = 35
mERainAdConfig.idAdResume = ""
ERainAd.getInstance().init(this, mERainAdConfig)
}Note:
initAdRemoteConfig()should still run beforeinitAds(), and remote config is still synced inSplashActivityviaRemoteConfigUtils.init(...)+AdRemoteConfig.initialize(...).
- Inter Splash:
- Condition:
AdRemoteConfig.inter_splash.isEnable == trueand network available. - API:
ERainAd.getInstance().loadSplashInterstitialAds(...). - On successful load (
onAdLoaded), preloadnative_language.
- Condition:
- Open Resume:
- Enabled/disabled by
ResumeAdsEntryRule.shouldEnableOpenResume().
- Enabled/disabled by
- Native language:
- Preload from Splash:
AdsManager.loadNativeLanguage(...) - Click variant:
AdsManager.loadNativeLanguageClick(...)
- Preload from Splash:
- Early preload for onboarding page 1:
AdsManager.loadNativeOnboarding1(...)
AdsManager.loadNativeOnboarding4(...)AdsManager.loadNativeOnboardingFull(...)AdsManager.loadInterOnboarding(...), then show viaAdsManager.showInterOnboarding(...)at onboarding completion.
- Native welcome:
AdsManager.loadNativeWelcome(...), gated bygetShouldDisplayNativeWelcomeBack(config.enableUaCheck).
- Inter welcome:
AdsManager.loadInterWelcome(...),AdsManager.showInterWelcome(...).- Welcome flow is triggered by
AppLifecycleObserverwhenResumeAdsEntryRule.shouldShowWelcomeOnResume()andgetShouldDisplayInterWelcomeBack(AdRemoteConfig.inter_welcome.enableUaCheck)allow it.
- Use
BaseActivityWithBanner. AdsManager.loadBanner(..., isCollapse = false)=> normal banner.AdsManager.loadBanner(..., isCollapse = true)=> collapsible banner (SDK expand/collapse behavior).- Reload interval follows
reloadIntervalSeconds.
In AdsManager, an ad loads only when all conditions pass:
adUnitConfig.isEnable == true!AppPurchase.getInstance().isPurchased(...)- Network available
- For mandatory organic-gated placements:
getShouldDisplay*(config.enableUaCheck) == true
If any condition fails, native LiveData emits null so UI hides the ad container.
Mandatory: 100% of the placements below must also check the SDK
getShouldDisplay*method.
The parameter isenableUaCheckfrom the placement config inad_config.json/ad_config_debug.json(mapped toAdUnitConfig.enableUaCheck).
This is the organic/UA check flag (force organic via ads config) — do not hard-codetrue/false; always take it from the config of the placement being loaded/shown.
| Ad placement | Required SDK method | Default enable_ua_check in ad_config.json |
Param from ad_config | Code usage |
|---|---|---|---|---|
| NativeOnboardingFull1 | getShouldDisplayNativeOnboardingFull1(...) |
true |
config.enableUaCheck |
AdsManager.loadNativeOnboardingFull (+ full page insert in OnBoardingActivity) |
| NativeOnboardingFull2 | getShouldDisplayNativeOnboardingFull2(...) |
true |
config.enableUaCheck |
AdsManager.loadNativeOnboardingFull2 (+ full page insert in OnBoardingActivity) |
| NativeOnboardingNormal2 | getShouldDisplayNativeOnboardingNormal2(...) |
false |
config.enableUaCheck |
AdsManager.loadNativeOnboarding4 |
| NativeHome | getShouldDisplayNativeHome(...) |
false |
config.enableUaCheck |
AdsManager.loadNativeHome |
| NativePermission | getShouldDisplayNativePermission(...) |
false |
config.enableUaCheck |
AdsManager.loadNativePermission |
| InterOnboarding | getShouldDisplayInterOnboarding(...) |
true |
config.enableUaCheck |
AdsManager.loadInterOnboarding / showInterOnboarding |
| NativeWelcomeBack | getShouldDisplayNativeWelcomeBack(...) |
false |
config.enableUaCheck |
AdsManager.loadNativeWelcome |
| InterWelcomeBack | getShouldDisplayInterWelcomeBack(...) |
false |
config.enableUaCheck |
AppLifecycleObserver (welcome screen redirect) |
| WidgetUninstall | getShouldDisplayWidgetUninstall(...) |
false |
config.enableUaCheck |
OnBoardingActivity widget shortcut; loadNativeSurvey / loadNativeConfirmUninstall |
ad_config defaults: when declaring JSON, set
enable_ua_checkto the default in the column above unless Infinity specifies otherwise. Example: Full1/Full2/inter_onboardingdefaulttrue; remaining placements defaultfalse.
In JSON for each placement:
"native_onboarding_fullscreen_1_3": {
"id": "ca-app-pub-xxx/yyy",
"isEnable": true,
"enable_ua_check": true
}In code:
val config = AdRemoteConfig.native_onboarding_fullscreen_1_3
ERainAd.getInstance().getShouldDisplayNativeOnboardingFull1(config.enableUaCheck)| JSON key | Kotlin field | Meaning |
|---|---|---|
enable_ua_check |
AdUnitConfig.enableUaCheck |
Enable/disable organic (UA) check for that placement when calling getShouldDisplay* |
loadNativeInternal(
activity,
config,
layoutRes,
liveData,
ERainAd.getInstance().getShouldDisplayNativeOnboardingFull1(config.enableUaCheck)
)Not compliant if:
- Skipping
getShouldDisplay*for any placement in the table above. - Calling
getShouldDisplay*(true/false)with a hard-coded value instead ofconfig.enableUaCheck. - Using the wrong gate method for a placement (e.g. Full1 using Normal2).
Organic is user classification from Ads SDK / growth logic used to:
- Reduce frequency or disable sensitive ad slots for certain users
- Balance retention, UX, and revenue
- Apply cohort rules without rewriting each screen
How it works in this app:
- The app does not compute organic with local rules.
- The app calls
ERainAd.getInstance().getShouldDisplay*(enableUaCheck)withenableUaCheckfromad_config. - When organic/cohort rules change, these method results change and directly affect load/show per slot.
- DevSetting / Unlimited Ads +
reset organichelp QA re-verify all ad placements + uninstall widget.
if (AdRemoteConfig.inter_splash.isEnable && isNetwork(this)) {
ERainAd.getInstance().loadSplashInterstitialAds(
this, AdRemoteConfig.inter_splash.id, 30000, 5000, object : AdCallback() {
override fun onNextAction() { moveActivity() }
}
)
} else moveActivity()AdsManager.loadNativeOnboarding1(this, appSharedPref.firstOnBoarding, R.layout.layout_native_onboarding)
AdsManager.nativeOnboarding1AdLive.observe(this) { ad ->
if (ad == null) hideAd() else showAd(ad)
}AdsManager.loadInterOnboarding(this)
AdsManager.showInterOnboarding(this) {
goNextScreen()
}override val bannerConfig = BannerConfig(
adUnitConfig = AdRemoteConfig.banner_home,
isCollapse = false
)override val bannerConfig = BannerConfig(
adUnitConfig = AdRemoteConfig.banner_home,
isCollapse = true
)- Infinity UI Documentation — Language & Onboarding — UI, Remote Config, and ad-unit display conditions (placement/method names should be cross-checked against this document).