Monday, September 19, 2011

Cocos2d-x: Launching a URL on Android...

Since this is my first blog post ever, let me start by introducing myself. My name is Alan, and I am a game programmer, game player, father, son, husband, lover of all things science, and Futurama's/Stargate's biggest fan. I guess thats about it... Now on to the code!


Cocos2d-x is awesome! However, the framework is not designed to do EVERYTHING for you, thus occasionally we have to break out those programming skills, roll our sleeves up, and get dirty. Thankfully, its not very often. I recently found one of these instances when I attempted to open a URL from inside my app while running on an Android device.

Its a bit of a round about process to get from C++ to Java to Android to another App, especially for someone who has been spoiled by how easy it is to do on iOS devices. The very first step is getting your cocos2d-x project up and running on android. I used the following blog post the first time I did it, and it works pretty well.

http://www.supersuraccoon-cocos2d.com/2011/08/10/cocos2d-x-iphone-androidide-installation-and-setup-under-mac-os/

So I assume you have followed the above instructions and made your way back here with a fully functioning "HelloWorld". Let me be the first to congratulate you! What a pain right! Well the good news is that the hard part is over. So lets get back to trying to open a URL on android from inside our cocos2d-x app.

On iOS you would simply call:

[[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithUTF8String:url]]];

However, since we are using the android NDK to interface from our Cocos2d-x project it takes a few more steps. The first step is to open your Eclipse Project you created using the link above. Inside this project you will find "ProjectName/src/org.cocos2dx.lib/Cocos2dxActivity.java". The beginning of that file should look something like this:

/**********Cocos2dxActivity.java**********/
    package org.cocos2dx.lib;

    import android.app.Activity;
    import android.app.AlertDialog;
    import android.app.Dialog;
    import android.content.DialogInterface;
    import android.content.pm.ApplicationInfo;
    import android.content.pm.PackageManager;
    import android.content.pm.PackageManager.NameNotFoundException;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.util.DisplayMetrics;
    import android.util.Log;

    public class Cocos2dxActivity extends Activity{
    public static int screenWidth;
    public static int screenHeight;
    private static Cocos2dxMusic backgroundMusicPlayer;
    private static Cocos2dxSound soundPlayer;
    private static Cocos2dxAccelerometer accelerometer;
    private static boolean accelerometerEnabled = false;
    private static Handler handler;
    private final static int HANDLER_SHOW_DIALOG = 1;
    private static String packageName;
    
    private static native void nativeSetPaths(String apkPath);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // get frame size
        DisplayMetrics dm = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(dm);
        screenWidth = dm.widthPixels;
        screenHeight = dm.heightPixels;
        accelerometer = new Cocos2dxAccelerometer(this);

        // init media player and sound player
        backgroundMusicPlayer = new Cocos2dxMusic(this);
        soundPlayer = new Cocos2dxSound(this);
        
        handler = new Handler(){
        public void handleMessage(Message msg){
        switch(msg.what){
        case HANDLER_SHOW_DIALOG:
        showDialog(((DialogMessage)msg.obj).title, ((DialogMessage)msg.obj).message);
        break;
        }
        }
        };
    }

    //CLASS METHODS REMOVED FROM EXAMPLE TO SINCE THEY DO NOT CHANGE
}

/********************/


Change the class to look like this. The lines denoted in RED are the changes.


/**********Cocos2dxActivity.java**********/
    package org.cocos2dx.lib;

    import android.app.Activity;
    import android.app.AlertDialog;
    import android.app.Dialog;
    import android.content.DialogInterface;
    import android.content.Intent;
    import android.content.pm.ApplicationInfo;
    import android.content.pm.PackageManager;
    import android.content.pm.PackageManager.NameNotFoundException;
    import android.net.Uri;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.util.DisplayMetrics;
    import android.util.Log;

    public class Cocos2dxActivity extends Activity{
    public static int screenWidth;
    public static int screenHeight;
    private static Cocos2dxMusic backgroundMusicPlayer;
    private static Cocos2dxSound soundPlayer;
    private static Cocos2dxAccelerometer accelerometer;
    private static boolean accelerometerEnabled = false;
    private static Handler handler;
    private final static int HANDLER_SHOW_DIALOG = 1;
    private static String packageName;
    private static Activity me = null;
    
    private static native void nativeSetPaths(String apkPath);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        me = this;
        
        // get frame size
        DisplayMetrics dm = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(dm);
        screenWidth = dm.widthPixels;
        screenHeight = dm.heightPixels;
        accelerometer = new Cocos2dxAccelerometer(this);

        // init media player and sound player
        backgroundMusicPlayer = new Cocos2dxMusic(this);
        soundPlayer = new Cocos2dxSound(this);
        
        handler = new Handler(){
         public void handleMessage(Message msg){
         switch(msg.what){
         case HANDLER_SHOW_DIALOG:
         showDialog(((DialogMessage)msg.obj).title, ((DialogMessage)msg.obj).message);
         break;
         }
         }
        };
    }

    public static void openURL(String url) { 
     Intent i = new Intent(Intent.ACTION_VIEW);  
     i.setData(Uri.parse(url));
     me.startActivity(i);
    }

    //CLASS METHODS REMOVED FROM EXAMPLE TO SINCE THEY DO NOT CHANGE
}

/********************/



Ok... So what did we just do? Well, we created a self reference "me" to the activity created by Cocos2d-x when "onCreate" is called durring the program initialization phase. Then we added a method named "openURL" that will launch a child activity from "me" that sends an intent to the Android OS to parse the URL and open it with the default web browser.

So, why on earth did we go through all of that just to call "startActivity"? Well, thats because "startActivity" is not a static method, and dealing with static methods from the NDK is WAY easier than dealing with non static methods. Thus creating a reference to "me" is the next best thing. Kind of a hack, but an effective one.

So thats all the Java we need to change. Lets move on to the C++ side. Go to your iOS/Android hybrid project root and open "ProjectName/libs/cocos2dx/platform/android/Cocos2dJni.h" and add the following line to the list of method declarations:


void openURLJNI(const char* url); 


Now open "ProjectName/libs/cocos2dx/platform/android/Cocos2dJni.cpp" and add the following method.


void openURLJNI(const char* url)
{
        TMethodJNI t;
        if (getMethodID(t
                        , "org/cocos2dx/lib/Cocos2dxActivity"
                        , "openURL"
                        , "(Ljava/lang/String;)V"))
   {
         jstring StringArg1 = t.env->NewStringUTF(url);
         t.env->CallStaticVoidMethod(t.classID, t.methodID, StringArg1);
   }
}

So what does that do? Well, its just a wrapper for calling the Java function we created earlier from within our C++ project. Now anywhere you want to open a URL you simply call openURLJNI("Http://someplace.com"); Please remember to use the full HTTP:// path when opening your URL since Android is REALLY picky about that.

Obviously that command will NOT work on iOS devices. So my wrapper function looks like this.

void PlatformResolver::openURL(const char* url)
{
#ifdef __APPLE__
    PlatformResolver_OBJC::openURL(url);    
#endif
    
#ifdef ANDROID
    openURLJNI(url);
#endif
}

Thats it! You can now launch URL links form within your Cocos2d-x application. YAY! Questions, comments, concerns, drop a note below. Thanks!