Ads Flow Audit

Dog Translator · splash→intro→main

Báo cáo kỹ thuật · AdMob

Đánh giá flow quảng cáo & rủi ro crash

Trace toàn bộ luồng ads first-run: Splash → Language → Intro → Welcome → Main. Soi ảnh hưởng match_rate, show_rate, hiệu năng và crash. Chưa động code — tài liệu này là spec để bắt tay sửa.

3
P0 · Crash & GDPR
4
Ảnh hưởng doanh thu
6
Perf / RAM / logic
2
Fix < 1 buổi

Luồng ads first-run

traced
0
Application
App start
preload p_aoa
1
Splash
SplashScreen
wait config ≤9spreload NATIVE_LANGINTER_SPLASH
2
Language
Chọn ngôn ngữ
NATIVE_LANG (cache)preload OB
3
Intro
Onboarding · ViewPager
NATIVE_OB1–4OB12/OB23_FULL
4
Welcome
Get Started
INTER_WELCOME
5
Main
Home
no entry adINTER_HOMENATIVE_POST
load / preload show blocking wait

Bảng ưu tiên

13 findings
Ưu tiênIDVấn đềLoạiFile
P0C1ad.show() không guard isFinishing/isDestroyedCrashInter/Reward/AppOpenAdsManager
P0C2onAdFailedToShow quên onLoadingEnd → hard-lockBugInterstitialAdsManager.kt:119
P0M1Không có UMP / consent (GDPR)match_ratetoàn app
P1S1Inter timeout 25s bỏ fill trễ, không cacheshow_rateInterstitialAdsManager.kt
P1P2getAdConfig() re-parse JSON mỗi callperfAdRemoteConfig.kt:59
P2S2Preload native cho page user không tớishow_rateSplash / Intro
P2P1Splash busy-wait Remote Config ≤9sperfSplashScreenActivity.kt:71
P2P3offscreenPageLimit = size-1perfIntroScreenActivity.kt:187
P2P4Init audio-hw trên main threadperf/ANRMainActivity.kt:669
P2C3Không nav guard / click không debounceBugSplash/Intro/Welcome
P2C4Splash không set isShowOpen=falseBugSplashScreenActivity
P2C5Native cache không evictRAMNativeAdsManager
P2C6AOA fetch trước khi config readyLogicMyApplication.kt:47

P0 — Fix ngay

Crash & Compliance
C1Guard Activity trước mọi ad.show()Crash

Vấn đề
Load ad async (timeout ≤25s). User rời màn khi đang chờ → WeakReference<Activity> chưa null nhưng Activity đã chết → ad.show() ném BadTokenException / IllegalStateException.

Vị trí
InterstitialAdsManager.kt:129 RewardAdsManager.kt:145 AppOpenAdManager.kt:78

Repo đã có helper Core/utils/Utils.kt:27

// onAdLoaded
val activity = activityRef.get()
if (activity == null || activity.isFinishing || activity.isDestroyed) {
    onLoadingEnd?.invoke()
    onActionPerform.invoke()   // vẫn nav, không show
    return
}
ad.show(activity)
Verify: mở màn có inter → bấm back/home ngay khi đang load → không crash, không show ad trên màn chết.
C2Gọi onLoadingEnd khi show failHard-lock

Vấn đề
LoadingInterAdsDialogsetCancelable(false). Inter load OK nhưng show fail (ad fullscreen khác đang hiện / activity state đổi) → callback fail quên tắt dialog → dialog modal kẹt vĩnh viễn → user phải kill app.

Vị trí
InterstitialAdsManager.kt:119-126

override fun onAdFailedToShowFullScreenContent(adError: AdError) {
    AdMobAdsManager.isShowingInterRewardAds = false
    onLoadingEnd?.invoke()      // ← THÊM DÒNG NÀY
    onActionPerform.invoke()
}
Verify: ép show fail (trigger 2 inter gần nhau) → dialog phải tắt, màn không kẹt.
M1Tích hợp UMP Consent (GDPR)match_rate

Vấn đề
Zero ref UserMessagingPlatform / ConsentInformation toàn repo. Request native + inter tại splash/language không có consent signal → fill rớt mạnh vùng EEA/UK (eCPM cao nhất). Rủi ro match_rate lớn nhất + rủi ro policy.

