SDL on Android with Google Billing or Google Ads

I’m wondering if anyone has in app purchases (Google Play Billing Library) or Google Mobile Ads working in their Android app. I have the basics working, but there is a common bug between the two of them.

To reproduce:

  1. Initiate an in app purchase (launchBillingFlow) - which brings up the Google UI for the player to enter their credit card etc…
  2. While the billing UI screen is up, switch to a different app or go to the home screen - then switch back to my app.
  3. Now leave the billing UI (for example cancel the transaction) so that my app will take over again.
  4. My app never regains control… the app first throws back to the android home screen, then when I try to task switch back to the app, it’s just a black screen - looks like Google UI is still in front (but all black). I don’t know what is happening - I don’t think OnResume is ever called.

A very similar thing happens when I was doing “rewarded ads” using Google Mobile Ads - if I exit and come back while an ad is playing, my game can never regain control.

I was thinking I had screwed up something with my Android project’s settings somehow, so I did barebones tests on the most recent stable SDL 2 and SDL 3 releases. Same issue in both.

This happened to me on multiple real devices. The behaviour is different in the simulator - it sort of works but strangely has the advertisement running as its own task/app.

Unfortunately I think this is actually a crash bug that will be seen by users quite a lot… If an ad starts playing a user is more likely than ever to task switch, but then when they go back to the game it will just be a black screen… pretty bad.

Is there a trick to setting up these APIs? I’m passing SDLActivity to them as the context, this seems correct.
Anyone have these working or know of an Android app that has the APIs implemented? Would like to see if everyone has this same bug.

I’m using both with no issues. How are you instantiating the ads / billing on the Java side?

1 Like

That’s good to hear, so maybe I’m just setting it up wrong. I really don’t know what I’m doing when it comes to Android.

I created a simple test project starting with SDL 3.1.6.

In SDLActivity.java:

// Setup
@Override
protected void onCreate(Bundle savedInstanceState) {
   Log.v(TAG, "Manufacturer: " + Build.MANUFACTURER);
   Log.v(TAG, "Device: " + Build.DEVICE);
   Log.v(TAG, "Model: " + Build.MODEL);
   Log.v(TAG, "onCreate()");

   super.onCreate(savedInstanceState);

   MobileAds.initialize(this, new OnInitializationCompleteListener() {
       @Override
       public void onInitializationComplete(InitializationStatus initializationStatus) {
           admobInitialized = true;

           RequestConfiguration requestConfiguration = new RequestConfiguration.Builder().build();
           MobileAds.setRequestConfiguration(requestConfiguration);
       }
   });

etc...

In SDLActivity.java, this would be called to load the ad:

public void admobLoadAd()
{
   if ( !admobInitialized ) {
       return ;
   }

   if ( admobAdRequested ) {
       return ; // already loading
   }

   admobAdRequested = true ;

   AdRequest adRequest = new AdRequest.Builder().build();
   RewardedAd.load(this,
       "ca-app-pub-3940256099942544/5224354917", // Test
       adRequest, new RewardedAdLoadCallback() {
           @Override
           public void onAdLoaded(@NonNull RewardedAd rewardedAd) {
               // Ad successfully loaded.
               Log.d(TAG, "Ad was loaded");
               mRewardedAd = rewardedAd ;


               admobAdRequested = false ;
           }

           @Override
           public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) {
               // Ad failed to load.
               Log.d(TAG, loadAdError.toString());
               mRewardedAd = null ;


               admobAdRequested = false ;
           }
       });
}

In SDLActivity.java, show the ad:

public void admobShowAd()
{
   if ( !admobInitialized ) {
       return ;
   }

   if ( mRewardedAd != null ) {

       mRewardedAd.setFullScreenContentCallback(new FullScreenContentCallback() {
           @Override
           public void onAdClicked() {
               // Called when a click is recorded for an ad.
               Log.d(TAG, "Ad was clicked.");
           }

           @Override
           public void onAdDismissedFullScreenContent() {
               // Called when ad is dismissed.
               // Set the ad reference to null so you don't show the ad a second time.
               Log.d(TAG, "Ad dismissed fullscreen content.");
               mRewardedAd = null;
           }


           @Override
           public void onAdFailedToShowFullScreenContent(AdError adError) {
               // Called when ad fails to show.
               Log.e(TAG, "Ad failed to show fullscreen content.");
               mRewardedAd = null;
           }

           @Override
           public void onAdImpression() {
               // Called when an impression is recorded for an ad.
               Log.d(TAG, "Ad recorded an impression.");
           }

           @Override
           public void onAdShowedFullScreenContent() {
               // Called when ad is shown.
               Log.d(TAG, "Ad showed fullscreen content.");
           }
       });

       mRewardedAd.show(this, new OnUserEarnedRewardListener() {
           @Override
           public void onUserEarnedReward(@NonNull RewardItem rewardItem) {
               // Handle the reward.
               Log.d(TAG, "The user earned the reward.");
           }
       });
   }
}

