androidkotlinadmobandroid-jetpack-composeinterstitial

InterstitialAd and java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 534336 bytes


I am trying to implement InterstitialAd in Jetpack Compose. This is my AdsHelpder.kt class:

class AdsHelper(private val context: Context) : FullScreenContentCallback() {

    private var onAdDismissed: (() -> Unit)? = null
    private var interstitialAd: InterstitialAd? = null
    private val adRequest = AdRequest.Builder().build()

    fun onDestroy() {
        interstitialAd?.fullScreenContentCallback = null
        interstitialAd = null
    }

    
    fun onLoadInterstitialAd(canLoad: Boolean) {
        val id = context.onString(R.string.interstitial_ad_id)
        if (canLoad) {
            InterstitialAd.load(context, id, adRequest, object : InterstitialAdLoadCallback() {
                override fun onAdLoaded(p0: InterstitialAd) {
                    interstitialAd = p0
                }

                override fun onAdFailedToLoad(p0: LoadAdError) {
                    interstitialAd = null
                }
            })
        }
    }

    override fun onAdDismissedFullScreenContent() {
        interstitialAd = null
        onAdDismissed?.invoke()
    }

    fun onShowInterstitialAd(onAdDismissed: () -> Unit) {
        interstitialAd?.apply {
            show(context.onMainActivity())
            fullScreenContentCallback = this@AdsHelper
        }
        this.onAdDismissed = onAdDismissed
    }

    override fun onAdFailedToShowFullScreenContent(e: AdError) {
        interstitialAd = null
    }
}

Since I need to show the InterstitialAd in several screens, I think this implementation way looks optimal for MainActivity.kt:

class MainActivity : ComponentActivity() {

    private lateinit var data: Data
    private var adsHelper: AdsHelper? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        data = ViewModelProvider(this)[Data::class.java]

        adsHelper = AdsHelper(this)

        setContent {
            AppTheme {

                val controller = rememberAnimatedNavController()

                controller.addOnDestinationChangedListener { _, route, _ ->
                   adsHelper?.onLoadInterstitialAd(route != HOME)
                }

                LaunchedEffect(data.interstitial) {
                    when {
                        data.interstitial -> adsHelper?.onShowInterstitialAd {
                            data.interstitial = false
                        }
                    }
                }

                Column {
                    Scaffold(
                        Modifier.weight(1f)
                    ) { 
                       NavGraph(controller, data)
                    }                
                }
            }
        }
    }

    override fun onDestroy() {
        adsHelper?.onDestroy()
        super.onDestroy()
    }
}

Here the Data.kt:

class Data(app: Application) : AndroidViewModel(app) {
    var interstitial by mutableStateOf(false)
}

I could avoide these lines in the MainActivity.kt

...
controller.addOnDestinationChangedListener { _, route, _ ->
    adsHelper?.onLoadInterstitialAd(route != HOME)
}
...

and directly call adsHelper?.onLoadInterstitialAd(true), but it causes endless logs:

...
D/CCodecConfig: c2 config diff is   c2::u32 default.color.matrix = 1
      c2::u32 default.color.primaries = 1
      c2::u32 default.color.range = 2
      c2::u32 default.color.transfer = 3
      c2::u32 raw.max-size.height = 360
      c2::u32 raw.max-size.width = 640
      c2::u32 raw.size.height = 360
      c2::u32 raw.size.width = 640
W/Codec2Client: query -- param skipped: index = 1107298332.
D/CCodec: client requested max input size 86400, which is smaller than what component recommended (6291456); overriding with component recommendation.
W/CCodec: This behavior is subject to change. It is recommended that app developers double check whether the requested max input size is in reasonable range.
D/CCodec: encoding statistics level = 0
D/CCodec: setup formats input: AMessage(what = 0x00000000) = {
      int32_t height = 360
      int32_t level = 256
      int32_t max-input-size = 6291456
      string mime = "video/x-vnd.on2.vp9"
      int32_t profile = 1
      int32_t width = 640
      Rect crop(0, 0, 639, 359)
    }