val params = ConsentRequestParameters.Builder().build()
consentInfo = UserMessagingPlatform.getConsentInformation(this)
consentInfo.requestConsentInfoUpdate(this, params, {
    UserMessagingPlatform.loadAndShowConsentFormIfRequired(this) { err ->
        if (consentInfo.canRequestAds()) initializeAdsAndPreload()
    }
}, { /* error → fallback theo policy */ })
Verify: set test region = EEA → form consent hiện trước ad → chọn consent → ad load fill bình thường.

P1 — Tăng doanh thu

show_rate & perf rẻ
S1Cache interstitial (tách load/show)show_rate

Vấn đề
loadAndShowAds fresh load mỗi lần; withTimeout(25_000) cancel coroutine — fill về sau 25s bị drop, không cache lại → mất luôn. show_rate loss trực tiếp.

Vị trí InterstitialAdsManager.kt:31 (timeout) · :147

Hướng fix

• Tách load() giữ InterstitialAd trong field + show() riêng.
• Preload INTER_SPLASH sớm ở splash; INTER_WELCOME ở Intro; preload next sau onAdDismissed.
• Giảm timeout 25s → 8-10s (đa số fill về <3s).
• Fill trễ vẫn nằm cache → dùng lần show sau, không rớt.

Verify: bấm tab Home nhiều lần → inter show tức thì (từ cache), không có dialog chờ dài.
P2Memoize getAdConfig()perf

Vấn đề
getAdConfig() tạo new Gson() + parse full JSON mỗi call, main thread, không cache. bindNativeAd gọi ~5-6 lần/native. Cả intro build, mỗi menu click, buildTrainingListWithAds → jank lúc transition.

Vị trí AdRemoteConfig.kt:59-73 AdConfig.kt:24-32

@Volatile private var cachedConfig: AdConfig? = null

fun getAdConfig(): AdConfig =
    cachedConfig ?: AdConfig.fromJson(remoteConfig.getString("ad_config"))
        .also { cachedConfig = it }

// invalidate khi fetchAndActivate() success:
cachedConfig = null
Verify: đo trace lúc scroll training list / chuyển tab → không còn spike Gson parse.

P2 — Cải thiện thêm

perf · RAM · logic
S2Chỉ preload native page N+1show_rate

Bỏ preload up-front NATIVE_OB2/OB23_FULL nếu tỉ lệ thoát onboarding cao. Mở rộng pattern preload-lăn-dần đã có (IntroScreenActivity.kt:65-82). Đo show_rate từng placement trên AdMob console; placement preload nhiều mà show thấp → giảm preload.

P1Bỏ busy-wait splashperf

SplashScreenActivity.kt:71-83 loop 30×300ms = ≤9s. Offline = treo 9s. Fix: bake default ad_config (enabled:true) dùng ngay, fetch config async không chặn nav, hard cap splash ~3s.

P3offscreenPageLimitperf

IntroScreenActivity.kt:187: size-1 tạo hết mọi slide + native fragment cùng lúc → jank + RAM. Về default (1); native chỉ load khi page gần tới.

P4Init nặng khỏi main threadperf/ANR

MainActivity.kt:669-683: MediaPlayer.setDataSource + Equalizer init audio-hw trên main thread trong initData (sync trong onCreate). Đẩy sang IO / lazy khi tab Whistle được chọn.

C3Nav guard + debounceBug

Splash navigateNextScreen(), Intro startActivity(), Welcome tvGetStarted: thêm cờ hasNavigated + đổi sang setOnSafeClick (repo có SafeOnClickListener / ViewExtension.kt:76). Chặn double-tap launch 2 Activity.

C4Suppress AOA trong splashBug

Set AdMobAdsManager.isShowOpen = false khi vào splash, = true khi rời. Tránh AOA fullscreen đè splash khi background→resume trong lúc chờ config.

C5Evict native cacheRAM

NativeAdsView.onDetachedFromWindow gọi release() cho intro fragment + Main training native. Hiện chỉ Language + TrainingItemAdapter release → cache phình dần suốt process.

C6Move AOA fetch sau config readyLogic

MyApplication.kt:47: fetchAd() chạy trong MobileAds.initialize callback trước khi Remote Config activate → getAdConfig() trả empty(enabled=false) → AOA không preload lần đầu. Move vào callback sau khi AdRemoteConfig activate xong.

Thứ tự thực thi đề xuất: ① C1+C2 (1 buổi — chặn crash + hard-lock, ship hotfix) → ② M1 UMP (compliance + match_rate) → ③ S1+P2 (revenue, sửa rẻ) → ④ phần P2 còn lại theo bandwidth. Sau sửa: build pass 2 flavor, test cold-start online/offline, double-tap, EEA region.