I suspect it’s because you are calling into Java from the main thread, and the ads and billing process needs to be called from the Java UI thread. You don’t need to touch SDL’s java code either, do it all from your MainActivity extends SDLActivity.

SDL offers SDL_SendAndroidMessage(), which lets you send custom events to java-side onUnhandledMessage(), which are picked up on the UI thread. This simplifies things a lot because you don’t have to touch jmethodIDs, or jclass.

SDL code:

constexpr Uint32 COMMAND_USER				= 0x8000;
constexpr Uint32 COMMAND_BANNERSHOW	    	= COMMAND_USER + 1;

void AdBannerShow() {
    SDL_SendAndroidMessage(COMMAND_BANNERSHOW, 0);
}

Java:

public class MainActivity extends SDLActivity {
    static final int COMMAND_BANNERSHOW         = COMMAND_USER + 1;

  @Override protected boolean onUnhandledMessage(int command, @Nullable Object param) {
        switch(command) {
            case COMMAND_BANNERSHOW:
                // show ad
                break;
            default:
                return false;
        }
        return true;
    }
}
1 Like

Thank you for your help!
In my real project I was actually extending the SDLActivity class and using “runOnUiThread” for these calls. However, I did not know about SDL_SendAndroidMessage, which does seem more convenient.

I tried your suggested style, but unfortunately, I’m still getting the same result. Here is a new sample project:

Java:

package org.libsdl.app;

import android.util.Log;

import com.google.android.gms.ads.MobileAds;
import com.google.android.gms.ads.AdError;
import com.google.android.gms.ads.RequestConfiguration;
import com.google.android.gms.ads.initialization.InitializationStatus;
import com.google.android.gms.ads.initialization.OnInitializationCompleteListener;
import com.google.android.gms.ads.rewarded.RewardItem;
import com.google.android.gms.ads.rewarded.RewardedAd;
import com.google.android.gms.ads.OnUserEarnedRewardListener;
import com.google.android.gms.ads.FullScreenContentCallback;
import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback;
import com.google.android.gms.ads.LoadAdError;
import com.google.android.gms.ads.AdRequest;
import androidx.annotation.NonNull;

public class MainActivity extends SDLActivity  {

    private boolean admobInitCalled = false ;
    private boolean admobInitialized = false ;
    private boolean admobAdRequested = false ;

    private RewardedAd mRewardedAd ;

    private String ADMOB_LOG_TAG = "admobTest";

    public void admobInit()
    {
        Log.d(ADMOB_LOG_TAG,"calling MobileAds.initialize" );

        admobInitCalled = true ;
        MobileAds.initialize(this, new OnInitializationCompleteListener() {
            @Override
            public void onInitializationComplete(InitializationStatus initializationStatus) {
                admobInitialized = true;

                RequestConfiguration requestConfiguration = new RequestConfiguration.Builder().build();
                MobileAds.setRequestConfiguration(requestConfiguration);

                Log.d(ADMOB_LOG_TAG,"onInitializationComplete" );
            }
        });
    }

