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!

14 comments:

  1. Nice tutorial.

    Thanks.

    ReplyDelete
  2. Hey that's one very useful tip!

    Hope to see more on Android / iOS services abstraction with cocos2dx on this blog!

    Thanks!

    ReplyDelete
  3. Hey I just went through your tutorial. Nice 1 if it worked for you.
    I need to ask a few things.
    First there was no Cocos2dJni file in libs of Cocos2dx latest Version. So where to get this file from.
    Secondly, I got this file from a sample project and I placed it in libs of cocos2dx.
    Now how to call this method as its showing undeclared. Do I need to include Cocos2dJni or how?
    M sorry as I have just started working on it. Its about a week only.
    Hope you help. :)
    Thanks in advance

    ReplyDelete
  4. Exactly the same question as nilhil ! Nihil: did you solve this issue ? Or Alan, can you answer ? Thanks !!

    ReplyDelete
  5. Sorry, I have not worked with the newer version of Cocos2D, but I downloaded the source this morning and it looks like they have separated the JNI functions into individual files now. Take a look at "cocos2dx/platform/android/jni" in there you will find a shared "JniHelper class" and a series of JNI based files like MessageJni, and SystemInfoJni. You should be able to create a new .h and .cpp using one of these as a template that will encapsulate the functions I created above.

    I assume the Java section is still pretty much the same, but I cannot verify at this time as I do not have eclipse installed on this machine. If you still cant figure it out, please let me know and I will try to port this later this week if I can find the time. Sorry I can't be more help at the moment. Good luck!

    ReplyDelete
  6. In case I use a JniHelper ot MessageJni.. I still need to call its methods... How should I do that???
    Thats a major concern.... Please help me in that....

    ReplyDelete
  7. Well, JniHelper looks to be a helper class used by the other JNI files, and contains most of what would be a base class. Try looking at "showMessageBoxJNI" in MessageJni and use that as your template in which to write your own functions. Then just include your new header, and call the function directly when needed. Or am I missing a larger question?

    ReplyDelete
  8. Hey m still not able to find the solution to it... Its not showing MessageJni if I include it in my scene class. :(...
    Hope you could help me out.. :)

    ReplyDelete
  9. Thank you very much! Finally I got this thing working.
    You're the best!

    ReplyDelete
  10. Perhaps you should add:
         t.env->DeleteLocalRef(StringArg1);
    right after the call to the static method (line 8 of the `openURLJNI` function)

    ReplyDelete
  11. Hi,

    there is no "cocos2djni.h" & "cocos2djni.cpp" in cocos2d-2.1beta3-x-2.1.0 2 version

    what should i do ?

    ReplyDelete
    Replies
    1. You can add them into the Java_org_cocos2dx_lib_Cocos2dxHelper.(h/cpp) in "\cocos2dx\platform\android\jni"

      Delete
  12. This comment has been removed by the author.

    ReplyDelete
  13. Thanks for the FANTASTIC post! This information is really good and thanks a ton for sharing it :-) I m looking forward desperately for the next post of yours..
    cocos2d game development

    ReplyDelete