D/CCodec: setup formats output: AMessage(what = 0x00000000) = {
      int32_t android._color-format = 2135033992
      int32_t android._video-scaling = 1
      Rect crop(0, 0, 639, 359)
      int32_t color-standard = 1
      int32_t color-range = 2
      int32_t color-transfer = 3
      Buffer hdr10-plus-info = {
      }
      int32_t height = 360
      int32_t max-height = 360
      int32_t max-width = 640
      string mime = "video/raw"
      int32_t rotation-degrees = 0
      int32_t sar-height = 1
      int32_t sar-width = 1
      int32_t width = 640
      int32_t android._dataspace = 260
      int32_t color-format = 2130708361
    }
I/CCodecConfig: query failed after returning 11 values (BAD_INDEX)
W/Codec2Client: query -- param skipped: index = 1107298332.
D/CCodec: client requested max input size 86400, which is smaller than what component recommended (6291456); overriding with component recommendation.
W/CCodec: This behavior is subject to change. It is recommended that app developers double check whether the requested max input size is in reasonable range.
D/CCodec: encoding statistics level = 0
D/CCodec: setup formats input: AMessage(what = 0x00000000) = {
      int32_t height = 360
      int32_t level = 256
      int32_t max-input-size = 6291456
      string mime = "video/x-vnd.on2.vp9"
      int32_t profile = 1
      int32_t width = 640
      Rect crop(0, 0, 639, 359)
    }
D/CCodec: setup formats output: AMessage(what = 0x00000000) = {
      int32_t android._color-format = 2135033992
      int32_t android._video-scaling = 1
      Rect crop(0, 0, 639, 359)
      int32_t color-standard = 1
      int32_t color-range = 2
      int32_t color-transfer = 3
      Buffer hdr10-plus-info = {
      }
      int32_t height = 360
      int32_t max-height = 360
      int32_t max-width = 640
      string mime = "video/raw"
      int32_t rotation-degrees = 0
      int32_t sar-height = 1
      int32_t sar-width = 1
      int32_t width = 640
      int32_t android._dataspace = 260
      int32_t color-format = 2130708361
    }