    public void admobLoadAd()
    {
        if ( !admobInitialized ) {
            return ;
        }

        if ( admobAdRequested ) {
            return ; // already loading
        }

        admobAdRequested = true ;

        Log.d(ADMOB_LOG_TAG,"calling RewardedAd.load" );

        AdRequest adRequest = new AdRequest.Builder().build();
        RewardedAd.load(this,
            "ca-app-pub-3940256099942544/5224354917", // Test
            adRequest, new RewardedAdLoadCallback() {
                @Override
                public void onAdLoaded(@NonNull RewardedAd rewardedAd) {
                    // Ad successfully loaded.
                    mRewardedAd = rewardedAd ;
                    admobAdRequested = false ;

                    Log.d(ADMOB_LOG_TAG,"onAdLoaded" );
                }

                @Override
                public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) {
                    // Ad failed to load.
                    mRewardedAd = null ;
                    admobAdRequested = false ;

                    Log.d(ADMOB_LOG_TAG,"onAdFailedToLoad" );
                }
            });
    }

    public void admobShowAd()
    {
        if ( !admobInitialized ) {
            return ;
        }

        if ( mRewardedAd != null ) {

            mRewardedAd.setFullScreenContentCallback(new FullScreenContentCallback() {
                @Override
                public void onAdClicked() {
                    // Called when a click is recorded for an ad.
                    Log.d(ADMOB_LOG_TAG,"onAdClicked" );
                }

                @Override
                public void onAdDismissedFullScreenContent() {
                    // Called when ad is dismissed.
                    // Set the ad reference to null so you don't show the ad a second time.
                    Log.d(ADMOB_LOG_TAG,"onAdDismissedFullScreenContent" );
                    mRewardedAd = null;
                }

                @Override
                public void onAdFailedToShowFullScreenContent(AdError adError) {
                    // Called when ad fails to show.
                    Log.d(ADMOB_LOG_TAG,"onAdFailedToShowFullScreenContent" );
                    mRewardedAd = null;
                }

                @Override
                public void onAdImpression() {
                    // Called when an impression is recorded for an ad.
                    Log.d(ADMOB_LOG_TAG,"onAdImpression" );
                }

                @Override
                public void onAdShowedFullScreenContent() {
                    // Called when ad is shown.

                    Log.d(ADMOB_LOG_TAG,"onAdShowedFullScreenContent" );
                }
            });

            mRewardedAd.show(this, new OnUserEarnedRewardListener() {
                @Override
                public void onUserEarnedReward(@NonNull RewardItem rewardItem) {
                    // Handle the reward.
                    Log.d(ADMOB_LOG_TAG,"onUserEarnedReward" );
                }
            });


        }

    }

    static final int COMMAND_REWARDEDTESTTOUCH = COMMAND_USER + 1;
    @Override protected boolean onUnhandledMessage(int command, Object param)
    {
        if ( command == COMMAND_REWARDEDTESTTOUCH ) {
            if (!admobInitCalled) {
                admobInit();
            } else {
                if (mRewardedAd == null && !admobAdRequested) {
                    admobLoadAd();
                } else if (mRewardedAd != null) {
                    admobShowAd();
                }
            }

            return true ;
        }

        return false ;
    }


}

C code:

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

const Uint32 COMMAND_USER				= 0x8000;
const Uint32 COMMAND_REWARDEDTESTTOUCH = COMMAND_USER + 1;

int main(int argc, char *argv[]) {


    if (!SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed (%s)", SDL_GetError());
        return 1;
    }

    // Create a window
    SDL_Window* window = SDL_CreateWindow("testadmob", 320, 200, SDL_WINDOW_MAXIMIZED);

    if (!window) {
        SDL_Log("Unable to create window: %s", SDL_GetError());
        SDL_Quit();
        return 1;
    }

    // Main loop flag
    bool running = true;

    // Main event loop
    while (running) {
        SDL_Event event;

        // Poll and process events
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
                case SDL_EVENT_QUIT: // Handle quit events
                    running = false;
                    break;

                case SDL_EVENT_FINGER_DOWN:
                    SDL_SendAndroidMessage(COMMAND_REWARDEDTESTTOUCH, 0);
                    break ;

                default:
                    // Handle other events
                    break;
            }
        }
    }

    // Cleanup and shutdown
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

I recorded a little test clip, this is from the code above being run. Each tap cycles through the adview cycle. I do one ad that works normally, and then on the second ad I go to the home screen and back - which creates the wonkyness. (This is just a test app, so the screen is all black regardless, but you can see it still says “Test Ad” after coming back from the home screen, and it no longer responds to my touches to load further ads - showing that the app has not gotten back control completely/or the Ad view is still in front).

@dingogames That code looks okay - you do know what you’re doing with Android :slight_smile:

I’m actually able to reproduce the same in my app, so I either didn’t test it enough before, or it’s something new. We’re both in the same boat.

From the top of my head, it might be because we’re not pausing / hiding the view in onPause(), like you have to with a banner ad. I’m not sure how you’d apply that to the billing API though - I can’t remember seeing anything about that when I implemented it. I’m hoping it’s not because SDL is complicating the view somehow. I’ll try some things and get back here with results. Here’s what I believe is right for banner ad views:

    @Override protected void onPause() {
        if(adView != null)
            adView.pause();
        super.onPause();
    }

    @Override protected void onResume() {
        super.onResume();
        queryPurchases();
        if (adView != null && adView.getVisibility() == View.VISIBLE)
            adView.resume();
    }
1 Like

@dingogames I’ve found what’s wrong. onDestroy() is being called when you press home, but only if there’s a full-screen advert or billing overlay. Pressing home normally will just onPause(). I’m sure this didn’t used to happen. I think this might be because SDL recently changed the way it handles app focus on Android; by destroying the surface and re-creating it. This must be what’s upsetting the full screen ads and billing overlay. Strange how it doesn’t affect banners though.

I think it’s best to raise a bug on github, which I’ll do now.

Great, glad you were able replicate it, and find a probable cause! Thanks for posting to GitHub. I’m not around for the next week or so to look into it anymore, but I know the game that I noticed this on was using SDL 2.30.1 or 2.30.2. So the issue goes back at least that far.