This exchange covers most of Google Play Services Achievements and Leaderboard, AdMob Banner and Interstitial, and Licensing check. It doesn’t cover in-app purchases, as I haven’t had any in-app products to sell.
There are several parts to this exchange.
I’ll start with Java code which will cover an optional part to remove the default shared library loader and replacing it with loading just a single game shared library, things that need to be changed in SDLActivity, what base function declarations are in SDLActivity, and onto a full implementation of it all in a secondary java file.
Then I’ll cover JNI functions which will be an intermediary to Java and native code, and onto ServiceCmd singleton class, and wrap it up with a game side sample code.
Edit: this project was built in Android SDK and linked with a library from https://github.com/okamstudio/godot/tree/master/platform/android/libs/play_licensing
[size=150]1) Removing the default shared library loader in SDLActivity (optional)[/size]
This is optional, but I didn’t see a point of having this shared library loader for a final product when I just needed to load MyGame.so file. Nor did I need the screen to change from portrait to landscape when I launched the game.
In SDLActivity.java, starting at line 88. Look for comment: // Urho3D: auto load all the shared libraries available in the library path
[ul]
i) Remove lines 88 to line 113.
ii) grep and remove all instances of mIsSharedLibraryLoaded in the file. Leave SDLActivity.nativeQuit(); there at line 172.
iii) replace protected boolean onLoadLibrary() function with
// load the .so
static {
System.loadLibrary( "MyGame" ); // actual filename has "lib" prefix and ".so" suffix, e.g. "libMyGame.so" in the jniLibs folder under Android SDK project
// or in libs folder for a non Android SDK project
}
iv) You no longer need SampleLauncher.java and Urho3D.java files, delete them both. These will be replaced by MyGame.java
[/ul]
[size=150]2) Changes to SDLActivity[/size]
[ul]
i) replace import android.widget.AbsoluteLayout; with import android.widget.RelativeLayout; // required for AdView
ii) optional - change private static final String TAG = “SDL”; to protected if you want to use the TAG in MyGame.java class.
iii) changes starting from onCreate(): change the layout, declare base class functions, JNI func. and changes to the onXXXX() funcs.
Note: //LUMAKSOFTWARE comments were added to keep track of where I made the changes to the original code.
// Setup
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.v("SDL", "onCreate():" + mSingleton);
super.onCreate(savedInstanceState);
SDLActivity.initialize();
// So we can call stuff from static callbacks
mSingleton = this;
// Set up the surface
mSurface = new SDLSurface(getApplication());
// SDL standard layout
//mLayout = new AbsoluteLayout(this);
mLayout = new RelativeLayout(this);
mLayout.addView( mSurface );
setContentView( mLayout );
// intialize app after mLayout is created
InitializeApp();
// rest of setup
if ( Build.VERSION.SDK_INT >= 12 ) {
mJoystickHandler = new SDLJoystickHandler_API12();
}
else {
mJoystickHandler = new SDLJoystickHandler();
}
}
// LUMAKSOFTWARE: declare base class funcs
protected void InitializeApp() {
}
protected void PauseApp() {
}
protected void ResumeApp() {
}
protected void DestroyApp() {
}
protected boolean onProcessUserCommand(int command, Object param) {
return false;
}
// LUMAKSOFTWARE: user native callback function
public static native void nativeUserActivityCallback(int val, int istat, String file);
// Events
@Override
protected void onPause() {
Log.v("SDL", "onPause()");
// LUMAKSOFTWARE: pause app
PauseApp();
super.onPause();
SDLActivity.handlePause();
}
@Override
protected void onResume() {
Log.v("SDL", "onResume()");
super.onResume();
SDLActivity.handleResume();
// LUMAKSOFTWARE: resume app
ResumeApp();
}
@Override
protected void onDestroy() {
Log.v("SDL", "onDestroy()");
// LUMAKSOFTWARE: destroy app
DestroyApp();
// Send a quit message to the application
SDLActivity.mExitCalledFromJava = true;
SDLActivity.nativeQuit();
// Now wait for the SDL thread to quit
if (SDLActivity.mSDLThread != null) {
try {
SDLActivity.mSDLThread.join();
} catch(Exception e) {
Log.v("SDL", "Problem stopping thread: " + e);
}
SDLActivity.mSDLThread = null;
//Log.v("SDL", "Finished waiting for SDL thread");
}
super.onDestroy();
// Reset everything in case the user re opens the app
SDLActivity.initialize();
}
iv) also replace another place that had AbsoluteLayout with RelativeLayout
static class ShowTextInputTask implements Runnable {
/*
* This is used to regulate the pan&scan method to have some offset from
* the bottom edge of the input region and the top edge of an input
* method (soft keyboard)
*/
static final int HEIGHT_PADDING = 15;
public int x, y, w, h;
public ShowTextInputTask(int x, int y, int w, int h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
@Override
public void run() {
// LUMAKSOFTWARE: AdView requires RelativeLayout - Add adView to the bottom of the screen.
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
//AbsoluteLayout.LayoutParams params = new AbsoluteLayout.LayoutParams(
// w, h + HEIGHT_PADDING, x, y);
if (mTextEdit == null) {
mTextEdit = new DummyEdit(getContext());
mLayout.addView(mTextEdit, params);
} else {
mTextEdit.setLayoutParams(params);
}
mTextEdit.setVisibility(View.VISIBLE);
mTextEdit.requestFocus();
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mTextEdit, 0);
}
}
v) changes to the SDLCommandHandler() func.
protected static class SDLCommandHandler extends Handler {
@Override
public void handleMessage(Message msg) {
Context context = getContext();
if (context == null) {
Log.e(TAG, "error handling message, getContext() returned null");
return;
}
switch (msg.arg1) {
case COMMAND_CHANGE_TITLE:
if (context instanceof Activity) {
((Activity) context).setTitle((String)msg.obj);
} else {
Log.e(TAG, "error handling message, getContext() returned no Activity");
}
break;
case COMMAND_TEXTEDIT_HIDE:
if (mTextEdit != null) {
mTextEdit.setVisibility(View.GONE);
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0);
}
break;
default:
// LUMAKSOFTWARE: onProcessUserCommand function
if ((context instanceof SDLActivity) && ((SDLActivity) context).onProcessUserCommand(msg.arg1, msg.obj)) {
// returning true means the message was intended for the game/app,
// otherwise process it as a unhandled message
}
else if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) {
Log.e(TAG, "error handling message, command is " + msg.arg1);
}
}
}
}
[/ul]
[size=150]3) MyGame.java code[/size]
This replaces the java files mentioned in option 1).
This is the main code that drives GooglePlay, AdMob, and License functions, class extends SDLActivity overriding the base class functions that we declared in step 2).
//=============================================================================
// Copyright (c) 2015 LumakSoftware
//=============================================================================
package com.mycompany.mygame;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import org.libsdl.app.SDLActivity;
import android.content.Intent;
import android.widget.RelativeLayout;
import android.content.*;
import android.view.*;
import android.os.*;
import android.util.Log;
//============================================
// imports for licensing, admob and googleAPI
//============================================
import com.google.android.vending.licensing.LicenseChecker;
import com.google.android.vending.licensing.LicenseCheckerCallback;
import com.google.android.vending.licensing.Policy;
import com.google.android.vending.licensing.ServerManagedPolicy;
import com.google.android.vending.licensing.AESObfuscator;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.AdSize;
import com.google.android.gms.ads.AdView;
import com.google.android.gms.ads.AdListener;
import com.google.android.gms.ads.InterstitialAd;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.games.Games;
import com.google.android.gms.games.Player;
import com.google.android.gms.plus.Plus;
// game R string values
import com.mycompany.mygame.R;
//=============================================================================
// game/app class
//=============================================================================
public class MyGame extends SDLActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
// google API
private GoogleApiClient mGoogleApiClient;
private static final int DBG_GCONNECT = 1;
private static final int RC_SIGN_IN = 9001;
private static final int RC_UNUSED = 5001;
// AdMob
private InterstitialAd mInterstitialAd;
private AdView mAdView;
private AdRequest mAdRequest;
private static final boolean DBG_ADMOB_TESTMODE = true;
private static final byte[] SALT = new byte[] {
-46, 63, 30, -77, -153, -59, 74, -64, 51, 88,
-95, -45, 77, -167, -33, -133, -15, 32, -64, 89
};
// license
private LicenseCheckerCallback mLicenseCheckerCallback;
private LicenseChecker mChecker;
private int miLicenseCheckRetries;
//=================================
// LUMAKSOFTWARE: func overrides
//=================================
@Override
protected void InitializeApp(){
// google api
CreateGoogleAPI();
// licensing - uncomment for test or for in-app purchase stuff
//CreateLicenseCheck();
// AdMob - only create the AdRequest on init, interstitial and adview are created on command
CreateAdRequest();
}
@Override
protected void PauseApp() {
// pause AdView
PauseAdView();
}
@Override
protected void ResumeApp() {
// resume AdView
ResumeAdView();
}
@Override
protected void DestroyApp(){
if ( mChecker != null )
{
mChecker.onDestroy();
mChecker = null;
}
// AdView
DestroyAdView();
// disconnect
if ( isSignedIn() ){
mGoogleApiClient.disconnect();
}
}
// LUMAKSOFTWARE: onProcessUserCommand function
@Override
protected boolean onProcessUserCommand(int command, Object param) {
boolean bresult = false;
int iParam = (int)param;
int iHasFocus = hasWindowFocus()?1:0;
//if ( command != COMMAND_WINDOW_HAS_FOCUS )
//{
// Log.i("SDL", "onProcessUserCommand() cmd=" + command + ", param=" + iParam);
//}
switch ( command )
{
case COMMAND_ADMOB_REQUEST_VIDEO:
CreateInterstitialAd();
if ( mInterstitialAd != null )
{
if ( mInterstitialAd.isLoaded() )
{
nativeUserActivityCallback( COMMAND_ADMOB_STATE_VIDEO, ADMOB_STATE_VIDEO_LOADED, " ");
}
else
{
mInterstitialAd.loadAd(mAdRequest);
}
}
bresult = true;
break;
case COMMAND_ADMOB_SHOW_VIDEO:
if ( mInterstitialAd != null && mInterstitialAd.isLoaded()) {
mInterstitialAd.show();
nativeUserActivityCallback(COMMAND_ADMOB_STATE_VIDEO, ADMOB_STATE_VIDEO_PLAYING, " ");
}
bresult = true;
break;
case COMMAND_ADMOB_HIDE_VIDEO:
// video hides itself, and the game is minimized while the video is playing - do nothing
bresult = true;
break;
case COMMAND_ADMOB_DELETE_VIDEO:
if ( mInterstitialAd != null )
{
mInterstitialAd = null;
nativeUserActivityCallback(COMMAND_ADMOB_STATE_VIDEO, ADMOB_STATE_VIDEO_DESTROYED, " ");
}
bresult = true;
break;
case COMMAND_ADMOB_REQUEST_BANNER:
CreateAdView();
if ( mAdView != null )
{
mAdView.loadAd( mAdRequest );
}
bresult = true;
break;
case COMMAND_ADMOB_SHOW_BANNER:
if ( mAdView != null )
{
mAdView.setVisibility( View.VISIBLE );
nativeUserActivityCallback(COMMAND_ADMOB_STATE_BANNER, ADMOB_STATE_BANNER_VISIBLE, " ");
}
bresult = true;
break;
case COMMAND_ADMOB_HIDE_BANNER:
if ( mAdView != null )
{
mAdView.setVisibility( View.GONE );
nativeUserActivityCallback(COMMAND_ADMOB_STATE_BANNER, ADMOB_STATE_BANNER_HIDDEN, " ");
}
bresult = true;
break;
case COMMAND_ADMOB_DELETE_BANNER:
if ( mAdView != null )
{
mAdView.destroy();
mAdView = null;
nativeUserActivityCallback( COMMAND_ADMOB_STATE_BANNER, ADMOB_STATE_BANNER_DESTROYED, " " );
}
bresult = true;
break;
case COMMAND_LICENSE_QUERY:
if ( mChecker != null )
{
mChecker.checkAccess(mLicenseCheckerCallback);
}
bresult = true;
break;
case COMMAND_GOOGLEAPI_CONNECT:
if ( !isSignedIn() ){
mGoogleApiClient.connect();
}
bresult = true;
break;
case COMMAND_GOOGLEAPI_DISCONNECT:
if ( isSignedIn() ){
mGoogleApiClient.disconnect();
}
bresult = true;
break;
case COMMAND_ACHIEVEMENT_QUERY:
ShowAchievementsRequested();
bresult = true;
break;
case COMMAND_ACHIEVEMENT_SUBMIT:
UnlockAchievement(iParam);
bresult = true;
break;
case COMMAND_LEADERBOARD_QUERY:
ShowLeaderboardsRequested();
bresult = true;
break;
case COMMAND_LEADERBOARD_SUBMIT:
bresult = true;
break;
case COMMAND_LEADERBOARD_SETTIME_1:
case COMMAND_LEADERBOARD_SETTIME_2:
case COMMAND_LEADERBOARD_SETTIME_3:
case COMMAND_LEADERBOARD_SETTIME_4:
case COMMAND_LEADERBOARD_SETTIME_5:
case COMMAND_LEADERBOARD_SETTIME_6:
SubmitLeaderboardScore( command - COMMAND_LEADERBOARD_SETTIME_1, iParam );
bresult = true;
break;
case COMMAND_WINDOW_HAS_FOCUS:
nativeUserActivityCallback(COMMAND_WINDOW_HAS_FOCUS, iHasFocus, " ");
bresult = true;
break;
default:
}
return bresult;
}
//================================
// static vars
//================================
// AdMob
static final int COMMAND_ADMOB_REQUEST_VIDEO = 4;
static final int COMMAND_ADMOB_SHOW_VIDEO = 5;
static final int COMMAND_ADMOB_HIDE_VIDEO = 6;
static final int COMMAND_ADMOB_DELETE_VIDEO = 7;
static final int COMMAND_ADMOB_REQUEST_BANNER = 8;
static final int COMMAND_ADMOB_SHOW_BANNER = 9;
static final int COMMAND_ADMOB_HIDE_BANNER = 10;
static final int COMMAND_ADMOB_DELETE_BANNER = 11;
// Licensing
static final int COMMAND_LICENSE_QUERY = 12;
static final int COMMAND_LICENSE_RETRY = 13;
static final int COMMAND_LICENSE_FAILED = 14;
// GoogleApi
static final int COMMAND_GOOGLEAPI_CONNECT = 15;
static final int COMMAND_GOOGLEAPI_ERROR = 16;
static final int COMMAND_GOOGLEAPI_DISCONNECT = 17;
// achievement
static final int COMMAND_ACHIEVEMENT_QUERY = 18;
static final int COMMAND_ACHIEVEMENT_SUBMIT = 19;
// leaderboard
static final int COMMAND_LEADERBOARD_QUERY = 20;
static final int COMMAND_LEADERBOARD_SUBMIT = 21;
static final int COMMAND_LEADERBOARD_SETTIME_1 = 22;
static final int COMMAND_LEADERBOARD_SETTIME_2 = 23;
static final int COMMAND_LEADERBOARD_SETTIME_3 = 24;
static final int COMMAND_LEADERBOARD_SETTIME_4 = 25;
static final int COMMAND_LEADERBOARD_SETTIME_5 = 26;
static final int COMMAND_LEADERBOARD_SETTIME_6 = 27;
// misc
static final int COMMAND_WINDOW_HAS_FOCUS = 28;
// states
static final int COMMAND_ADMOB_STATE_APP = 1000;
static final int COMMAND_ADMOB_STATE_VIDEO = 1001;
static final int ADMOB_STATE_VIDEO_LOADED = 0;
static final int ADMOB_STATE_VIDEO_PLAYING = 1;
static final int ADMOB_STATE_VIDEO_CLOSED = 2;
static final int ADMOB_STATE_VIDEO_DESTROYED = 3;
static final int ADMOB_STATE_VIDEO_ERROR = 4;
static final int COMMAND_ADMOB_STATE_BANNER = 1002;
static final int ADMOB_STATE_BANNER_LOADED = 0;
static final int ADMOB_STATE_BANNER_VISIBLE = 1;
static final int ADMOB_STATE_BANNER_HIDDEN = 2;
static final int ADMOB_STATE_BANNER_DESTROYED = 3;
static final int ADMOB_STATE_BANNER_ERROR = 4;
// size declaration
static final int APP_SIZE_NUM_ACHIEVEMENTS = 9;
static final int APP_SIZE_NUM_LEADERBOARD = 6; // should match COMMAND_LEADERBOARD_SETTIME_1 to last leaderboard settime
//================================
// googleAPI
//================================
protected void CreateGoogleAPI() {
// instantiate
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks( this )
.addOnConnectionFailedListener(this)
.addApi(Plus.API).addScope(Plus.SCOPE_PLUS_LOGIN)
.addApi(Games.API).addScope(Games.SCOPE_GAMES)
.build();
}
//================================
// license
//================================
private class MyLicenseCheckerCallback implements LicenseCheckerCallback {
// void allow()
public void allow(int reason) {
Log.v("SDL", "allow()" + reason);
nativeUserActivityCallback(COMMAND_LICENSE_QUERY, 0, "OK");
if (isFinishing()) {
// Don't update UI if Activity is finishing.
return;
}
}
// void dontAllow
public void dontAllow(int reason) {
Log.v("SDL", "dontAllow()" + reason);
if (isFinishing()) {
// Don't update UI if Activity is finishing.
return;
}
if (reason == Policy.RETRY) {
// If the reason received from the policy is RETRY, it was probably
// due to a loss of connection with the service, so we should give the
// user a chance to retry. So show a dialog to rety.
//showDialog(DIALOG_RETRY); COMMAND_LICENSE_RETRY
nativeUserActivityCallback(COMMAND_LICENSE_RETRY, 0, "OK");
} else {
// Otherwise, the user is not licensed to use this app.
// Your response should always inform the user that the application
// is not licensed, but your behavior at that point can vary. You might
// provide the user a limited access version of your app or you can
// take them to Google Play to purchase the app.
//showDialog(DIALOG_GOTOMARKET);
nativeUserActivityCallback(COMMAND_LICENSE_FAILED, reason, "OK");
}
}
// void applicationError()
public void applicationError(int errorCode){
Log.v("SDL", "MyLicenseCheckerCallback() app error" + errorCode);
// error 3 - not published in google play, just mark it as 'ok' for testing
if ( errorCode == 3 ) {
nativeUserActivityCallback(COMMAND_LICENSE_QUERY, 0, "OK");
}
}
}
// licensing
protected void CreateLicenseCheck() {
// Construct the LicenseCheckerCallback. The library calls this when done.
mLicenseCheckerCallback = new MyLicenseCheckerCallback();
// Construct the LicenseChecker with a Policy.
mChecker = new LicenseChecker(
this, new ServerManagedPolicy(this,
new AESObfuscator(SALT, getPackageName(), getResources().getString(R.string.app_id) )),
getResources().getString(R.string.BASE64_PUBLIC_KEY) // Your public licensing key.
);
}
//================================
// AdRequest
//================================
protected void CreateAdRequest() {
if ( mAdRequest != null )
{
return;
}
if ( !DBG_ADMOB_TESTMODE )
{
// live mode
mAdRequest = new AdRequest.Builder().build();
} else {
// test mode
mAdRequest = new AdRequest.Builder()
// the string below is not populated at the beginning and you should comment it out the
// first time you run it then grep for addTestDevice in logcat to get it
.addTestDevice( "AF9C9F5B6C6595D55D1E9A9B07011546" ) // add as many test devices as you want
.addTestDevice( AdRequest.DEVICE_ID_EMULATOR )
.build();
}
}
//================================
// AdView
//================================
protected void CreateAdView() {
if ( mAdView != null )
{
return;
}
mAdView = new AdView(this);
mAdView.setAdUnitId( getResources().getString(R.string.banner_ad_unit_id) );
mAdView.setAdSize(AdSize.SMART_BANNER);
// set the layer type as LAYER_TYPE_SOFTWARE to avoid getting the black screen of death (will avoid most of them, not all)
mAdView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
// Add adView to the bottom of the screen.
RelativeLayout.LayoutParams adParams = new RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
//adParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
adParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
// add it to the layout
mLayout.addView(mAdView, adParams);
mAdView.setAdListener( new AdListener() {
@Override
public void onAdLoaded() {
// Code to be executed when an ad finishes loading.
nativeUserActivityCallback( COMMAND_ADMOB_STATE_BANNER, ADMOB_STATE_BANNER_LOADED, " " );
//Log.i("SDL", "ADMOB_STATE_BANNER_LOADED" );
}
@Override
public void onAdFailedToLoad(int errorCode) {
// Code to be executed when an ad request fails.
String strError = Integer.toString( errorCode );
nativeUserActivityCallback( COMMAND_ADMOB_STATE_BANNER, ADMOB_STATE_BANNER_ERROR, strError );
}
//@Override
public void onAdClosed() {
// Code to be executed when when the user is about to return
// to the application after tapping on an ad.
nativeUserActivityCallback(COMMAND_ADMOB_STATE_BANNER, ADMOB_STATE_BANNER_HIDDEN, " ");
//Log.i("SDL", "-- mAdView -- onAdClosed() " );
}
});
}
protected void PauseAdView() {
if ( mAdView != null ) {
mAdView.pause();
}
}
protected void ResumeAdView() {
if ( mAdView != null ) {
mAdView.resume();
}
}
protected void DestroyAdView() {
if ( mAdView != null ) {
mAdView.destroy();
mAdView = null;
}
}
//================================
// interstitial
//================================
protected void CreateInterstitialAd() {
if ( mInterstitialAd != null )
{
return;
}
mInterstitialAd = new InterstitialAd(this);
mInterstitialAd.setAdUnitId(getResources().getString(R.string.interstitial_ad_unit_id));
mInterstitialAd.setAdListener(new AdListener() {
@Override
public void onAdLoaded() {
// Code to be executed when an ad finishes loading.
nativeUserActivityCallback(COMMAND_ADMOB_STATE_VIDEO, ADMOB_STATE_VIDEO_LOADED, " ");
Log.i("SDL", "ADMOB_STATE_VIDEO_LOADED" );
}
@Override
public void onAdFailedToLoad(int errorCode) {
// Code to be executed when an ad request fails.
Log.v(TAG, "Interstitial - onAdFailedToLoad() error=" + errorCode);
String strError = Integer.toString(errorCode);
nativeUserActivityCallback(COMMAND_ADMOB_STATE_VIDEO, ADMOB_STATE_VIDEO_ERROR, strError);
}
@Override
public void onAdClosed() {
// Code to be executed when when the user is about to return
// to the application after tapping on an ad.
nativeUserActivityCallback(COMMAND_ADMOB_STATE_VIDEO, ADMOB_STATE_VIDEO_CLOSED, " ");
Log.i("SDL", "ADMOB_STATE_VIDEO_CLOSED" );
}
});
}
//================================
// google api - connect
//================================
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
if (requestCode == RC_SIGN_IN) {
if (resultCode == RESULT_OK) {
mGoogleApiClient.connect();
} else {
nativeUserActivityCallback( COMMAND_GOOGLEAPI_ERROR, resultCode, "1" );
}
}
}
@Override
public void onConnected(Bundle bundle) {
//Log.d(TAG, "onConnected(): ");
// uncomment below if you want to get the playa's name
//Player p = Games.Players.getCurrentPlayer(mGoogleApiClient);
String displayName = "playa";
//if (p == null) {
// Log.w(TAG, "mGamesClient.getCurrentPlayer() is NULL!");
// displayName = "???";
//} else {
// displayName = p.getDisplayName();
//}
nativeUserActivityCallback(COMMAND_GOOGLEAPI_CONNECT, 1, displayName);
}
@Override
public void onConnectionSuspended(int var1) {
mGoogleApiClient.connect();
Log.d(TAG, "onConnectionSuspended():");
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Log.d(TAG, "onConnectionFailed(): attempting to resolve");
if ( connectionResult.hasResolution() ) {
try {
connectionResult.startResolutionForResult(this, RC_SIGN_IN);
} catch (IntentSender.SendIntentException e) {
// The intent was canceled before it was sent. Return to the default
// state and attempt to connect to get an updated ConnectionResult.
mGoogleApiClient.connect();
}
} else {
// not resolvable... so show an error message
int errorCode = connectionResult.getErrorCode();
nativeUserActivityCallback( COMMAND_GOOGLEAPI_ERROR, errorCode, " " );
}
}
private boolean isSignedIn() {
return (mGoogleApiClient != null && mGoogleApiClient.isConnected());
}
//================================
// achievement and leaderboard
//================================
public void ShowAchievementsRequested() {
if ( isSignedIn() )
{
startActivityForResult(Games.Achievements.getAchievementsIntent(mGoogleApiClient), RC_UNUSED );
}
}
public void UnlockAchievement(int _idx) {
if ( isSignedIn() && _idx >= 0 && _idx < APP_SIZE_NUM_ACHIEVEMENTS )
{
String[] astrAchievementNames = getResources().getStringArray(R.array.achievement_array);
//Log.v("SDL", "UnlockAchievement() idx=" + _idx + ", str='" + astrAchievementNames[_idx] + "'" );
Games.Achievements.unlock(mGoogleApiClient, astrAchievementNames[_idx]);
}
}
public void ShowLeaderboardsRequested() {
if ( isSignedIn() )
{
startActivityForResult(Games.Leaderboards.getAllLeaderboardsIntent(mGoogleApiClient), RC_UNUSED);
}
}
public void SubmitLeaderboardScore(int _idx, int _iTime) {
if ( isSignedIn() && _idx >= 0 && _idx < APP_SIZE_NUM_LEADERBOARD )
{
long lTime = _iTime;
String[] astrLBNames = getResources().getStringArray(R.array.leaderboard_array);
//Log.v("SDL", "SubmitLeaderboardScore() idx=" + _idx + ", str='" + astrLBNames[_idx] + "'" );
Games.Leaderboards.submitScore(mGoogleApiClient, astrLBNames[_idx], lTime);
}
}
}
IDs, achievement, and leaderboard strings
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_id">10XXXXXX130</string>
<string name="banner_ad_unit_id">ca-app-pub-4xxxxxxxx/1xxxxxxxx</string>
<string name="interstitial_ad_unit_id">ca-app-pub-4xxxxxx/31xxxxxxx</string>
<string name="BASE64_PUBLIC_KEY">MIIBIjAxxxxxxxxxxxxxxxxxxxxxBCgKCAQEAkF4wRG9st6jp0qqswPsk0UgzWg/hkj3yzGGxr2o+ogLYbCwrqZsk7x7mDAvC277Je+xmBiKFCXp/qnR3xyW02xp3aebIr8dOUeRovJdbNYhOaRar+5gGOkFABxzcpykftaEhalHV5XSwRJmSQIox220/P/1bRoOgCKnzo9Qm81SzvjaMmnz27joAdPkdP7MLinR7N4tuCgh4UqlrevoAx1XIYLdCziMPR2YQ5lCNOuLM5VtPMc4djAPw3RNfgpPJgD0xbRqlvpdk0DXzlVPuSo/tuqsCRtFK2GSb0cM1oU/pJzCASp66SC+VztgvLhxjcFvSXb7iQ/STRL3kP0r0gwIDAQAB</string>
</resources>
You get the following tags from Google Play Developer Console when you create achievements and leaderboard for you game/app.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MyGame</string>
<string-array name="achievement_array">
<item>CgkIyusYYYuIQAQ</item> <!-- novice -->
<item>CgkIyuYYYYeIQAg</item> <!-- skilled -->
<item>CgkIyuYYYYYIQAw</item> <!-- advanced -->
<item>CgkIyuYYseEAIQBA</item> <!-- expert -->
<item>CgkIsYYYYYEAIQBQ</item> <!-- master -->
<item>CgkIyufYYYYYeuBg</item> <!-- apprentice -->
<item>CgkIyufYYYYYeuw</item> <!-- journeyman -->
<item>CgkIyufYYYYYeuCA</item> <!-- master craftsman -->
<item>CgkIyufYYYYeYuCQ</item> <!-- gold -->
</string-array>
<string-array name="leaderboard_array">
<item>CgkIyYYyyyYufeuCg</item> <!-- stage 1 -->
<item>CgkIyYYYYufeuQCw</item> <!-- stage 2 -->
<item>CgkIyufYYYYYuQDA</item> <!-- stage 3 -->
<item>CgkIyuYYYYYeuQDQ</item> <!-- stage 4 -->
<item>CgkIyufYYYYYeuQDg</item> <!-- stage 5 -->
<item>CgkIyufYYYYYeuQDw</item> <!-- artists -->
</string-array>
</resources>
[size=150]4) JNI functions[/size]
Java function sendMessage(int, int) already exists and is called from Android_JNI_SendMessage(int, int).
This part covers the Java function public static native void nativeUserActivityCallback(int val, int istat, String file); that we declared in SDLActiviy in section 2). Along with a callback function that we need to hook into from game side.
typedef void (*pfnUserActivityCallback)(int id1, int istat, const char *str, void *param);
static pfnUserActivityCallback gpUserActivityCallback = NULL;
static void *gActivityCallbackParam = NULL;
void RegisterUserActivityCallback(pfnUserActivityCallback callback, void *param)
{
gpUserActivityCallback = callback;
gActivityCallbackParam = param;
}
void Java_org_libsdl_app_SDLActivity_nativeUserActivityCallback(JNIEnv* env, jclass cls, jint id1, jint istat, jstring jstrParam)
{
const char *str = (*env)->GetStringUTFChars(env, jstrParam, 0);
if ( gpUserActivityCallback )
{
(*gpUserActivityCallback)( id1, istat, str, gActivityCallbackParam );
}
if ( str )
{
(*env)->ReleaseStringUTFChars(env, jstrParam, str);
}
}
[size=150]5) ServiceCmd singleton class[/size]
This class handles messages to/from the Java class. It is designed as a singleton to allow access from main game class, classes to access achievement and leaderboard calls, and other classes.
Note:This class is not thread safe, however, AdMob messages are state driven and the game side is also state driven to reduce the chance of over writing status during AdMob activities
ServiceCmd.cpp
//=============================================================================
// Copyright (c) 2015 LumakSoftware
//=============================================================================
#include <Urho3D/Urho3D.h>
#include <Urho3D/Core/CoreEvents.h>
#include <Urho3D/Core/Context.h>
#include <Urho3D/Engine/Engine.h>
#include <Urho3D/Input/InputEvents.h>
#include <Urho3D/Scene/Scene.h>
#include <Urho3D/Core/Timer.h>
#include "ServiceCmd.h"
#include <Urho3D/DebugNew.h>
#include <SDL/SDL_Log.h>
#include <SDL/SDL_assert.h>
//=============================================================================
//=============================================================================
#ifdef WIN32
typedef void (*pfnUserActivityCallback)(int id1, int istat, const char *str, void *param);
static pfnUserActivityCallback gpFnUserActivityCallback = NULL;
static void *gpActivityCallbackParam = NULL;
void RegisterUserActivityCallback(pfnUserActivityCallback callback, void *param)
{
}
int Android_JNI_SendMessage(int command, int param)
{
return 0;
}
#endif
//#define DBG_DUMP_SVCLOG
//=============================================================================
//=============================================================================
ServiceCmd* ServiceCmd::s_pCAdMob = NULL;
//=============================================================================
//=============================================================================
ServiceCmd::ServiceCmd(Context *_pcontext)
: Object( _pcontext )
{
// init
m_TimerWindowFocus.Reset();
m_TimerVideo.Reset();
m_TimerBanner.Reset();
m_TimerLicense.Reset();
// focus
m_bWindowHasFocus = true;
// connect
m_bGooglePlayConnected = false;
m_strGooglePlayUserName.Clear();
m_iConnectErrorCode = INVALID_CONNECT_CODE;
// admob
m_uAdInterval = 2 * 60 * 1000; // 2 mins
m_iCmdTimeout = 5;
m_bAdPlayedSinceStartup = false;
m_iVideoState = kAdMobVideo_Ready;
m_iBannerState = kAdMobBanner_NotLoaded;
m_HasVideoPlayedOnce = false;
m_bAppIsPaused = false;
// license
m_bContinueLicenseQuery = true;
m_bLicenseQueryMade = false;
m_bHasValidLicense = false;
m_bHasLicenseQueryResp = false;
m_iLicenseResp = -1;
// register java callback
RegisterUserActivityCallback( &ServiceCmd::JavaActivityCallback, this );
}
//=============================================================================
//=============================================================================
ServiceCmd::~ServiceCmd()
{
RegisterUserActivityCallback( NULL, NULL );
}
//=============================================================================
//=============================================================================
void ServiceCmd::JavaActivityCallback(int _ival, int _istat, const char *_pstr, void *param)
{
if ( param )
{
((ServiceCmd*)param)->ActivityCallback( _ival, _istat, _pstr );
}
}
//=============================================================================
//=============================================================================
void ServiceCmd::ActivityCallback(int _icmd, int _istat, const char *_pstr)
{
#ifdef DBG_DUMP_SVCLOG
if ( _icmd != COMMAND_WINDOW_HAS_FOCUS )
{
SDL_Log("ServiceCmd::ActivityCallback() icmd=%d, istat=%d, pstr='%s'\n", _icmd, _istat, _pstr?_pstr:" ");
}
#endif
switch ( _icmd )
{
case COMMAND_ADMOB_STATE_VIDEO:
switch ( _istat )
{
case kAdMobVideo_Stat_Loaded: m_iVideoState = kAdMobVideo_Loaded; break;
case kAdMobVideo_Stat_Playing: m_iVideoState = kAdMobVideo_Playing; break;
case kAdMobVideo_Stat_Closed: m_iVideoState = kAdMobVideo_Ready; ResetVideoTimer(); break;
case kAdMobVideo_Stat_Destroyed: m_iVideoState = kAdMobVideo_Ready; break;
default:
// failed to load error - retry
ResetVideoTimer();
break;
}
break;
case COMMAND_ADMOB_STATE_BANNER:
switch ( _istat )
{
case kAdMobBanner_Stat_Loaded:
if ( m_iBannerState == kAdMobBanner_NotLoaded || m_iBannerState == kAdMobBanner_Requested )
{
m_iBannerState = kAdMobBanner_Ready;
}
// AdMob banner continuously loads new banners in the background, and any subsequent calls will automatically show itself
// -- prevent it from being visible if not in the visible state
else if ( m_iBannerState != kAdMobBanner_Visible )
{
RequestServiceCmd( COMMAND_ADMOB_HIDE_BANNER );
}
break;
case kAdMobBanner_Stat_Visible: m_iBannerState = kAdMobBanner_Visible; break;
case kAdMobBanner_Stat_Hidden: m_iBannerState = kAdMobBanner_Hidden; break;
case kAdMobBanner_Stat_Destroyed: m_iBannerState = kAdMobBanner_NotLoaded; break;
default:
// failed to load error - retry
ResetBannerTimer();
break;
}
break;
case COMMAND_GOOGLEAPI_CONNECT:
if ( _istat == 1 )
{
m_bGooglePlayConnected = true;
if ( _pstr )
{
m_strGooglePlayUserName = String( _pstr );
}
#ifdef DBG_DUMP_SVCLOG
SDL_Log("COMMAND_GOOGLEAPI_CONNECT username='%s'\n", m_strGooglePlayUserName.CString() );
#endif
}
else
{
m_bGooglePlayConnected = false;
}
break;
case COMMAND_GOOGLEAPI_ERROR:
m_iConnectErrorCode = _istat;
break;
case COMMAND_LICENSE_QUERY:
case COMMAND_LICENSE_RETRY:
case COMMAND_LICENSE_FAILED:
m_bHasLicenseQueryResp = true;
if ( _icmd == COMMAND_LICENSE_QUERY )
{
m_bHasValidLicense = true;
m_iLicenseResp = 0;
}
else if ( _icmd == COMMAND_LICENSE_RETRY )
{
m_iLicenseResp = COMMAND_LICENSE_RETRY;
}
else
{
m_iLicenseResp = COMMAND_LICENSE_FAILED;
}
break;
case COMMAND_WINDOW_HAS_FOCUS:
m_bWindowHasFocus = _istat?true:false;
break;
}
}
//=============================================================================
//=============================================================================
void ServiceCmd::RequestServiceCmd(int _iCmdType, int _iSubCmd)
{
if ( _iCmdType >= COMMAND_ADMOB_REQUEST_VIDEO && _iCmdType < COMMAND_END )
{
#ifdef DBG_DUMP_SVCLOG
if ( _iCmdType != COMMAND_WINDOW_HAS_FOCUS )
{
SDL_Log( "ServiceCmd::RequestServiceCmd() _iCmdType=%d, sub=%d\n", _iCmdType, _iSubCmd );
}
#endif
Android_JNI_SendMessage( _iCmdType, _iSubCmd );
switch ( _iCmdType )
{
case COMMAND_WINDOW_HAS_FOCUS:
ResetWindowFocusTimer();
break;
case COMMAND_GOOGLEAPI_CONNECT:
m_iConnectErrorCode = INVALID_CONNECT_CODE;
break;
//========================
// admob video
case COMMAND_ADMOB_REQUEST_VIDEO:
m_TimerVideo.Reset();
m_iVideoState = kAdMobVideo_Requested;
break;
case COMMAND_ADMOB_SHOW_VIDEO:
m_HasVideoPlayedOnce = true;
m_TimerVideo.Reset();
m_TimerBanner.Reset();
break;
case COMMAND_ADMOB_HIDE_VIDEO:
// can't really send a request to hide video
break;
case COMMAND_ADMOB_DELETE_VIDEO:
m_iVideoState = kAdMobVideo_Ready;
break;
//========================
// admob banner
case COMMAND_ADMOB_REQUEST_BANNER:
m_iBannerState = kAdMobBanner_Requested;
m_TimerBanner.Reset();
break;
case COMMAND_ADMOB_SHOW_BANNER:
m_TimerBanner.Reset();
break;
case COMMAND_ADMOB_HIDE_BANNER:
break;
case COMMAND_ADMOB_DELETE_BANNER:
break;
case COMMAND_LICENSE_QUERY:
m_bLicenseQueryMade = true;
m_bHasLicenseQueryResp = false;
m_iLicenseResp = -1;
m_TimerLicense.Reset();
break;
}
}
else
{
#ifdef DBG_DUMP_SVCLOG
SDL_Log( "ServiceCmd::StartAd() unknown add type=%d\n", _iCmdType );
#endif
}
}
//=============================================================================
//=============================================================================
void ServiceCmd::SendUnlockAchievement(unsigned _idx)
{
if ( _idx < APP_SIZE_NUM_ACHIEVEMENTS )
{
RequestServiceCmd( COMMAND_ACHIEVEMENT_SUBMIT, _idx );
}
}
//=============================================================================
//=============================================================================
void ServiceCmd::SendLeaderboardUpdate(unsigned _idx, unsigned _uNumUnits)
{
if ( _idx < APP_SIZE_NUM_LEADERBOARD )
{
RequestServiceCmd( COMMAND_LEADERBOARD_SETTIME_BEG + _idx, (int)_uNumUnits );
}
}
ServiceCmd.h
//=============================================================================
// Copyright (c) 2015 LumakSoftware
//=============================================================================
#pragma once
using namespace Urho3D;
//=============================================================================
//=============================================================================
#define INVALID_CONNECT_CODE 51123333 // arbitrary value
enum APPJAVASERVICECOMMANDS
{
// admob
COMMAND_ADMOB_REQUEST_VIDEO = 4,
COMMAND_ADMOB_SHOW_VIDEO = 5,
COMMAND_ADMOB_HIDE_VIDEO = 6,
COMMAND_ADMOB_DELETE_VIDEO = 7,
COMMAND_ADMOB_REQUEST_BANNER = 8,
COMMAND_ADMOB_SHOW_BANNER = 9,
COMMAND_ADMOB_HIDE_BANNER = 10,
COMMAND_ADMOB_DELETE_BANNER = 11,
// Licensing
COMMAND_LICENSE_QUERY = 12,
COMMAND_LICENSE_RETRY = 13,
COMMAND_LICENSE_FAILED = 14,
// GoogleApi
COMMAND_GOOGLEAPI_CONNECT = 15,
COMMAND_GOOGLEAPI_ERROR = 16,
COMMAND_GOOGLEAPI_DISCONNECT = 17,
// achievement
COMMAND_ACHIEVEMENT_QUERY = 18,
COMMAND_ACHIEVEMENT_SUBMIT = 19,
// leaderboard
COMMAND_LEADERBOARD_QUERY = 20,
COMMAND_LEADERBOARD_SUBMIT = 21,
COMMAND_LEADERBOARD_SETTIME_BEG = COMMAND_LEADERBOARD_SUBMIT + 1,
COMMAND_LEADERBOARD_SETTIME_END = COMMAND_LEADERBOARD_SETTIME_BEG + 5,
// misc
COMMAND_WINDOW_HAS_FOCUS = 28,
// end of valid commands
COMMAND_END,
// response only, not a valid command to send
COMMAND_ADMOB_STATE_APP = 1000,
COMMAND_ADMOB_STATE_VIDEO = 1001,
COMMAND_ADMOB_STATE_BANNER = 1002,
};
enum APPSIZES
{
APP_SIZE_NUM_ACHIEVEMENTS = 9,
APP_SIZE_NUM_LEADERBOARD = 6,
};
enum VIDEOSTATE
{
kAdMobVideo_Stat_Loaded = 0,
kAdMobVideo_Stat_Playing = 1,
kAdMobVideo_Stat_Closed = 2,
kAdMobVideo_Stat_Destroyed = 3,
kAdMobVideo_Stat_Error = 4,
kAdMobVideo_Requested,
kAdMobVideo_Loaded,
kAdMobVideo_Ready,
kAdMobVideo_Playing,
kAdMobVideo_Done,
};
enum BANNERSTATE
{
kAdMobBanner_Stat_Loaded = 0,
kAdMobBanner_Stat_Visible = 1,
kAdMobBanner_Stat_Hidden = 2,
kAdMobBanner_Stat_Destroyed = 3,
kAdMobBanner_Stat_Error = 4,
kAdMobBanner_NotLoaded,
kAdMobBanner_Requested,
kAdMobBanner_Ready,
kAdMobBanner_Visible,
kAdMobBanner_Hidden,
kAdMobBanner_Done,
};
//=============================================================================
//=============================================================================
class ServiceCmd : public Object
{
OBJECT(ServiceCmd);
public:
enum RESPTYPE{ kRespType_Invalid = -9999 };
static void Create(Context *_pContext)
{
if ( s_pCAdMob == NULL )
{
s_pCAdMob = new ServiceCmd( _pContext );
}
}
static void Destroy()
{
if ( s_pCAdMob )
{
delete s_pCAdMob;
s_pCAdMob = NULL;
}
}
static ServiceCmd& Instance()
{
return *s_pCAdMob;
}
static void JavaActivityCallback(int _ival, int _istat, const char *_pstr, void *param);
void RequestServiceCmd(int _iAdType, int _iSubCmd=0);
// window focus
bool WindowHasFocus() const { return m_bWindowHasFocus; }
void ResetWindowFocusTimer() { m_TimerWindowFocus.Reset(); }
unsigned GetWindowFocusTimer() { return m_TimerWindowFocus.GetMSec( false ); }
// connection
bool IsConnectedToGameServices() const { return m_bGooglePlayConnected; }
const String& GetGooglePlayUserName() const { return m_strGooglePlayUserName; }
const int GetConnectErrorCode() const { return m_iConnectErrorCode; }
// achievement and leaderboard
void SendUnlockAchievement(unsigned _idx);
void SendLeaderboardUpdate(unsigned _idx, unsigned _uNumUnits);
// admob
void SetAdInterval(Uint32 _uInterval) { m_uAdInterval = _uInterval; }
void SetCmdTimeout(int _iMins) { m_iCmdTimeout = _iMins; }
void ResetVideoTimer() { m_TimerVideo.Reset(); }
void ResetBannerTimer() { m_TimerBanner.Reset(); }
unsigned GetVideoElapsedTime() { return m_TimerVideo.GetMSec( false ); }
unsigned GetBannerElapsedTime() { return m_TimerBanner.GetMSec( false ); }
int GetVideoState() const { return m_iVideoState; }
int GetBannerState() const { return m_iBannerState; }
bool HasVideoPlayedOnce() const { return m_HasVideoPlayedOnce; }
bool IsVideoHidden() const { return m_bVideoHidden; }
bool IsBannerVisible() const { return m_bBannerVisible; }
bool IsAppPaused() const { return m_bAppIsPaused; }
// license
bool LicenseQueryMade() const { return m_bLicenseQueryMade; }
void SetContinueLicenseQuery(bool _bset){ m_bContinueLicenseQuery = _bset; }
bool GetContinueLicenseQuery() const { return m_bContinueLicenseQuery; }
bool IsLicenseValid() const { return m_bHasValidLicense; }
bool IsLicenseRespReady() const { return m_bHasLicenseQueryResp; }
int GetLicenseResp() const { return m_iLicenseResp; }
int GetLicenseReason() const { return m_iLicenseReason; }
void ResetLicenseTimer() { m_TimerLicense.Reset(); }
unsigned GetLicenseQueryElapsedTime() { return m_TimerLicense.GetMSec( false ); }
protected:
ServiceCmd(Context *_pContext);
~ServiceCmd();
void ActivityCallback(int _ival, int _istat, const char *_pstr);
//void HandleUpdate(StringHash eventType, VariantMap& eventData);
protected:
static ServiceCmd *s_pCAdMob;
Timer m_TimerWindowFocus;
Timer m_TimerVideo;
Timer m_TimerBanner;
Timer m_TimerLicense;
// focus
bool m_bWindowHasFocus;
// googleplay connection
bool m_bGooglePlayConnected;
String m_strGooglePlayUserName;
int m_iConnectErrorCode;
// AdMob
bool m_bAdPlayedSinceStartup;
int m_iVideoState;
int m_iBannerState;
bool m_HasVideoPlayedOnce;
bool m_bVideoHidden;
bool m_bBannerVisible;
// licensing
bool m_bContinueLicenseQuery;
bool m_bLicenseQueryMade;
bool m_bHasValidLicense;
bool m_bHasLicenseQueryResp;
int m_iLicenseResp;
int m_iLicenseReason;
// misc
bool m_bAppIsPaused;
Uint32 m_uAdInterval;
int m_iCmdTimeout;
};
//=============================================================================
//=============================================================================
extern "C"
{
//#ifndef pfnUserActivityCallback
typedef void (*pfnUserActivityCallback)(int _id, int _istat, const char *_str, void *param);
//#endif
void RegisterUserActivityCallback(pfnUserActivityCallback callback, void *param);
int Android_JNI_SendMessage(int command, int param);
}
[size=150]6) Game side sample code[/size]
Sample code
// somewhere in your startup
ServiceCmd::Create( GetContext() );
// some where in your shutdown process
ServiceCmd::Destroy();
// in your handleUpdate() func
void GameMain::ProcessAdMob()
{
// only play AdMob when game is not playing
if ( m_pGamePlayFeature == NULL )
{
// video state
switch ( ServiceCmd::Instance().GetVideoState() )
{
case kAdMobVideo_Ready:
if ( ServiceCmd::Instance().GetVideoElapsedTime() > 12 * 60 * 1000 ) // 12 min wait
{
ServiceCmd::Instance().RequestServiceCmd( COMMAND_ADMOB_REQUEST_VIDEO );
}
break;
case kAdMobVideo_Requested:
if ( ServiceCmd::Instance().GetVideoElapsedTime() > 20 * 1000 ) // 20 sec wait
{
ServiceCmd::Instance().RequestServiceCmd( COMMAND_ADMOB_REQUEST_VIDEO );
}
break;
case kAdMobVideo_Loaded:
ServiceCmd::Instance().RequestServiceCmd( COMMAND_ADMOB_SHOW_VIDEO );
break;
case kAdMobVideo_Playing:
// wait for it to go back to 'Ready'
break;
}
}
}
And I’ve run out of room for anything else.