I/CCodecConfig: query failed after returning 11 values (BAD_INDEX)
D/MediaCodec: keep callback message for reclaim
D/MediaCodec: keep callback message for reclaim
W/Codec2Client: query -- param skipped: index = 1342179345.
W/Codec2Client: query -- param skipped: index = 2415921170.
W/Codec2Client: query -- param skipped: index = 1342179345.
W/Codec2Client: query -- param skipped: index = 2415921170.
W/Codec2Client: query -- param skipped: index = 2684356609.
W/Codec2Client: query -- param skipped: index = 2684356609.
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#948] Created input block pool with allocatorID 16 => poolID 60 - OK (0)
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#415] Created input block pool with allocatorID 16 => poolID 59 - OK (0)
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#948] Query output surface allocator returned 0 params => BAD_INDEX (6)
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#415] Query output surface allocator returned 0 params => BAD_INDEX (6)
I/CCodecBufferChannel: [c2.goldfish.vp9.decoder#415] Created output block pool with allocatorID 18 => poolID 151 - OK
I/CCodecBufferChannel: [c2.goldfish.vp9.decoder#948] Created output block pool with allocatorID 18 => poolID 152 - OK
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#415] Configured output block pool ids 151 => OK
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#948] Configured output block pool ids 152 => OK
D/Codec2-OutputBufferQueue: remote graphic buffer migration 0/0
D/Codec2-OutputBufferQueue: remote graphic buffer migration 0/0
D/Codec2Client: setOutputSurface -- failed to set consumer usage (6/BAD_INDEX)
D/Codec2Client: setOutputSurface -- generation=9693208 consumer usage=0x900
D/Codec2Client: setOutputSurface -- failed to set consumer usage (6/BAD_INDEX)
D/Codec2Client: setOutputSurface -- generation=9693207 consumer usage=0x900
D/Codec2Client: Surface configure completed
D/Codec2Client: Surface configure completed
I/DynamiteModule: Considering local module com.google.android.gms.ads.dynamite:0 and remote module com.google.android.gms.ads.dynamite:230500000
I/DynamiteModule: Selected remote version of com.google.android.gms.ads.dynamite, version >= 230500000
D/CCodecConfig: c2 config diff is   c2::u32 raw.channel-mask.value = 12
D/CCodecBuffers: [c2.android.aac.decoder#912:Output[N]] popFromStashAndRegister: at 1000000000000us, output format changed to AMessage(what = 0x00000000) = {
      int32_t aac-drc-album-mode = 0
      int32_t aac-drc-boost-level = 127
      int32_t aac-drc-cut-level = 127
      int32_t aac-drc-effect-type = 3
      int32_t aac-drc-output-loudness = -1
      int32_t aac-encoded-target-level = -1
      int32_t aac-max-output-channel_count = 8
      int32_t aac-target-ref-level = 64
      int32_t channel-count = 2
      int32_t channel-mask = 12
      int32_t max-output-channel-count = 8
      string mime = "audio/raw"
      int32_t sample-rate = 44100
      int32_t android._config-pcm-encoding = 2
    }
D/CCodecConfig: c2 config diff is   c2::u32 raw.crop.height = 360
      c2::u32 raw.crop.left = 0
      c2::u32 raw.crop.top = 0
      c2::u32 raw.crop.width = 640
D/CCodecConfig: c2 config diff is   c2::u32 raw.channel-mask.value = 12
D/CCodecBuffers: [c2.android.aac.decoder#806:Output[N]] popFromStashAndRegister: at 1000000000000us, output format changed to AMessage(what = 0x00000000) = {
      int32_t aac-drc-album-mode = 0
      int32_t aac-drc-boost-level = 127
      int32_t aac-drc-cut-level = 127
      int32_t aac-drc-effect-type = 3
      int32_t aac-drc-output-loudness = -1
      int32_t aac-encoded-target-level = -1
      int32_t aac-max-output-channel_count = 8
      int32_t aac-target-ref-level = 64
      int32_t channel-count = 2
      int32_t channel-mask = 12
      int32_t max-output-channel-count = 8
      string mime = "audio/raw"
      int32_t sample-rate = 44100
      int32_t android._config-pcm-encoding = 2
    }
I/DynamiteModule: Considering local module com.google.android.gms.ads.dynamite:0 and remote module com.google.android.gms.ads.dynamite:230500000
I/DynamiteModule: Selected remote version of com.google.android.gms.ads.dynamite, version >= 230500000
V/FA: Inactivity, disconnecting from the service
D/BufferPoolAccessor2.0: evictor expired: 4, evicted: 4
I/DynamiteModule: Considering local module com.google.android.gms.ads.dynamite:0 and remote module com.google.android.gms.ads.dynamite:230500000
I/DynamiteModule: Selected remote version of com.google.android.gms.ads.dynamite, version >= 230500000
I/DynamiteModule: Considering local module com.google.android.gms.ads.dynamite:0 and remote module com.google.android.gms.ads.dynamite:230500000
I/DynamiteModule: Selected remote version of com.google.android.gms.ads.dynamite, version >= 230500000
D/CCodecConfig: c2 config diff is   c2::u32 raw.crop.height = 360
      c2::u32 raw.crop.left = 0
      c2::u32 raw.crop.top = 0
      c2::u32 raw.crop.width = 640
D/BufferPoolAccessor2.0: evictor expired: 1, evicted: 1
I/Ads: This request is sent from a test device.
...

furthermore, while generating those logs and especially showing up a InterstitialAd (when data.interstitial = true), the the app slows down/freezes.

That's why I decided to load the InterstitialAd only in target screens, in this case all screens beside the HOME. But this does not solve the problem and the logs are kept loading endless.

Moreover, according to Crashlytics and App Quality Insights there occurs a TransactionTooLargeException.

enter image description here

But the app does not crash, only freezes until data.interstitial = false. After that the app runs ok.

Obviously, loading and showing up the InterstitialAd consumes a lot of resources/memory, especially, if there is a video ad.

I recently started working with Jetpack Compose and many things are new to me. Therefore, my implementation could be wrong and not really optimal. What might be wrong here?


Solution

  • Firstly, I would recommend that you consider separating your concerns in your classes.


    AdComponents.kt

    class holding your InterstitialAd and AdaptiveBannerAd implementation.

    @Composable
    fun AdMobBanner() {
        val currentWidth = LocalConfiguration.current.screenWidthDp
        AndroidView(factory = { context ->
            AdView(context).apply {
                setAdSize(AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context, currentWidth))
                adUnitId = context.getString(R.string.test_app_banner_id)
                loadAd(AdRequest.Builder().build())
            }
        })
    }
    
    // Remove @Inject annotation if you're not using Hilt
    class AdMobInterstitial @Inject constructor(
        private val context : Context
    ){
        var mInterstitialAd : InterstitialAd? = null
    
        fun loadAd() {
            val adUnitId = context.getString(R.string.test_app_interstitial_id)
            val adRequest = AdRequest.Builder().build()
    
            InterstitialAd.load(
                context, adUnitId, adRequest,
                object : InterstitialAdLoadCallback() {
                    override fun onAdLoaded(ad: InterstitialAd) {
                        mInterstitialAd = ad
                    }
    
                    override fun onAdFailedToLoad(error: LoadAdError) {
                        mInterstitialAd = null
                    }
                }
            )
        }
    
        fun showAd(activity: Activity) {
            mInterstitialAd?.let {
                it.fullScreenContentCallback = object : FullScreenContentCallback() {
                    override fun onAdDismissedFullScreenContent() {
                        super.onAdDismissedFullScreenContent()
                        mInterstitialAd = null
                        loadAd()
                        logger("Ad Dismissed")
                    }
    
                    override fun onAdShowedFullScreenContent() {
                        super.onAdShowedFullScreenContent()
                        logger("Ad Showed Full Screen Content")
                    }
    
                    override fun onAdFailedToShowFullScreenContent(p0: AdError) {
                        super.onAdFailedToShowFullScreenContent(p0)
                        mInterstitialAd = null
                        logger("Ad failed to show fullScreenContent")
                    }
                }
                it.show(activity)
            } ?: loadAd()
        }
    
        private fun logger(msg : String) {
            Log.e("AdComponents",msg)
        }
    }
    

    Feel free to change the above Interstitial class according to your need.

    I suggest creating a separate NavGraph class to contain your composable.

    AppNavGraph

    @OptIn(ExperimentalAnimationApi::class)
    @Composable
    fun AppNavGraph(showAd: () -> Unit) {
        val navController = rememberAnimatedNavController()
        
        AnimatedNavHost(
            navController = navController,
            startDestination = Screens.HomeScreen.name
        ) {
            composable(
                route = "Your screen name",
                enterTransition = { slideInHorizontally (tween(400)) { 1000 } },
                exitTransition = { slideOutHorizontally (tween(400)) { -1000 } },
                popEnterTransition = { slideInHorizontally (tween(400)) { -1000 } },
                popExitTransition = { slideOutHorizontally (tween(400)) { 1000 } },
            ) {
                LaunchedEffect(key1 = Unit) {
                    it.whenCreated { showAd() }
                }
                // call your screen composable here
            }
        }
    }
    

    MainActivity

    with Hilt Dependency Injection.

    @AndroidEntryPoint
    class MainActivity : ComponentActivity() {
    
        @Inject lateinit var adMobInterstitial : AdMobInterstitial
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            adMobInterstitial.loadAd()
            setContent {
                AppTheme {
                    // A surface container using the 'background' color from the theme
                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colorScheme.background
                    ) {
                        AppNavigation {
                            adMobInterstitial.showAd(this)
                        }
                    }
                }
            }
        }
    }
    

    MainActivity

    Without Dependency Injection.

    class MainActivity : ComponentActivity() {
    
        private lateinit var adMobInterstitial : AdMobInterstitial
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            adMobInterstitial = AdMobInterstitial(context = this)
            adMobInterstitial.loadAd()
            setContent {
                AppTheme {
                    // A surface container using the 'background' color from the theme
                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colorScheme.background
                    ) {
                        AppNavigation {
                            adMobInterstitial.showAd(this)
                        }
                    }
                }
            }
        }
    }