2013-07-24

Create Android Live Wallpaper with Cocos2d-x

底下敘述使用 Cocos2d-x 製作 Android Live Wallpaper 的方法。(建構環境 : OSX ML,cocos2d-2.1rc0-x-2.1.3,Android NDK r8e,Android SDK r22。執行環境 : Sony Xperia TX with Android 4.1.2)

A. 程式架構 :
Cocos2d-x 本身提供一個便利的方法建立 Android Project ( 執行 Cocos2d-x 裡的 create-android-project.sh 即可 ),從自動建立的 Android Project 大概可以了解 App 模式之 Cocos2d-x 與 Android 作業系統間的關係,目前 Cocos2d-x 的程式架構在 Android 環境下,單一 Process 只能同時擁有及處理單一 GL Context 及 GLSurface,若要直接把 Cocos2dxRenderer 及 Cocos2dxGLSurfaceView 整合至 WallpaperService.Engine 內會衍生不少問題。因此這裡實作把它們整合在 WallpaperService。各別 Engine 在進行繪圖時 ( onDrawFrame ),只實作貼圖 ( Texture ) 的動作 ( 使用 Android 系統提供的 GL Function ),其中圖片的來源則是向 WallpaperService 的 Cocos2dxRenderer 取得 ( 此處的 Cocos2dxRenderer 是以 off-screen rendering 方式產生貼圖 )。

在不同的 GL Context 之間分享貼圖的方法,這裡選擇使用 EGL_KHR_gl_texture_2D_image Extension 的功能來實作,可避免因為使用 glReadPixles 及 glTexImage2D 造成效能低落的問題。

B. 實作程式 :

1. 建立 Android Project
從 cocos2d-x 網站下載檔案 cocos2d-2.1rc0-x-2.1.3.zip ,將壓縮檔解開並編輯檔案 create-android-project.sh,找到 :
# set environment paramters
NDK_ROOT_LOCAL="/home/laschweinski/android/android-ndk-r5"
ANDROID_SDK_ROOT_LOCAL="/home/laschweinski/android/android-sdk-linux_86"
將它們修改成自己電腦裡 NDK 及 Android SDK 的路徑,例如 :
# set environment paramters
NDK_ROOT_LOCAL="/Users/macaronics/Downloads/android-ndk-r8e"
ANDROID_SDK_ROOT_LOCAL="/Users/macaronics/android-sdks"
完成之後執行這個 sh 檔案,執行畫面如下 :
執行 create-android-project.sh

輸入 package path (com.macaronics.cclw) 之後,畫面會出現目前 Android SDK 已安裝的 API (依照各電腦情況不同),此範例選擇的是 id:1 (API Level 10),最後輸入專案名稱 (cclw),就完成了。

打開 eclipse 並匯入 cclw 這個專案,完成之後會發現幾個錯誤回報,那是因為找不到 cocos2dx package,此時匯入 cocos2dx 專案即可解決 ( cocos2dx 專案的路徑為 cocos2d-2.1rc0-x-2.1.3/cocos2dx/platform/android/java )。

匯入 cclw 及 cocos2dx 專案

本專案建立時選擇之 API Level 為 10,因此匯入完成之後檢查 AndroidManifest.xml 檔案,將 :
<uses-sdk android:minSdkVersion="8"/>
改為
<uses-sdk android:minSdkVersion="10"/>
到目前為止已經可以正常編譯並執行 cclw 專案,首先在資料夾 cocos2d-2.1rc0-x-2.1.3/cclw/proj.android 底下執行 build_native.sh,完成之後會看到專案裡的 lib 資料夾多出檔案 libgame.so,接著在 eclipse 點選 cclw 專案,並於功能表上選擇 Run --> Run As --> Android Application 即可實際編譯執行。此外也可直接在 Terminal 裡輸入 :
ant release -Dsdk.dir=/Users/macaronics/android-sdks/
編譯 apk 檔案 (/Users/macaronics/android-sdks/ 為 Android SDK 之路徑)。

2. 新增 PixelBuffer
如圖所示,在 cclw/src/com/macaronics/cclw 資料夾底下增加檔案 PixelBuffer.java :
新增檔案 PixelBuffer.java

PixelBuffer 的作用是向系統要求建立 GL Context 與 Surface Buffer ,它和 GLSurfaceView 的功能幾乎一樣,只是它所建立的 Surface Buffer 是 PBuffer (作為 off-screen rendering 使用),與 GLSurfaceView 建立的 Window Surface Buffer 不同。此段程式碼參考的來源在此,但 EGL Config Chooser 的部分參考 GLSurfaceView 做了一些修改,程式碼內容如下 :
public class PixelBuffer
{
    final static String TAG = "PixelBuffer";
    final static boolean LIST_CONFIGS = true;

    GLSurfaceView.Renderer mRenderer; // borrow this interface
    int mWidth, mHeight;
    Bitmap mBitmap;
        
    EGL10 mEGL; 
    EGLDisplay mEGLDisplay;
    EGLConfig[] mEGLConfigs;
    EGLConfig mEGLConfig;
    EGLContext mEGLContext;
    EGLSurface mEGLSurface;
    GL10 mGL;

    EGLConfigChooser mEGLConfigChooser;
    int mEGLContextClientVersion;
    
    String mThreadOwner;
    private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    private static int EGL_LARGEST_PBUFFER        = 0x3058;
    private static int EGL_OPENGL_ES2_BIT          = 4;


    public PixelBuffer(int width, int height) {
        mWidth = width;
        mHeight = height;
        mEGLContextClientVersion =2;
                
        int[] version = new int[2];
        int[] attribList = new int[] {
            EGL_WIDTH,              mWidth,
            EGL_HEIGHT,             mHeight,
            EGL_LARGEST_PBUFFER,    1,
            EGL_NONE
        };

        int [] context_attribList =new int[] {
            EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, 
            EGL_NONE
        };
                
        // No error checking performed, minimum required code to elucidate logic
        mEGL = (EGL10) EGLContext.getEGL();
        mEGLDisplay = mEGL.eglGetDisplay(EGL_DEFAULT_DISPLAY);
        mEGL.eglInitialize(mEGLDisplay, version);
        Log.i("cclw", "eglInitialized, version ="+version[0]+"."+version[1]);
        Log.i("cclw", "EGL Extension : "+mEGL.eglQueryString(mEGLDisplay, EGL_EXTENSIONS));

        mEGLConfigChooser =new SimpleEGLConfigChooser(8, 8, 8, 0, true);
        mEGLConfig =mEGLConfigChooser.chooseConfig(mEGL, mEGLDisplay);
        if (mEGLConfig ==null)
        {
            mEGLConfigChooser =new SimpleEGLConfigChooser(5, 8, 5, 0, true);
            mEGLConfig =mEGLConfigChooser.chooseConfig(mEGL, mEGLDisplay);
        }
        
        mEGLContext = mEGL.eglCreateContext(mEGLDisplay, mEGLConfig, EGL_NO_CONTEXT, context_attribList);
        mEGLSurface = mEGL.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, attribList);
        mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
        mGL = (GL10) mEGLContext.getGL();
        
        // Record thread owner of OpenGL context
        mThreadOwner = Thread.currentThread().getName();
        Log.i("cclw", "PixelBuffer created. mThreadOwner ="+mThreadOwner);

    }

    public void dispose()
    {
        if (mEGLDisplay !=EGL_NO_DISPLAY)
        {
            mEGL.eglMakeCurrent(mEGLDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
            if (mEGLContext !=EGL_NO_CONTEXT)
                mEGL.eglDestroyContext(mEGLDisplay, mEGLContext);

            if (mEGLSurface !=EGL_NO_SURFACE)
                mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface);
            mEGL.eglTerminate(mEGLDisplay);
        }
    }
    
    public void setRenderer(GLSurfaceView.Renderer renderer) {
        mRenderer = renderer;
        
        // Does this thread own the OpenGL context?
        if (!Thread.currentThread().getName().equals(mThreadOwner)) {
            Log.e(TAG, "setRenderer: This thread does not own the OpenGL context.");
            return;
        }
        
        // Call the renderer initialization routines
        mRenderer.onSurfaceCreated(mGL, mEGLConfig);
        mRenderer.onSurfaceChanged(mGL, mWidth, mHeight);
    }

    public void drawFrame()
    {
        makeCurrent();
        mRenderer.onDrawFrame(mGL);
    }

    public void makeCurrent()
    {
        // Do we have a renderer?
        if (mRenderer == null) {
            Log.e(TAG, "getBitmap: Renderer was not set.");
            return;
        }
        
        // Does this thread own the OpenGL context?
        if (!Thread.currentThread().getName().equals(mThreadOwner)) {
            Log.e(TAG, "getBitmap: This thread does not own the OpenGL context.");
            return;
        }
                
        // Call the renderer draw routine
        mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
    } 

    public interface EGLConfigChooser {
        EGLConfig chooseConfig(EGL10 egl, EGLDisplay display);
    }

    private abstract class BaseConfigChooser implements EGLConfigChooser {
        public BaseConfigChooser(int[] configSpec) {
            mConfigSpec =configSpec;
        }

        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
            int[] num_config = new int[1];
            if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config)) {
                throw new IllegalArgumentException("eglChooseConfig failed");
            }

            int numConfigs = num_config[0];

            if (numConfigs <= 0) {
                throw new IllegalArgumentException("No configs match configSpec");
            }

            EGLConfig[] configs = new EGLConfig[numConfigs];
            if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config)) {
                throw new IllegalArgumentException("eglChooseConfig#2 failed");
            }

            //list configs
            listConfig(configs);

            EGLConfig config = chooseConfig(egl, display, configs);
            if (config == null) {
                throw new IllegalArgumentException("No config chosen");
            }
            return config;
        }

        abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs);
        protected int[] mConfigSpec;
    }

    private class ComponentSizeChooser extends BaseConfigChooser {
        public ComponentSizeChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) {
            super(new int[] {
                    EGL10.EGL_RENDERABLE_TYPE,  EGL_OPENGL_ES2_BIT,
                    EGL10.EGL_SURFACE_TYPE,     EGL_WINDOW_BIT | EGL_PBUFFER_BIT,

                    EGL10.EGL_RED_SIZE,         redSize,
                    EGL10.EGL_GREEN_SIZE,       greenSize,
                    EGL10.EGL_BLUE_SIZE,        blueSize,
                    EGL10.EGL_ALPHA_SIZE,       alphaSize,
                    EGL10.EGL_DEPTH_SIZE,       depthSize,
                    EGL10.EGL_STENCIL_SIZE,     stencilSize,

                    EGL10.EGL_NONE});

            mValue = new int[1];
            mRedSize = redSize;
            mGreenSize = greenSize;
            mBlueSize = blueSize;
            mAlphaSize = alphaSize;
            mDepthSize = depthSize;
            mStencilSize = stencilSize;
        }

        @Override
        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display,
                EGLConfig[] configs) {
            for (EGLConfig config : configs) {
                int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0);
                int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0);
                if ((d >= mDepthSize) && (s >= mStencilSize)) {
                    int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0);
                    int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0);
                    int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0);
                    int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0);
                    if ((r == mRedSize) && (g == mGreenSize) && (b == mBlueSize) && (a == mAlphaSize)) {
                        return config;
                    }
                }
            }
            return null;
        }

        private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) {

            if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) {
                return mValue[0];
            }
            return defaultValue;
        }

        private int[] mValue;
        // Subclasses can adjust these values:
        protected int mRedSize;
        protected int mGreenSize;
        protected int mBlueSize;
        protected int mAlphaSize;
        protected int mDepthSize;
        protected int mStencilSize;
        }

    private class SimpleEGLConfigChooser extends ComponentSizeChooser {
        public SimpleEGLConfigChooser(int red, int green, int blue, int alpha, boolean withDepthBuffer) {
            super(red, green, blue, alpha, withDepthBuffer ? 16 : 0, 0);
        }
    }    
    
    private void listConfig(EGLConfig[] tmpConfig) {
        Log.i("cclw", "Config List {");

        for (EGLConfig config : tmpConfig) {
            int d, s, r, g, b, a;
                    
            // Expand on this logic to dump other attributes        
            d = getConfigAttrib(config, EGL_DEPTH_SIZE);
            s = getConfigAttrib(config, EGL_STENCIL_SIZE);
            r = getConfigAttrib(config, EGL_RED_SIZE);
            g = getConfigAttrib(config, EGL_GREEN_SIZE);
            b = getConfigAttrib(config, EGL_BLUE_SIZE);
            a = getConfigAttrib(config, EGL_ALPHA_SIZE);
            Log.i("cclw", "    <d,s,r,g,b,a> = <" + d + "," + s + "," +  r + "," + g + "," + b + "," + a + ">");
        }

        Log.i("cclw", "}");
    }
        
    private int getConfigAttrib(EGLConfig config, int attribute) {
        int[] value = new int[1];
        return mEGL.eglGetConfigAttrib(mEGLDisplay, config,
                        attribute, value)? value[0] : 0;
    }
}
3. EGL_KHR_gl_texture_2D_image Extension 的部分 :
WallpaperService.Engine 命令 Cocos2d-x 以 off-screen rendering 的方式將圖案畫在 PBuffer 裡,然後將 PBuffer 裡的資料複製到自己的 GL Context,此處複製 PBuffer 資料的步驟使用 EGL_KHR_gl_texture_2D_image Extension 以 C 語言來實作,讓 WallpaperService.Engine 以 JNI 的方式呼叫。實作時,直接將函數寫在檔案 cclw/jni/hellocpp/main.cpp 內,底下列出修改後的結果 :
#include "AppDelegate.h"
#include "platform/android/jni/JniHelper.h"
#include <jni.h>
#include <android/log.h>

#include "cocos2d.h"
#include "HelloWorldScene.h"

#define EGL_EGLEXT_PROTOTYPES

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2ext.h>

#define  LOG_TAG    "main"
#define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)

using namespace cocos2d;

extern "C"
{

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JniHelper::setJavaVM(vm);

    return JNI_VERSION_1_4;
}

void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv*  env, jobject thiz, jint w, jint h)
{
    if (!CCDirector::sharedDirector()->getOpenGLView())
    {
        CCEGLView *view = CCEGLView::sharedOpenGLView();
        view->setFrameSize(w, h);

        AppDelegate *pAppDelegate = new AppDelegate();
        CCApplication::sharedApplication()->run();
    }
    /*
    else
    {
        ccDrawInit();
        ccGLInvalidateStateCache();
        
        CCShaderCache::sharedShaderCache()->reloadDefaultShaders();
        CCTextureCache::reloadAllTextures();
        CCNotificationCenter::sharedNotificationCenter()->postNotification(EVNET_COME_TO_FOREGROUND, NULL);
        CCDirector::sharedDirector()->setGLDefaultValues(); 
    }
    */
}

EGLImageKHR eglImage =NULL;
EGLDisplay eglDisplay =NULL;

void Java_com_macaronics_cclw_cclwservice_nativeAttachEGLImageKHR(JNIEnv*  env, jobject thiz)
{
    if (eglImage !=NULL)
    {
        glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage);
    }
}

void Java_com_macaronics_cclw_cclwservice_nativeDestroyEGLImageKHR(JNIEnv*  env, jobject thiz)
{
    if (eglImage !=NULL && eglDisplay !=NULL)
    {
        bool ret =eglDestroyImageKHR(eglDisplay, eglImage);
        LOGD("cclw, nativeDestroyEGLImageKHR ret val =%d", ret);

        eglImage =NULL;
        eglDisplay =NULL;
    }
}

void Java_com_macaronics_cclw_cclwservice_nativeRenderToTexture(JNIEnv*  env, jobject thiz)
{
    int textureID =HelloWorld::renderToTexture();

    if (eglImage ==NULL)
    {
        LOGD("cclw, nativeRenderToTexture - Generate EGLImageKHR ...");
        EGLint imageAttributes[] = {
            EGL_GL_TEXTURE_LEVEL_KHR,   0, // mip map level to reference
            EGL_IMAGE_PRESERVED_KHR,    EGL_FALSE,
            EGL_NONE,                   EGL_NONE
        };

        eglDisplay =eglGetCurrentDisplay();
        EGLContext eglContext =eglGetCurrentContext();
        LOGD("cclw, eglDisplay=%d, eglContext=%d, textureID=%d", eglDisplay, eglContext, textureID);

        eglImage =
            eglCreateImageKHR(
                eglDisplay,
                eglContext,
                EGL_GL_TEXTURE_2D_KHR,
                reinterpret_cast<EGLClientBuffer>(textureID),
                imageAttributes
            );
    }
}

}
其中值得注意的是,並不是所有的 GPU 都支援 EGL_GL_TEXTURE_2D_KHR (例如: Adreno 205 GPU 不支援),此外,函數 :
HelloWorld::renderToTexture();
利用 Cocos2d-x 提供的 CCRenderTexture 物件將圖案畫在 Texture 裡,實作內容如下 (檔案 HelloWorldScene.cpp) :
CCRenderTexture* g_RT =NULL;
int HelloWorld::renderToTexture()
{
    if (g_RT==NULL)
    {
        g_RT =CCRenderTexture::create(720, 1184, kCCTexture2DPixelFormat_RGBA8888);
        g_RT->retain();
        g_RT->setPosition(ccp(720 / 2, 1184 / 2));
        CCLOG("cclw, create RenderTexture (id=%d)...", g_RT->getSprite()->getTexture()->getName());
    }

    g_RT->beginWithClear(0.0f, 0.0f, 0.0f, 1.0f);
    CCDirector::sharedDirector()->mainLoop();
    g_RT->end();

    return g_RT->getSprite()->getTexture()->getName();
}
其中的 720 是畫面寬度,1184 是畫面高度。

4. WallpaperService 的部分 :
新增檔案 cclwservice.java 於資料夾 cclw/src/com/macaronics/cclw 。檔案的程式碼如底下所示,其中 WallpaperService 主要分為 Engine、Renderer 及 Service (負責創造 Engine Instance 、 Cocos2d-x Renderer 執行緒及收拾殘局) :
public class cclwservice extends WallpaperService {
 
    public final static long NANOSECONDSPERSECOND = 1000000000L;
    public final static long NANOSECONDSPERMICROSECOND = 1000000;
 
    public static native void nativeAttachEGLImageKHR();
    public static native void nativeDestroyEGLImageKHR();
    public static native void nativeRenderToTexture();

    //----------------------
    // LIBRARY
    //
        static {
            System.loadLibrary("game");
        }

    //----------------------
    // VARIABLES
    //
        public static cclwservice inst =null;

        public static PixelBuffer pb =null;
        public static Handler ph =null;
        public static HandlerThread mThread;

        private Cocos2dxRenderer pc2r =null;

    //----------------------
    // MAIN FUNCTION
    //
        @Override
        public Engine onCreateEngine() {

            Log.i("cclw", "cclw - onCreateEngine");
            Engine retEng =null;
            retEng =new GLEngine();

            return retEng;
        }

        @Override
        public void onCreate(){
            Log.i("cclw", "cclw - onCreate");
            super.onCreate();
            inst =this;

            mThread =new HandlerThread("Rendering Thread");
            mThread.start();
            ph =new Handler(mThread.getLooper());
            ph.post(new Runnable(){
                @Override
                public void run(){
                    Log.i("cclw", "cclw - calling Cocos2dxHelper.init...");
                    Cocos2dxHelper.init(cclwservice.inst, null);

                    //prepare offscreen buffer
                    Log.i("cclw", "cclw - prepare offscreen buffer...");
                    if (pb ==null)
                        pb =new PixelBuffer(720, 1184);

                    Log.i("cclw", "cclw - create native renderer...");

                    pc2r =new Cocos2dxRenderer();
                    pc2r.setScreenWidthAndHeight(720, 1184);
                    pb.setRenderer(pc2r);
                }
            });
                    
        }

        @Override
        public void onDestroy(){
            Log.i("cclw", "cclw - onDestroy");
            super.onDestroy();

            //dispose handler
            ph =null;

            //dispose thread
            Log.i("cclw", "cclw - dispose Rendering Thread...");
            mThread.quit();

            cclwservice.nativeDestroyEGLImageKHR();

            Log.i("cclw", "cclw - dispose PixelBuffer");
            if (pb !=null){
                pb.dispose();
            }
            pb =null;

        }

        synchronized static public void renderToTexture(){
            pb.makeCurrent();
            cclwservice.nativeRenderToTexture();
        }

}
函數 renderToTexture 執行時,會透過 JNI 呼叫 cclw/jni/hellocpp/main.cpp 檔案裡的函數 Java_com_macaronics_cclw_cclwservice_nativeRenderToTexture 讓 Cocos2d-x 將圖案畫在 PixelBuffer 所建立之 PBuffer 的 Texture 裡,並將此 Texture 設定為 KHR_image。

底下為 Engine 部分的程式 ( Engine 負責建立自己的 Renderer、接收系統訊息,及乎如同一般的 Activity ) :
//----------------------
// GL Engine
//
private class GLEngine extends Engine
{

    //----------------------
    // VARIABLES
    //
        private WallpaperGLSurfaceView glSurfaceView;
        private boolean rendererHasBeenSet;

        private WallpaperGLRenderer glRenderer;
         
    //----------------------
    // FUNCTION
    //                 
        @Override
        public Bundle onCommand(String action, int x, int y, int z, Bundle extras, boolean resultRequested) {
            Log.i("cclw", "onCommand");
            return super.onCommand(action, x, y, z, extras, resultRequested);
        }

        @Override
        public void onCreate(SurfaceHolder surfaceHolder) {
            super.onCreate(surfaceHolder);

            setTouchEventsEnabled(true);

            Log.i("cclw", "onCreate");
            glSurfaceView = new WallpaperGLSurfaceView(cclwservice.this);

            setEGLContextClientVersion(2);
            glRenderer =new WallpaperGLRenderer();
            setRenderer(glRenderer);

        }

        @Override
        public void onDestroy() {
            super.onDestroy();

            glRenderer.onDestroy();
            glRenderer =null;

            Log.i("cclw", "onDestroy");
            glSurfaceView.onDestroy();
        }

        @Override
        public void onVisibilityChanged(boolean visible) {
            super.onVisibilityChanged(visible);
         
            Log.i("cclw", "onVisibilityChanged, visible ="+visible);
            if (rendererHasBeenSet) {
                if (visible) {
                    Log.i("cclw", "calling onResume...");
                    glSurfaceView.onResume();
                } else {
                    Log.i("cclw", "calling onPause...");
                    glSurfaceView.onPause();
                }
            }
        }

        @Override
        public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset)
        {
            super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset);
            //Log.i("cclw", "onOffsetsChanged, xOffset="+xOffset+", yOffset="+yOffset+", xOffsetStep="+xOffsetStep+", yOffsetStep="+yOffsetStep+", xPixelOffset="+xPixelOffset+", yPixelOffset="+yPixelOffset);
        }

        protected void setRenderer(Renderer renderer){
            glSurfaceView.setRenderer(renderer);
            rendererHasBeenSet =true;
        }

        protected void setEGLContextClientVersion(int version) {
              glSurfaceView.setEGLContextClientVersion(version);
        }


    //------------------------
    // Custom SurfaceView for WallpaperService.engine
    //
        private class WallpaperGLSurfaceView extends GLSurfaceView {
         
            WallpaperGLSurfaceView(Context context) {
                super(context);
            }
         
            @Override
            public SurfaceHolder getHolder() {
                return getSurfaceHolder();
            }
        }

}
值得注意的是 WallpaperGLSurfaceView 類別裡的函數 getHolder,是透過 Engine 類別的函數 getSurfaceHolder 來取得 SurfaceHolder。此外,Engine 所建立的 Renderer 與 Cocos2d-x Renderer 無關,它擁有自己的 GL Context 及 Window Surface Buffer。

WallpaperGLRenderer 的部分 : (底下的程式如同一般的 GL 程式,使用 OpenGL ES 2.0 來顯示一張 Texture,只是這個 Texture 的資料是 PixelBuffer 的 PBuffer 的資料 )
//----------------------
// RENDERER
//
private class WallpaperGLRenderer implements GLSurfaceView.Renderer{

    private String TAG ="cclw";
    
    int[] mTextureNameWorkspace =new int[] {0};

    private final float[] mTriangleVerticesData = { -1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f };
    private final float[] mTriangleNormalData ={0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f};
    private final float[] mTriangleTexCoordData ={0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f };
    private final float[] mTriangleTexCoordData2 ={1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f };

    private FloatBuffer mTriangleVertices;
    private FloatBuffer mTriangleNormal;
    private FloatBuffer mTriangleTexCoord;
    private FloatBuffer mTriangleTexCoord2;

    private final String mVertexShader = "attribute vec4 a_position;   \n"+
        "attribute vec3 a_normal;     \n"+
        "attribute vec2 a_texCoord;   \n"+
        "varying vec2 v_texCoord;     \n"+
        "varying vec3 v_normal;       \n"+
        "void main()                  \n"+
        "{                            \n"+
        "   gl_Position =a_position;  \n"+
        "   v_normal = a_normal;      \n"+
        "   v_texCoord = a_texCoord;  \n"+
        "}                            \n";

    private final String mFragmentShader = "precision mediump float; \n"+
                "varying vec2 v_texCoord;                            \n"+
                "varying vec3 v_normal;                              \n"+
                "uniform sampler2D s_texture;                        \n"+
                "void main()                                         \n"+
                "{                                                   \n"+
                "  gl_FragColor = texture2D( s_texture, v_texCoord );\n"+
                "}                                                   \n";
    
    private int mProgram;
    private int mvPositionHandle;
    private int mvNormalHandle;
    private int mvTexCoordHandle;
    private int mvSamplerHandle;

    private long mLastTickInNanoSeconds;
    public long aniInterval;

    public int mOrientation =0;

    public boolean onDestroyCalled =false;

    public WallpaperGLRenderer() {
        mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleVertices.put(mTriangleVerticesData).position(0);

        mTriangleNormal = ByteBuffer.allocateDirect(mTriangleNormalData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleNormal.put(mTriangleNormalData).position(0);

        mTriangleTexCoord = ByteBuffer.allocateDirect(mTriangleTexCoordData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleTexCoord.put(mTriangleTexCoordData).position(0);

        mTriangleTexCoord2 =ByteBuffer.allocateDirect(mTriangleTexCoordData2.length *4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleTexCoord2.put(mTriangleTexCoordData2).position(0);

        this.mLastTickInNanoSeconds =System.nanoTime();
        aniInterval =(long) (1.0 / 30 * NANOSECONDSPERSECOND);
    }

    public void onDestroy(){
        Log.i("cclw", "onDestroy called...");
        onDestroyCalled =true;
    }
    
    protected class WorkerRunnable implements Runnable{
        private final CountDownLatch doneSignal;
        WorkerRunnable(CountDownLatch doneSignal){
            this.doneSignal =doneSignal;
        }

        public void run(){
            try{
                cclwservice.renderToTexture();
                this.doneSignal.countDown();
            }catch(Exception ex){
                Log.i("cclw", "Error : Runnable return exception : "+ex);

            }                    
        }
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        try{
                final long nowInNanoSeconds = System.nanoTime();
                final long interval = nowInNanoSeconds - this.mLastTickInNanoSeconds;

                //------------------------
                // FETCH DATA
                //
                    EGL10 mEGL = (EGL10) EGLContext.getEGL();
                    EGLSurface mEGLSurface =mEGL.eglGetCurrentSurface(EGL10.EGL_DRAW);
                    EGLDisplay mEGLDisplay =mEGL.eglGetCurrentDisplay();
                    EGLContext mEGLContext =mEGL.eglGetCurrentContext();

                    CountDownLatch doneSignal =new CountDownLatch(1);
                    cclwservice.ph.post(new WorkerRunnable(doneSignal));

                    doneSignal.await();

                //------------------------
                // SETUP BASIC ENVIRONMENT
                //
                    mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
                    if (onDestroyCalled==true)
                    {
                        Log.i("cclw", "onDestroyCalled==true, ignore drawing...");
                        return;
                    }

                //------------------------
                // UPDATE TEXTURE
                //
                    if (mTextureNameWorkspace[0]==0){                                                            
                        //Load texture
                        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
                        gl.glGenTextures(1, mTextureNameWorkspace, 0);
                        Log.i("cclw", "mTextureNameWorkspace[0]="+mTextureNameWorkspace[0]);

                    }

                    gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureNameWorkspace[0]);

                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);

                    cclwservice.nativeAttachEGLImageKHR();

                    gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);


                //------------------------
                // RENDER SCENE
                //
                    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
                    GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
                    GLES20.glUseProgram(mProgram);

                    GLES20.glVertexAttribPointer(mvPositionHandle, 3, GLES20.GL_FLOAT, false, 0, mTriangleVertices);
                    GLES20.glVertexAttribPointer(mvNormalHandle, 3, GLES20.GL_FLOAT, false, 0, mTriangleNormal);

                    if (mOrientation ==1)
                        GLES20.glVertexAttribPointer(mvTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTriangleTexCoord);
                    else if(mOrientation ==0)
                        GLES20.glVertexAttribPointer(mvTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTriangleTexCoord2);

                    GLES20.glEnableVertexAttribArray(mvPositionHandle);
                    GLES20.glEnableVertexAttribArray(mvNormalHandle);
                    GLES20.glEnableVertexAttribArray(mvTexCoordHandle);

                    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
                    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureNameWorkspace[0]);
                    GLES20.glUniform1i(mvSamplerHandle, 0);

                    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);

                //------------------------
                // FPS Limitation
                //
                    final long val =(aniInterval - interval) / NANOSECONDSPERMICROSECOND;
                    if (val >0)
                    {
                        try{
                            Thread.sleep(val);
                        }catch(final Exception e){}
                    }

                    this.mLastTickInNanoSeconds = nowInNanoSeconds;

        }
        catch(Exception e){

        }
    }

    @Override
    public void onSurfaceChanged(GL10 arg0, int arg1, int arg2) { //arg1:width, arg2:height
        // TODO Auto-generated method stub
        Log.i("cclw", "WallpaperGLRenderer::onSurfaceChanged, arg1="+arg1+", arg2="+arg2);

        float myRatio =1184.0f/720.0f;
        mOrientation =0;
        if (arg2>=arg1)
        {
            myRatio =720.0f/1184.0f;
            mOrientation =1;
        }

        int targetHeight =arg2;
        int targetWidth =(int)((float)arg2*myRatio);

        if (targetWidth >arg1)
        {
            targetWidth =arg1;
            targetHeight =(int)((float)arg1/myRatio);
        }

        Log.i("cclw", "WallpaperGLRenderer::onSurfaceChanged, fit targetWidth="+targetWidth+", targetHeight="+targetHeight);
        GLES20.glViewport((int)((arg1-targetWidth)*0.5f), (int)((arg2-targetHeight)*0.5f), targetWidth, targetHeight);
    }

    @Override
    public void onSurfaceCreated(GL10 arg0, EGLConfig arg1) {
        // TODO Auto-generated method stub
        Log.i("cclw", "WallpaperGLRenderer::onSurfaceCreated");

        mProgram =createProgram(mVertexShader, mFragmentShader);
        if (mProgram ==0)
            return;

        mvPositionHandle =GLES20.glGetAttribLocation(mProgram, "a_position");
        if (mvPositionHandle ==-1)
            return;
        
        mvNormalHandle =GLES20.glGetAttribLocation(mProgram, "a_normal");
        mvTexCoordHandle =GLES20.glGetAttribLocation(mProgram, "a_texCoord");
        mvSamplerHandle =GLES20.glGetUniformLocation(mProgram, "s_texture");

        GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
    }

    ...

其中值得注意的是函數 onDrawFrame 被系統呼叫之後,會產生一個實體 WorkerRunnable ,讓負責 cocos2d-x 繪圖的執行緒去執行,目的是讓 cocos2d-x 更新 PBuffer 裡的資料,更新完成之後,onDrawFrame 才繼續執行貼圖的動作。

5. 設定 AndroidManifest.xml
為 WallpaperService 新增 service 標籤,如底下所示 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.macaronics.cclw"
      android:versionCode="1"
      android:versionName="1.0">

    <uses-sdk android:minSdkVersion="10"/>
    <uses-feature android:glEsVersion="0x00020000" />

    <application android:label="@string/app_name"
        android:icon="@drawable/icon">

        <activity android:name=".cclw"
                  android:label="@string/app_name"
                  android:screenOrientation="landscape"
                  android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
                  android:process="com.macaronics.cclw.cclw"
                  android:configChanges="orientation">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <service
            android:name=".cclwservice"
            android:label="@string/app_name"
            android:screenOrientation="portrait"
            android:permission="android.permission.BIND_WALLPAPER"
            android:process="com.macaronics.cclw.cclwservice">
            <intent-filter>
                <action android:name="android.service.wallpaper.WallpaperService" />
            </intent-filter>
            <meta-data android:name="android.service.wallpaper" android:resource="@xml/cclw_res" />
        </service>
                
    </application>
    <supports-screens android:largeScreens="true"
                      android:smallScreens="true"
                      android:anyDensity="true"
                      android:normalScreens="true"/>
</manifest> 
其中的 xml/cclw_res.xml 表示 Live Wallpaper 選單列表時所顯示之資訊,需要額外手動建立,步驟是首先建立資料夾 xml 於 cclw/res,然後建立檔案 cclw_res.xml,並設定內容如下 :
<?xml version="1.0" encoding="utf-8"?>
<wallpaper 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:thumbnail="@drawable/icon" 
/>

6. 設定 jni/Android.mk :
此時若執行 native_build.sh 會顯示一些錯誤,那是因為尚未設定連結 EGL Library,將 jni/Android.mk 修改如下即可正常編譯 :
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := game_shared

LOCAL_MODULE_FILENAME := libgame

LOCAL_SRC_FILES := hellocpp/main.cpp \
                   ../../Classes/AppDelegate.cpp \
                   ../../Classes/HelloWorldScene.cpp
                   
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes                   

LOCAL_LDLIBS := -lEGL

LOCAL_WHOLE_STATIC_LIBRARIES := cocos2dx_static cocosdenshion_static cocos_extension_static
            
include $(BUILD_SHARED_LIBRARY)

$(call import-module,CocosDenshion/android) \
$(call import-module,cocos2dx) \
$(call import-module,extensions)

7. 執行結果 :
動態桌布選單多出了 cocos2d-x 建立的 cclw


將 cclw 設定成動態桌布

完整範例程式碼可以到 Git 下載: http://github.com/phardera/cocos2dx_android_livewallpaper

2013-06-27

BeagleBone on OSX Mountain Lion

依照官方提供的 Getting Started 步驟來執行的結果就是卡在 Step3,... 無法 Launch USB-to-Ethernet interface 因此沒辦法開啟 http://192.168.7.2 的網頁 (使用的 BeagleBone 版本是 A6a)。
底下是更詳細的步驟:
  1. 安裝 Driver : FTDI USB to serial。
  2. 將 BeagleBorad 接上 mac,此時在 Finder 會看到一個 BEAGLE_BONE 的 USB Disk,然後 Eject 這個 Disk。 
  3. 此時 System Preferences 的 Network 裡應該會自動多出 interface : RNDIS/Ethernet Gadget,如圖所示 :
    左方有 RNDIS/Ethernet Gadget

  4. 若沒發現 RNDIS/Ethernet Gadget 的話是正常的,因為我的 Mountain Lion 也沒有。
  5. 開啟 Terminal,輸入  screen `ls /dev/{tty.usb*B,beaglebone-serial}` 115200
  6. Terminal 全黑之後再按下 Enter,出現 Login 畫面,如圖所示 :
    登入頁面

  7. 輸入 root 後登入,再輸入 systemctl start network-gadget-init.service
  8. 完成!RNDIS/Ethernet Gadget 出現!
  9. 開啟瀏覽器並輸入 http://192.168.7.2
在 BeagleBone 內建的作業系統上編譯新版 node.js :
  1. 執行 opkg install python python-compiler python-modules python-distutils python-misc
  2. 執行 opkg install openssl openssl-dev
  3. 使用 wget 下載新版 nodejs 然後使用 tar xf 解壓縮之
  4. 修改檔案 deps/v8/SConstruct 內的 :
    'CCFLAGS' : ['$DIALECTFLAGS', '$WARNINGFLAGS'],
    
    'CCFLAGS' : ['$DIALECTFLAGS', '$WARNINGFLAGS', '-march=armv7-a'],
    
  5. 執行 export CC='gcc -march=armv7-a -mtune=cortex-a8'
  6. 執行 export CXX='g++ -march=armv7-a -mtune=cortex-a8'
  7. 執行 ./configure
  8. 執行 make
ps.
1. Eject USB Disk 的指令是 systemctl stop storage-gadget-init.service
2. 在 OSX 上設定好 Internet Sharing 之後鍵入 udhcpc -i usb0 則可透過 OSX 連結上 Internet
3. 資料參考來源 https://groups.google.com/forum/#!topic/beagleboard/gKQtFpdpB6k
4. 資料參考來源 http://brachestudios.com/2012/03/13/installing-the-latest-version-of-node-js-on-the-beaglebone/
5. 資料參考來源 http://fastr.github.io/articles/Node.js-on-OpenEmbedded.html

2013-04-08

Unity 3D + Facebook SDK for Android


底下是將 Facebook SDK 打包成 Facebook Plugin 的步驟,執行的平台是 Android,Unity3D 的版本是 3.5。

1. 在這裡取得 Facebook SDK 。
2. 開啟 eclipse 之後在功能表上選擇 File --> Import...,出現對話框之後在 import source 選擇 Existing Android Code Into Workspace。

3. 在 Root Directory 選擇剛剛下載的 SDK 資料夾 (此範例為 /Users/macaronics/facebook-android-sdk-3.0.1),Project to Import 的部分只要勾選 facebook (Facebook SDK) 即可。

4. 按下 Finish 之後在 Package Explorer 應該可以發現 Eclipse 已經自動產生一些檔案 (FacebookSDK/gen) 並且自動編譯好 facebooksdk.jar (FacebookSDK/bin) 了。

5. 建立一個 Unity Project,並將 facebooksdk.jar 及 res 資料夾複製到 Unity 專案的 Assets/Plugins/Android 資料夾底下。(ps. res 資料夾是指 facebook-android-sdk-3.0.1/facebook/res)

6. 複製 android-support-v4.jar  至 Unity 專案的 Assets/Plugins/Android 資料夾底下。( ps. facebook sdk 裡的 android-support-v4.jar 版本似乎太舊所以在編譯時會出問題,因此檔案可以從 android sdk 裡的 extras/android/support/v4 資料夾取得)

7. 建立 AndroidManifest.xml 檔案至 Assets/Plugins/Android 資料夾,內容如下 :
<?xml version="1.0" encoding="utf-8"?>
<manifest android:versionCode="1" android:versionName="1.0" 
          android:installLocation="preferExternal" 
          package="com.macaronics.fbonandroid" 
          xmlns:android="http://schemas.android.com/apk/res/android">
  <supports-screens android:anyDensity="true" android:smallScreens="true" 
                    android:normalScreens="true" android:largeScreens="true" 
                    android:xlargeScreens="true" />
  <application android:label="@string/app_name" 
               android:icon="@drawable/app_icon" android:debuggable="false">
    <activity android:label="@string/app_name" 
              android:name="com.macaronics.iab.overrideActivity"
              android:screenOrientation="portrait" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"              
              >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity android:label="@string/app_name" 
              android:name="com.unity3d.player.UnityPlayerActivity" 
              android:screenOrientation="portrait" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
              />
    <activity android:label="@string/app_name" 
              android:name="com.unity3d.player.UnityPlayerNativeActivity" 
              android:screenOrientation="portrait" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
              >
      <meta-data android:name="android.app.lib_name" android:value="unity" />
      <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" 
                 android:value="false" />
    </activity>
    <activity android:label="@string/app_name" 
              android:name="com.unity3d.player.VideoPlayer" 
              android:screenOrientation="behind" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale" 
              />
    <activity android:name="com.facebook.LoginActivity" android:label="@string/app_name" />
  </application>
  <uses-feature android:glEsVersion="0x20000" />
 
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

</manifest>
值得注意的是這裡設定了 activity "com.facebook.LoginActivity" 以及 Permission "INTERNET"。

8. 利用 git ( https://github.com/douglascrockford/JSON-java.git ) 取得 org.json 原始碼並以下述指令編譯 Java (在相同資料夾輸入指令) :
javac *.java -d .
完成之後輸入 :
jar cvfM json.jar org/
然後將產生的 json.jar 檔案置於 Assets/Plugins/Android 資料夾底下。

9. 建立 overrideActivity.jar ,功用在於 relay 系統資訊給 facebook SDK。首先建立 overrideActivity.java 檔案如下 :
package com.macaronics.iab;

import com.unity3d.player.UnityPlayerActivity;
import android.os.Bundle;
import android.util.Log;
import android.content.Intent;

public class overrideActivity extends UnityPlayerActivity {
    public interface cbEvent{
        public boolean cbEvent(int requestCode, int resultCode, Intent data);
    }
    
    public interface cbSaveInstState{
        public boolean cbSaveInstState(Bundle outState);
    }

    protected cbEvent ie;
    protected cbSaveInstState isis;
    protected Bundle mSavedInstState;

    static protected overrideActivity inst;
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        inst =this;
        mSavedInstState =savedInstanceState;
        
        // print debug message to logcat
        Log.d("Unity::overrideActivity", "onCreate called!");
    }

    @Override
 public void onDestroy(){
        super.onDestroy();
        inst =null;
        Log.d("Unity::overrideActivity", "onDestroy called!");
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data){
        Log.d("Unity::overrideActivity", "onActivityResult called!");
        
        boolean ret =false;
        if (ie !=null){
            try{
                ret =ie.cbEvent(requestCode, resultCode, data);
            }
            catch(Exception e){
                ret =false;
            }
        }

        if (ret ==false){
            super.onActivityResult(requestCode, resultCode, data);
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        Log.d("Unity::overrideActivity", "onSaveInstanceState called!");
    
        boolean ret =false;
        
        if (isis !=null)
        {
            try{
                ret =isis.cbSaveInstState(outState);
            }
            catch(Exception e){
                ret =false;
            }
        }
        
        if (ret ==false){
            super.onSaveInstanceState(outState);
        }
    }
    
    static public void registerOnActivityResultCBFunc(final cbEvent pcbfunc){
        if (inst !=null)
            inst.ie =pcbfunc;
    }
    
    static public void registerOnSaveInstanceSataeCBFunc(final cbSaveInstState pcbfunc){
        if (inst !=null)
            inst.isis =pcbfunc;
    }
    
    static public Bundle getSavedInstanceState()
    {
        if (inst !=null)
            return inst.mSavedInstState;
        
        return null;
    }
} 
編譯指令為 :
javac ./overrideActivity.java -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar:/Applications/Unity3.5.6/Unity/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar -d .
其中,引入的 library 包含了 android-10/android.jar (Android 2.3.3) ,以及 Unity 的 classes.jar。編譯完成後將其打包成 overrideActivity.jar :
jar cvfM ../overrideActivity.jar com/
將產生的 overrideActivity.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下。此外,須注意資料夾 com 要刪除,以免後續執行 jar 時,打包到舊的 com package。

10. 建立 fbWrapper.jar (主要用於介接 facebook SDK 及 Unity),首先建立 fbWrapper.java 檔案如下 :
package com.macaronics;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.util.Base64;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.Toast;

import com.facebook.*;
import com.facebook.widget.WebDialog;
import com.facebook.widget.WebDialog.OnCompleteListener;
import com.facebook.model.GraphObject;

import com.unity3d.player.UnityPlayer;
import com.macaronics.iab.overrideActivity;

import org.json.JSONArray;


public class fbWrapper
{
    private String myHash;
    private String mEventHandler;
    private Activity mActivity;
    private String mAppId;
    
    public fbWrapper(String packageName, String applicationId, String strEventHandler){

        mActivity =UnityPlayer.currentActivity;
        mEventHandler =strEventHandler;
        mAppId =applicationId;
        
        //--------------------
        //fetch keyhash
        try
        {
            PackageInfo info =mActivity.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
            for (Signature sig : info.signatures){
                MessageDigest md =MessageDigest.getInstance("SHA");
                md.update(sig.toByteArray());
                myHash =Base64.encodeToString(md.digest(), Base64.DEFAULT);
            }
        }
        catch(NameNotFoundException e){
            Log.d("Unity::fbWrapper()", "error on fetch haskkey -name not found");
        }
        catch(NoSuchAlgorithmException e){
            Log.d("Unity::fbWrapper()", "error on fetch haskkey -no such algorithm");
        }

        
        //--------------------
        //register callback (onActivityResult)
        overrideActivity.registerOnActivityResultCBFunc(
            new overrideActivity.cbEvent(){
                public boolean cbEvent(int requestCode, int resultCode, Intent data)
                {
                    Session.getActiveSession().onActivityResult(mActivity, requestCode, resultCode, data);
                    return false;
                }
            });
        
        //--------------------
        //register callback (onSaveInstanceState)
        overrideActivity.registerOnSaveInstanceSataeCBFunc(
            new overrideActivity.cbSaveInstState(){
                public boolean cbSaveInstState(Bundle outState)
                {
                    Session session = Session.getActiveSession();
                    Session.saveSession(session, outState);
                    return false;
                }
            });
        
        //--------------------
  //facebook
        Bundle savedInstanceState =overrideActivity.getSavedInstanceState();
  Settings.addLoggingBehavior(LoggingBehavior.INCLUDE_ACCESS_TOKENS);
  Session session =Session.getActiveSession();
        if (session == null) {
            if (savedInstanceState != null) {
                session = Session.restoreSession(mActivity, null, statusCallback, savedInstanceState);
                Log.d("Unity::onCreate()", "Session.restoreSession()");
            }
            if (session == null) {
             session = new Session.Builder(mActivity).setApplicationId(mAppId).build();
                Log.d("Unity::onCreate()", "new Session.Builder(this)");
            }
            
            Session.setActiveSession(session);
            if (session.getState().equals(SessionState.CREATED_TOKEN_LOADED)) {
                session.openForRead(new Session.OpenRequest(mActivity).setCallback(statusCallback));
                Log.d("Unity::onCreate()", "openForRead()");
            }
        }        
    }

    public void dispose()
    {
        Log.d("Unity::dispose()", "dispose called");
    }
    
    public boolean isSessionOpened()
    {
        Session session =Session.getActiveSession();
        if (session !=null && session.isOpened()==true && session.getAccessToken() !=null)
        {
            return true;
        }
        
        return false;
    }

    public String getHash()
    {
        return myHash;
    }
    
    public void login()
    {
        Session session = Session.getActiveSession();
        if (!session.isOpened() && !session.isClosed()) {
            Log.d("Unity::onLogin()","session.openForRead()");
            session.openForRead(new Session.OpenRequest(mActivity).setCallback(statusCallback));
        }else if (session !=null){
            Log.d("Unity::onLogin()","Session.openActiveSession()");

            //regenerate session
            Session newsession = new Session.Builder(mActivity).setApplicationId(mAppId).build();
            Session.setActiveSession(newsession);
            newsession.openForRead(new Session.OpenRequest(mActivity).setCallback(statusCallback));
        }
        else
        {
         Log.d("Unity", "error -session ==null");
        }
    }
    
    public void logout()
    {
     Log.d("Unity::onLogout()","on logout called !");
        Session session = Session.getActiveSession();
        if (!session.isClosed()) {
            session.closeAndClearTokenInformation();
            Log.d("Unity::onLogout()","session.closeAndClearTokenInformation()");
        }
    }
    
    public void invokeQuery(String fqlQueryStr)
    {
     Session session =Session.getActiveSession();
     if (session.isOpened()==false)
     {
      Log.d("Unity::invokeQuery()", "invalid session");
      return;
     }

     Bundle params =new Bundle();
     params.putString("q", fqlQueryStr);

     final Request request =new Request(session,
       "/fql",
       params,
       HttpMethod.GET,
       new Request.Callback(){
        public void onCompleted(Response response){
                        GraphObject graphObject = response.getGraphObject();
                        if (graphObject !=null)
                        {
                            if (graphObject.getProperty("data")!=null &&
                                mEventHandler !=null)
                            {
                                try{
                                    String arr =graphObject.getProperty("data").toString();
                                    JSONArray pj =new JSONArray(arr);
                                    UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"ret\":\"true\",\"dat\":"+pj.toString()+"}");
                                }
                                catch(Exception e)
                                {
                                    if (mEventHandler!=null)
                                        UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"ret\":\"false\"}");
                                }
                                
                            }
                            else
                            {
                                if (mEventHandler!=null)
                                    UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"ret\":\"false\"}");
                            }
                            
                        }
                        else
                        {
                            if (mEventHandler!=null)
                                UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"ret\":\"false\"}");
                        }
                        
        }

       });

     Log.d("Unity::invokeQuery()", "send fql request...");
        
        mActivity.runOnUiThread(new Runnable(){
            public void run(){
                Request.executeBatchAsync(request);                
            }
        });


    }

    public void sendRequest(String reqMessage)
    {
     final Session session =Session.getActiveSession();
     if (session.isOpened()==false)
     {
      Log.d("Unity::SendRequest()", "invalid session");
      return;
     }

     final Bundle params =new Bundle();
     params.putString("message", reqMessage);

     final Activity act =mActivity;

        mActivity.runOnUiThread(new Runnable(){
            public void run(){
                
                WebDialog requestDialog =(
                    new WebDialog.RequestsDialogBuilder(act,
                        session.getActiveSession(),
                        params))
                        .setOnCompleteListener(new OnCompleteListener(){

                            @Override
                            public void onComplete(Bundle values,
                                    FacebookException error){
                                if (error !=null)
                                {
                                    if (error instanceof FacebookOperationCanceledException){
                                        Toast.makeText(act.getApplicationContext(),
                                        "Request cancelled",
                                        Toast.LENGTH_SHORT).show();
                                    }
                                    else
                                    {
                                        Toast.makeText(act.getApplicationContext(), "Network Error", Toast.LENGTH_SHORT).show();
                                    }
                                }
                                else
                                {
                                    final String requestId =values.getString("request");
                                    if (requestId !=null)
                                    {
                                        Toast.makeText(act.getApplicationContext(),
                                        "Request sent",
                                        Toast.LENGTH_SHORT).show();
                                    }
                                    else
                                    {
                                        Toast.makeText(act.getApplicationContext(),
                                        "Request cancelled",
                                        Toast.LENGTH_SHORT).show();
                                    }
                                }
                            }
                    })
                    .build();
                requestDialog.show();
                
            }
        });

    }
    
    private Session.StatusCallback statusCallback = new SessionStatusCallback();
    private class SessionStatusCallback implements Session.StatusCallback {
        @Override
        public void call(Session session, SessionState state, Exception exception) {
            if (state.isOpened())
            {
                Log.d("Unity::onSessionStateChange()", "state.isOpened()");
            }
            else
            {
                Log.d("Unity::onSessionStateChange()", "state.isOpened() ===false");
            }
        }
    }

}
其中除了 login 及 logout 外,還提供了兩個重要的功能,一是 FQL Query,另一個是 Requests Dialog。執行編譯的指令為 :
javac ./fbWrapper.java -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar:/Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar:./facebooksdk.jar:./overrideActivity.jar:./android-support-v4.jar:./json.jar -d .
其中,引入的 library 包含了 android-10/android.jar (Android 2.3.3),Unity 的 classes.jar,facebooksdk.jar,先前編譯的 overrideActivity.jar,json.jar,以及 android-support-v4.jar。編譯完成後將其打包成 fbWrapper.jar :
jar cvfM ../fbWrapper.jar com/
將產生的 fbWrapper.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下。直至目前為止,資料夾底下應該有這些檔案 :

11. 建立 fbWrapper.cs ,讓 fbWrapper class 更適用於 Unity 的 Component 架構之環境,程式碼如下 :
using UnityEngine;

using System.Collections;
using System.Collections.Generic;

public class fbWrapper : MonoBehaviour {

    AndroidJavaObject fbWrapperObj =null;
    public static fbWrapper inst =null;

 // Use this for initialization
 void Start () {
        inst =this;
 }

    void OnApplicationQuit(){
        dispose();
        inst =null;
    }

    public bool init(string unity_bundle_id, string fb_app_id)
    {
        dispose();
        fbWrapperObj =new AndroidJavaObject("com.macaronics.fbWrapper", new object[3]{unity_bundle_id, fb_app_id, "fbWrapper"});

        return true;
    }

    public void dispose()
    {
        if (fbWrapperObj !=null)
        {
            fbWrapperObj.Call("dispose");
            fbWrapperObj.Dispose();
            fbWrapperObj =null;
        }
    }

    void msgReceiver(string msg)
    {
        if (tmpCBFunc !=null)
        {
            Dictionary<string, object> cache =(Dictionary<string, object>)MiniJSON.Json.Deserialize(msg);
            if (cache.ContainsKey("ret")==true)
            {
                string ret =(string)cache["ret"];
                if (ret=="false")
                {
                    tmpCBFunc(false, null);
                }
                else
                {
                    if (cache.ContainsKey("dat")==true)
                    {
                        tmpCBFunc(true, cache["dat"]);
                    }
                }
            }
            else
            {
                tmpCBFunc(false, null);
            }
        }
    }

    //目前是否已登入
    public bool isSessionAvailable()
    {
        if(fbWrapperObj==null)
            return false;

        return fbWrapperObj.Call<bool>("isSessionOpened");
    }

    //登入
    public bool login()
    {
        if (fbWrapperObj !=null)
        {
            fbWrapperObj.Call("login");
            return true;
        }

        return false;
    }

    //登出
    public bool logout()
    {
        if (fbWrapperObj !=null)
        {
            fbWrapperObj.Call("logout");
            return true;
        }
        return false;
    }

    public string getHashKey()
    {
        string myhash =fbWrapperObj.Call<string>("getHash");
        if (myhash ==null)
            myhash ="package name not found";

        return myhash;
    }

    public delegate void queryCBFunc(bool ret, object data);
    protected queryCBFunc tmpCBFunc =null;
    public bool invokeQuery(string fql, queryCBFunc pcbfunc)
    {
        tmpCBFunc =pcbfunc;
        if (fbWrapperObj !=null)
        {
            fbWrapperObj.Call("invokeQuery", new object[1]{fql});
            return true;
        }
        return false;
    }

    //送出邀請
    public bool sendRequest(string message)
    {
        if (fbWrapperObj !=null)
        {
            fbWrapperObj.Call("sendRequest", new object[1]{message});
            return true;
        }
        return false;
    }

}
在 Unity 建立一個 GameObject 並命名為 fbWrapper,然後將 fbWrapper.cs 設定成此 GameObject 之 Component。

此外值得注意的是,函數 init 的第一個參數是 Unity 之 Bundle Identifier,第二個參數是 Facebook App ID,此 ID 應該在建立 Facebook App 時,由 Facebook 所提供,如圖所示 :

函數 getHashKey 所回傳的字串必須設定至 Native Android App 之 Key Hash 欄位 ,如圖所示 :

12. 底下是簡單的 fbWrapper 應用範例,建立一個  C# script 名稱為 test.cs 並設定成為 GameObject "Main Camera" 之 Component。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class test : MonoBehaviour {

 // Use this for initialization
    string myhash =null;
 void Start () {
        fbWrapper.inst.init("com.macaronics.fbonandroid", "FB_APP_ID");
        myhash =fbWrapper.inst.getHashKey();
 }

    void OnGUI()
    {
        if (GUI.Button(new Rect(10,10,100,100), "login"))
        {
            fbWrapper.inst.login();
            myhash =fbWrapper.inst.getHashKey();
        }

        if (GUI.Button(new Rect(10,120,100,100), "logout"))
        {
            fbWrapper.inst.logout();
            queryresult =null;
            myhash =null;
        }

        if (fbWrapper.inst.isSessionAvailable())
        {
            if (GUI.Button(new Rect(10,230,100,100), "invoke query"))
            {
                fbWrapper.inst.invokeQuery("SELECT uid, name, pic_square, is_app_user FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 =me())", queryCB);
            }

            if (GUI.Button(new Rect(10,340,100,100), "send request"))
            {
                fbWrapper.inst.sendRequest("check out my app !");
            }
        }

        if (myhash !=null)
        {
            GUI.Label(new Rect(120, 10, Screen.width-120, 40), "Hash Key: "+myhash);
        }
        if (queryresult !=null)
        {
            GUI.Label(new Rect(120, 50, Screen.width-120, Screen.height-50), queryresult);
        }
    }

    string queryresult =null;
    void queryCB(bool ret, object data)
    {
        string tmpstr ="";
        List<object> OBJARR =(List<object>)data;
        for (int i=0;i<OBJARR.Count;++i)
        {
            
            Dictionary<string, object> items =(Dictionary<string, object>)OBJARR[i];
            foreach(KeyValuePair<string, object> item in items)
            {
                tmpstr +=(item.Key+":"+item.Value+"\n");
            }
        }

        queryresult =tmpstr;
    }
}
將 Script 內的 FB_APP_ID 換成當初建立 FB APP 時,Facebook 給予的 App ID。此外,"com.macaronics.fbonandroid" 是本範例所設定之 Bundle Identifier 。另外,呼叫 invokeQuery 函數的第一個參數是 FQL 字串,第二個參數是作為回傳結果用的 Callback Function,回傳的結果字串屬於 JSON 格式,範例把字串內容解析並顯示出來。

13. 設定 Untiy 載入 Scene 之後,各 Script 的執行順序,將 fbWrapper 設定早於預設的時間 (Edit --> Project Settings --> Script Execution Order)。

14. 當一切設定就緒之後 (記得設定 Player Settings 之 Other Settings 的 Bundle Identifier),嘗試 Build apk 檔案,Build 完成之後,在 Unity 專案資料夾下會多出一個 Temp 資料夾,在路徑 Temp/StagingArea/gen/com/macaronics/fbonandroid/ 找到檔案 R.java。

打開此檔案,將 package com.macaronics.fbonandroid; 改為 package com.facebook.android; 之後另存成 R.java 在別的資料夾。

15. 編譯 R.java :
javac R.java -d .
打包成為 jar 檔案 :
jar cvfM facebooksdk_R.jar com/
將檔案放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下。

16. 重新編譯 apk 檔案。

此範例的原始碼可以從這裡 ( http://github.com/phardera/unity3d_fb_android.git ) 取得。

2013-03-25

Unity 3D + Google Play In-app Billing (IAB)


底下是 Unity 介接 Google Play In-app Billing (IAB) 金流的步驟,使用的環境是 OSX,IAB 是 V3 版 :

1. 編譯 IInAppBillingService.java

a. 按照 GooglePlay 官方網站的指示 (http://developer.android.com/training/in-app-billing/preparing-iab-app.html) 產生此檔案,其實這是一個自動產生的檔案,詳細方法可以參考連結網頁敘述。此外值得注意的是,程式碼也可以由 git 從此處下載 :

git clone https://code.google.com/p/marketbilling/

(附註 : 也許現在已經更新了,但文章撰寫的當下,若是從 Android SDK Manager 勾選 Google Play Billing Library 下載的版本有 Bug,所以才由 git 下載。 ref :
http://stackoverflow.com/questions/14397343/google-play-in-app-billing-version-3-crash-on-item-already-owned-and-missing )

b. 編譯 IInAppBillingService.java,指令如下 (此處假設 android sdk 之資料夾路徑在 /Users/macaronics/android-sdks/) :

javac ./IInAppBillingService.java -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar -d .

其中 -cp 指示編譯時參考之 library (android-10, Android 2.3.3)。編譯完成後輸入 :

jar cvfM ./iiabs.jar com/

產生的 iiabs.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下( 此範例將檔案置於 /Users/macaronics/UnityIabProj/Assets/Plugins/Android/) 。
注意要刪除資料夾 com,以免後續執行 jar 時,打包到舊的 com package。

2. 編譯 class IabHelper

a. 此類別由九個檔案組成,方便用來介接 Google IAB,九個 java 檔案應該位於前步驟下載之程式碼的 marketbilling/v3/src/com/example/android/trivialdrivesample/util 底下。

b. 逐一修改九個檔案裡的 "package com.example.android.trivialdrivesample.util" 改為你的 package name,例如  package com.macaronics.iab.util。

c. 建立欲編譯的檔案的 list,在 Terminal 底下的話直接輸入 cat > sources 然後依序輸入九個檔案檔名後按 control+d 儲存離開。

在 terminal 底下利用 cat 建立 sources file

d. 編譯 class IabHelper,輸入指令如下 :

javac @sources -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar:/Users/macaronics/UnityIabProj/Assets/Plugins/Android/iiabs.jar -d .

其中 @sources 表示參考檔案 sources 裡的路徑。此外,引入的 library 除了 android-10/android.jar (Android 2.3.3) 外,還包含前一步驟所編譯的檔案 iiabs.jar。完成後接著輸入 :

jar cvfM ./iabhelper.jar com/

產生的 iabhelper.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下( 此範例將檔案置於 /Users/macaronics/UnityIabProj/Assets/Plugins/Android/) 。


3. 處理 onActivityResult 的結果訊息

a. 為了讓 IabHelper 能在 Activity 結束時處理 Activity 的結果,這裡我們需要自己撰寫繼承 UnityPlayerActivity 的類別來將結果 Relay 給 IabHelper。底下是範例的程式碼 :
package com.macaronics.iab;

import com.unity3d.player.UnityPlayerActivity;

import android.os.Bundle;
import android.util.Log;
import android.content.Intent;

public class overrideActivity extends UnityPlayerActivity {
    public interface cbEvent{
        public boolean cbEvent(int requestCode, int resultCode, Intent data);
    }

    protected cbEvent ie;
    static protected overrideActivity inst;
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        inst =this;
        
        // print debug message to logcat
        Log.d("overrideActivity", "onCreate called!");
    }

    @Override
    public void onDestroy(){
        super.onDestroy();
        inst =null;
        Log.d("overrideActivity", "onDestroy called!");
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data){
        Log.d("overrideActivity", "onActivityResult called!");
        
        boolean ret =false;
        if (ie !=null){
            try{
                ret =ie.cbEvent(requestCode, resultCode, data);
            }
            catch(Exception e){
                ret =false;
            }
        }

        if (ret ==false){
            super.onActivityResult(requestCode, resultCode, data);
        }
    }
    
    static public void registerOnActivityResultCBFunc(final cbEvent pcbfunc){
        if (inst !=null)
            inst.ie =pcbfunc;
    }
} 
其中函數 registerOnActivityResultCBFunc 只能記憶單一個 call back function,讀者可以把功能擴充成使用佇列來記憶更多的 call back function。

b. 同樣的編譯這個 class :

javac ./overrideActivity.java -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar:/Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar -d .

其中引入的 library 有 android-10/android.jar (Android 2.3.3),及 Unity 的 classes.jar。然後輸入 :

jar cvfM ./overrideActivity.jar com/

產生的 overrideActivity.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下( 此範例將檔案置於 /Users/macaronics/UnityIabProj/Assets/Plugins/Android/) 。


4. 撰寫 iabWrapper.java

a. 主要用於介接 IabHelper 及 Unity ,提供基本的下單功能。程式碼分別敘述如下 :
package com.macaronics.iab;

import com.unity3d.player.UnityPlayer;

import android.app.Activity;
import android.util.Log;
import android.os.Bundle;
import android.os.Looper;
import android.content.Intent;

import java.util.*;

import com.macaronics.iab.util.*;
import com.macaronics.iab.overrideActivity;
值得注意的是這裡引入 UnityPlayer,步驟2編譯的 IabHelper (com.macaronics.iab.util),及步驟3編譯的 overrideActivity (com.macaronics.iab.overrideActivity)。

底下是 class iabWrapper 的私有變數部分 :
public class iabWrapper{
    private Activity mActivity;
    private IabHelper mHelper;
    private String mEventHandler;  
    ...
其中 mEventHandler 是回呼 Unity 時,接收訊息的 GameObject 名稱。此 class 有一個 Constructor 函數,三個成員函數及兩個給 IabHelper 回呼的函數,分別敘述如下 :

Constructor 函數在呼叫時給予 GooglePlay API PublicKey (由 GooglePlay 提供),以及回呼 Unity 時,接收訊息的 GameObject 之名稱 :
public iabWrapper(String base64EncodedPublicKey, String strEventHandler){
    mActivity =UnityPlayer.currentActivity;
    mEventHandler =strEventHandler;

    if (mHelper !=null){
        dispose();
    }

    mHelper =new IabHelper(mActivity, base64EncodedPublicKey);
    mHelper.enableDebugLogging(true);

    mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
        public void onIabSetupFinished(IabResult result){
            if (!result.isSuccess()){
                //回呼 Unity GameObject 之函數 "msgReceiver", 並傳送字串訊息 (JSON 格式)
                UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"code\":\"1\",\"ret\":\"false\",\"desc\":\""+result.toString()+"\"}");
                dispose();
                return;
            }
            
            //回呼 Unity GameObject 之函數 "msgReceiver", 並傳送字串訊息 (JSON 格式)
            UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"code\":\"1\",\"ret\":\"true\",\"desc\":\""+result.toString()+"\"}");

            //register mHelper
            //向 overrideActivity 註冊 onActivityResult 回呼函數,並將資料 Relay 給 mHelper
            overrideActivity.registerOnActivityResultCBFunc(
                new overrideActivity.cbEvent(){
                    public boolean cbEvent(int requestCode, int resultCode, Intent data)
                    {
                            
                        if (mHelper.handleActivityResult(requestCode, resultCode, data)){
                            return true;
                        }
                        else{
                            return false;
                        }
                    }
                }
            );
        }
    });
}
其中的 UnitySendMessage 用來呼叫 Unity GameObject 之函數 "msgReceiver", 並傳送字串訊息 (JSON 格式)。此外值得注意的是此 Constructor 函數還向 overrideActivity 註冊用來接收 onActivityResult 訊息的回呼函數。底下的dispose 函數主要用於釋放 mHelper :
public void dispose()
{
    if (mHelper !=null)
    {
        mHelper.dispose();
    }
    mHelper =null;
}
purchase 函數會啟動購買商品的介面,其中第一項參數是 Product SKU (在 GooglePlay 設定的產品 SKU),第二項是回呼 onActivityResult 函數時用來分辨購買動作的 reqCode,第三項是用來確認此筆講買是否合法的 payloadString (購買成功之後, GooglePlay 回覆的資訊裡會包含此字串。)。
public void purchase(String strSKU, String reqCode, String payloadString)
{
    int intVal =Integer.parseInt(reqCode);
    if (mHelper !=null)
        mHelper.launchPurchaseFlow(mActivity, strSKU, intVal, mPurchaseFinishedListener, payloadString);
}
底下是完成購買之後回呼的函數 (onIabPurchaseFinished),這裡把接收的資訊以 JSON 格式轉送給 Unity :
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener =new IabHelper.OnIabPurchaseFinishedListener() {
    public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
        if (result.isFailure()){
            UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"code\":\"2\",\"ret\":\"false\",\"desc\":\"\",\"sign\":\"\"}");
            return;
        }
  
        boolean ret =false;
        String result_json ="";
        String result_sign ="";
        if (purchase !=null){
            ret =true;
            result_json =purchase.getOriginalJson().replace('\"', '\'');
            result_sign =purchase.getSignature();
        }

        UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"code\":\"2\",\"ret\":\""+ret+"\",\"desc\":\""+result_json+"\",\"sign\":\""+result_sign+"\"}");
    }
};
產品在購買 (Purchase) 之後若再次購買則會失敗,在 Console 裡可以看到 "Item already owned" 訊息。若產品的類型屬於消耗性的產品,則需要執行 consume 指令才可再次購買 :
public void consume(String itemType, String jsonPurchaseInfo, String signature)
{
    String transedJSON =jsonPurchaseInfo.replace('\'', '\"');
    if (mHelper ==null)
        return;

    Purchase pp =null;
    try{
        pp =new Purchase(itemType, transedJSON, signature);
    }
    catch(Exception e){
        pp=null;
    }

    if (pp !=null){
        final Purchase currpp =pp;
        mActivity.runOnUiThread(new Runnable(){
            public void run(){
                mHelper.consumeAsync(currpp, mConsumeFinishedListener);
            }
        });
    }
}
其中參數 itemType 在此範例為 "inapp",jsonPurchaseInfo 及 signature 是在 purchase 完成後, IabHelper 回呼 onIabPurchaseFinished 所給的資料。consume 完成之後回呼的函數如下 :
IabHelper.OnConsumeFinishedListener mConsumeFinishedListener =new IabHelper.OnConsumeFinishedListener() {
    public void onConsumeFinished(Purchase purchase, IabResult result) {
        if (result.isSuccess()){
            Log.d("iabWrapper", "Consumption successful. Provisioning");
            UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"code\":\"3\",\"ret\":\"true\",\"desc\":\""+purchase.getOriginalJson().replace('\"', '\'')+"\",\"sign\":\""+purchase.getSignature()+"\"}");
        }
        else{
            UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver", "{\"code\":\"3\",\"ret\":\"false\",\"desc\":\"\",\"sign\":\"\"}");
        }
    }
};

b. 編譯 iabWrapper.java

javac ./iabWrapper.java -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar:/Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar:/Users/macaronics/UnityIabProj/Assets/Plugins/Android/iabhelper.jar:/Users/macaronics/UnityIabProj/Assets/Plugins/Android/overrideActivity.jar -d .

其中引入的 library 有 android-10/android.jar (Android 2.3.3), Unity 的 classes.jar,先前編譯的 iabhelper.jar 及 overrideActivity.jar。然後輸入 :

jar cvfM ./iabWrapper.jar com/

產生的 iabWrapper.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下( 此範例將檔案置於 /Users/macaronics/UnityIabProj/Assets/Plugins/Android/) 。


5. 設定 Permission ( AndroidManifest.xml )

若要執行 GooglePlay Billing 功能則要設定 AndroidManifest.xml,將底下的 AndroidManifest.xml 置於 /Users/macaronics/UnityIabProj/Assets/Plugins/Android 資料夾。
<?xml version="1.0" encoding="utf-8"?>
<manifest android:versionCode="1" android:versionName="1.0" 
          android:installLocation="preferExternal" 
          package="com.macaronics.iab" 
          xmlns:android="http://schemas.android.com/apk/res/android">
  <supports-screens android:anyDensity="true" android:smallScreens="true" 
                    android:normalScreens="true" android:largeScreens="true" 
                    android:xlargeScreens="true" />
  <application android:label="@string/app_name" 
               android:icon="@drawable/app_icon" android:debuggable="false">
    <activity android:label="@string/app_name" 
              android:name="com.macaronics.iab.overrideActivity"
              android:screenOrientation="portrait" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"              
              >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity android:label="@string/app_name" 
              android:name="com.unity3d.player.UnityPlayerActivity" 
              android:screenOrientation="portrait" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
              />
    <activity android:label="@string/app_name" 
              android:name="com.unity3d.player.UnityPlayerNativeActivity" 
              android:screenOrientation="portrait" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
              >
      <meta-data android:name="android.app.lib_name" android:value="unity" />
      <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" 
                 android:value="false" />
    </activity>
    <activity android:label="@string/app_name" 
              android:name="com.unity3d.player.VideoPlayer" 
              android:screenOrientation="behind" 
              android:configChanges=
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale" 
              />
  </application>
  <uses-feature android:glEsVersion="0x20000" />
 
  <uses-permission android:name="com.android.vending.BILLING" />
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

</manifest>

注意其中的 package="com.macaronics.iab" 要改成你自己的 package 名稱,與 Unity 裡的 Bundle Identifier 相同。此外,這裡還設定了主要啟動的 Activity 為 overrideActivity,以及 uses-permission。

到目前為止 /Users/macaronics/UnityIabProj/Assets/Plugins/Android 資料夾底下應該會有這些檔案 (iabhelper.jar, iabWrapper.jar, iiabs.jar, overrideActivity.jar, AndroidManifest.xml ) :

到目前為止應該要有的檔案

6. Unity 部分的 iabWrapper.cs

利用 AndroidJNI 介接 Java 程式 (注意為了接收 java 回傳的字串訊息,此 script (component) 必須加入到名為 iabWrapper 的 GameObject 裡)。 :
using UnityEngine;

using System.Collections;
using System.Collections.Generic;

public class iabWrapper : MonoBehaviour 
{
    public delegate void cbFunc(object[] retarr);
    cbFunc iabSetupCB =null;
    cbFunc iabPurchaseCB =null;
    cbFunc iabConsumeCB =null;

    AndroidJavaObject mIABHelperObj =null;
    static iabWrapper g_inst =null;

    void Start(){
        g_inst =this;
    }

    static public void init(string base64EncodedPublicKey, cbFunc tmpIabSetupCBFunc){
        if (g_inst ==null)
            return;

        g_inst.iabSetupCB =tmpIabSetupCBFunc;

        dispose();
        g_inst.mIABHelperObj =new AndroidJavaObject("com.macaronics.iab.iabWrapper", new object[2]{base64EncodedPublicKey, "iabWrapper"});
    }

    static public void dispose(){
        if (g_inst ==null)
            return;

        if (g_inst.mIABHelperObj !=null){
            g_inst.mIABHelperObj.Call("dispose");
            g_inst.mIABHelperObj.Dispose();
            g_inst.mIABHelperObj =null;
        }
    }

        ...

其中 init 負責呼叫 iabWrapper.java 之建構函數並建立該物件,dispose 則是負責刪除釋放 iabWrapper.java 物件。底下是主要的兩個功能,purchase 和 consume :
static public void purchase(string strSKU, int reqCode, string payload, cbFunc tmpIabPurchaseCBFunc){
    if (g_inst ==null)
        return;

    g_inst.iabPurchaseCB =tmpIabPurchaseCBFunc;

    if (g_inst.mIABHelperObj !=null){
        g_inst.mIABHelperObj.Call("purchase", new object[3]{strSKU, reqCode.ToString(), payload});
    }
}

static public void consume_inapp(string strPurchaseJsonInfo, string strSignature, cbFunc tmpIabConsumeCBFunc){
    if (g_inst ==null)
        return;

    g_inst.iabConsumeCB =tmpIabConsumeCBFunc;

    if (g_inst.mIABHelperObj !=null){
        g_inst.mIABHelperObj.Call("consume", new object[3]{"inapp", strPurchaseJsonInfo, strSignature});
    }
}
其中先將 callback 函數記錄起來,然後呼叫其對應的 java function。最後一部分是 msgReceiver,負責接收 java 回呼的結果並將資訊回呼對應的 callback function (先前呼叫 purchase 或是 consume 所給予的 callback function)。
void msgReceiver(string msg){
    if (g_inst ==null)
        return;
 
    //parse json
    Dictionary<string, object> cache =(Dictionary<string, object>)MiniJSON.Json.Deserialize(msg);

    //dispatch msg
    if (cache.ContainsKey("code")==true){
        int val =0;
        int.TryParse((string)cache["code"], out val);
        switch(val){
            case 0:{
                //unknown
                Debug.Log("Unity-iabWrappe :cannot parse cache[code]");

            }
            break;

            case 1:{
                //OnIabSetupFinishedListener
                if (cache.ContainsKey("ret")==true){
                    string retval =(string)cache["ret"];
                    if (retval =="true"){
                        //可使用
                        if (iabSetupCB !=null){
                            iabSetupCB( new object[1]{true} );
                        }

                    }
                    else if (retval =="false"){
                        //不可使用
                        if (iabSetupCB !=null)
                        {
                            iabSetupCB( new object[1]{false} );
                        }
                    }else{
                        Debug.Log("Unity-iabWrapper :cannot parse cache[ret], code=1");
                    }
                }
            }
            break;

            case 2:{
                //onIabPurchaseFinished
                if (cache.ContainsKey("ret")==true){
                    string retval =(string)cache["ret"];
                    if (retval =="true"){
                        //可使用
                        if (iabPurchaseCB !=null){
                            iabPurchaseCB( new object[3]{true, (string)cache["desc"], (string)cache["sign"]} );
                        }

                    }
                    else if (retval =="false"){
                        //不可使用
                        if (iabPurchaseCB !=null)
                        {
                            iabPurchaseCB( new object[3]{false, "", ""} );
                        }

                    }
                    else{
                        Debug.Log("Unity-iabWrapper  :cannot parse cache[ret], code=2");
                    }
                }
            }
            break;

            case 3:{
                //OnConsumeFinishedListener
                if (cache.ContainsKey("ret")==true){
                    string retval =(string)cache["ret"];
                    if (retval =="true"){
                        //可使用
                        if (iabConsumeCB !=null)
                        {
                            iabConsumeCB( new object[3]{true, (string)cache["desc"], (string)cache["sign"]} );
                        }

                    }else if (retval =="false"){
                        //不可使用
                        if (iabConsumeCB !=null)
                        {
                            iabConsumeCB( new object[3]{false, "", ""} );
                        }

                    }
                    else{
                        Debug.Log("Unity-iabWrapper :cannot parse cache[ret], code=3");

                    }
                }
            }
            break;
        }
    }
}
程式碼先將 JSON 字串轉為 object 物件並依照物件種類做對應的處理,其中 purchase 的 callback (case 2),回傳的參數依序為 : 1. 是否成功,2. purchase 結果字串以及 3. purchase 結果之 signature (可作為驗証之用,參考這篇)。


7. 實作 IAB APP

建立一個 script 名為 main,並將此 script 加入至 Camera。這裡實作一個 purchase 功能,並在 purchase 完成之後立刻執行 consume。
using UnityEngine;
using UnityEngine;
using System.Collections;
 
public class main : MonoBehaviour {
 
    // Use this for initialization
    void Start () {
        iabWrapper.init(
            "PUBLIC_KEY",
            delegate(object[] ret){
                if (true ==(bool)ret[0]){
                    Debug.Log("iab successfully initialized");
                }
                else{
                    Debug.Log("failed to initialize iab");
                }
            });
    }
 
    void OnGUI(){
        if (GUI.Button(new Rect(0, 0, 100, 100), "purchase")){
            iabWrapper.purchase("PRODUCT_SKU", 10001, "PRODUCT_SKU_AND_USER_ID_AND_DATE",
                delegate(object[] ret){
                    if (false ==(bool)ret[0]){
                        Debug.Log("purchase cancelled");
                    }
                    else{
                        string purchaseinfo =(string)ret[1];
                        string signature =(string)ret[2];
                        iabWrapper.consume_inapp(purchaseinfo, signature, 
                            delegate(object[] ret2){
                                if (false ==(bool)ret2[0])
                                {
                                    Debug.Log("failed to consume product");
                                }
                            });
                    }
                });
        }
    }
 
    void OnApplicationQuit(){
        iabWrapper.dispose();
    }
}
其中的 PUBLIC_KEY,更換成你的 Public Key,代入 purchase 函數的參數更改成你的參數。範例原始碼可以從這裡取得 : http://github.com/phardera/unity3d_googleplay_iab.git

2013-03-17

NodeJS 驗證 Google Play In-app Billing Signature

使用 IabHelper 可以方便介接 Google Play 的金流系統 (In-App billing, IAB) ,IabHelper 提供一個用來驗證下單結果 (使用者下單之後,由 GooglePlay IAB 回傳的結果 ) 的函數(Security.verifyPurchase),它主要用來確定資料是否有被竄改或是不完整。
若 App 的架構具有後端伺服器,則 IAB 回傳的結果應該由伺服器端來做驗證,底下是以 NodeJS 為例子的驗證程序 :
var crypto =require('crypto');
var verifySignature =function(publicKey, purchaseData, signature)
{
    var genPublicKey =function(key)
    {
        var chunkSize, chunks, str;
        str = key;
        chunks = [];
        chunkSize = 64;
        while (str) {
            if (str.length < chunkSize) {
                chunks.push(str);
                break;
            } else {
                chunks.push(str.substr(0, chunkSize));
                str = str.substr(chunkSize);
            }
        }
        str = chunks.join("\n");
        str = '-----BEGIN PUBLIC KEY-----\n' + str + '\n-----END PUBLIC KEY-----';
        return str;
    }
    
    return crypto.createVerify('RSA-SHA1').update(purchaseData).verify(genPublicKey(publicKey), signature, 'base64');
}


回傳若為 true 則資料正確。

參考來源 : https://github.com/nothing2lose/node-InAppBilling

2013-02-18

C# AES-256-CBC 加解密

使用到的名稱空間 :
using System;
using System.Text;
using System.Security.Cryptography;
using System.IO;

程式碼如下,將字串 "text to be encrypted" 加密後解密看結果是否相同 :
RijndaelManaged rijalg = new RijndaelManaged();
 
//-----------------
//設定 cipher 格式 AES-256-CBC
rijalg.BlockSize = 128;
rijalg.KeySize = 256;
rijalg.FeedbackSize = 128;
rijalg.Padding = PaddingMode.PKCS7;
rijalg.Mode = CipherMode.CBC;
 
rijalg.Key = (new SHA256Managed()).ComputeHash(Encoding.ASCII.GetBytes("IHazSekretKey"));
rijalg.IV = System.Text.Encoding.ASCII.GetBytes("1234567890123456");
 
//-----------------
//加密
ICryptoTransform encryptor =rijalg.CreateEncryptor(rijalg.Key, rijalg.IV);
 
byte[] encrypted;
// Create the streams used for encryption.
using (MemoryStream msEncrypt = new MemoryStream())
{
    using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
    {
        using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
        {
            //Write all data to the stream.
            swEncrypt.Write("text to be encrypted");
        }
        encrypted = msEncrypt.ToArray();
    }
}
 
//-----------------
//加密後的 base64 字串 :
//eiLbdhFSFrDqvUJmjbUgwD8REjBRoRWWwHHImmMLNZA=
System.Console.WriteLine(Convert.ToBase64String(encrypted));
 
//-----------------
//解密
ICryptoTransform decryptor = rijalg.CreateDecryptor(rijalg.Key, rijalg.IV);
 
string plaintext;
// Create the streams used for decryption. 
using (MemoryStream msDecrypt = new MemoryStream(encrypted))
{
    using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
    {
        using (StreamReader srDecrypt = new StreamReader(csDecrypt))
        {
 
            // Read the decrypted bytes from the decrypting stream 
            // and place them in a string.
            plaintext = srDecrypt.ReadToEnd();
        }
    }
}
 
//-----------------
//最後印出字串 "text to be encrypted"
System.Console.WriteLine(plaintext);

iOS App 在背景狀態下持續更新 GPS 回報之位置

一般 App 在進入 Background 背景狀態之後就會被暫停執行,但做以下設定之後 App 可在此狀態持續更新 GPS 回報的使用者座標位置 :

1. 在 App 的 Info.plist 檔案的 Required device capabilities 項目底下加入 location-services 及 gps
2. 同樣在該檔案的 Required background modes 底下加入 App registers for location updates

設定 Info.plist 檔案之 Required device capabilities 及 Required background modes

接下來的範例是開一個 empty iOS Application project 然後加入一些程式碼來測試 :

1. 為 project 加入 CoreLocation.framework

加入 CoreLocation.framework 至 Linked Frameworks and framework

2. 在 AppDelegate.h 檔案增加 / 修改程式碼  :
#import <CoreLocation/CoreLocation.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate, CLLocationManagerDelegate>
3. 在 AppDelegate.cpp 檔案增加程式碼 :
CLLocationManager* locationManager =nil;
- (void)startStandardUpdates
{
    if (nil == locationManager)
        locationManager = [[CLLocationManager alloc] init];
    
    locationManager.delegate = self;
    locationManager.desiredAccuracy = kCLLocationAccuracyBest;
    locationManager.distanceFilter = kCLDistanceFilterNone;
    [locationManager startUpdatingLocation];
}

-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    NSLog(@"update location err-\n%@", error);
}

// Delegate method from the CLLocationManagerDelegate protocol.
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    CLLocation* location = [locations lastObject];
    NSString* pp =[NSString stringWithFormat:@"http://192.168.11.6:10001/?lat=%.3f&lon=%.3f", location.coordinate.latitude, location.coordinate.longitude];
    
    NSURL* response =[NSURL URLWithString:pp];
    NSString* response_str =[NSString stringWithContentsOfURL:response encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"%@, response of report=%@", pp, response_str);
}

函數 didUpdateLocations 在每次更新 GPS 回報的位置時使用 NSURL 以 http 方式向伺服器 192.168.11.6 以 GET 方式回報位置 (lat, lon)。

4. 在 AppDelegate.cpp 檔案之函數 didFinishLaunchingWithOptions 內加入程式碼 :
[self startStandardUpdates];
在程式啟動時呼叫 startStandardUpdates 開始取得 GPS 回報的使用者位置。

2013-01-30

NodeJS 簡單的 HTTP Server

底下的範例在啟動之後自動指定同一資料夾為 HTTP Server 之根目錄:
var http = require('http'),
    url = require('url'),
    path = require('path'),
    fs = require('fs');
 
var mimeTypes = {
    "html": "text/html",
    "jpeg": "image/jpeg",
    "jpg": "image/jpeg",
    "png": "image/png",
    "js": "text/javascript",
    "css": "text/css"};
 
http.createServer(function(req, res) {
    var uri = url.parse(req.url).pathname;
    console.log(uri);
    var empty =function(){
        res.writeHead(200, {'Content-Type': 'text/plain'});     
        res.end();
    };
    if (uri ==='/'){
        return empty();
    };
    var filename = path.join(process.cwd(), uri);
    fs.exists(filename, function(exists){
        if(!exists || fs.lstatSync(filename).isDirectory()===true){
            return empty();
        }
        res.writeHead(200, {
            'Content-Type':mimeTypes[path.extname(filename).split(".")[1]]
        });
        fs.createReadStream(filename).pipe(res);
    }); //end fs.exists
}).listen(8080);
在同一資料夾裡擺一個檔案 (index.html),啟動之後利用網址 http://localhost:8080/index.html 即可瀏覽該檔案。

2012-12-17

刪除 Open With 選單裡重覆的項目


OSX 用久了會發現當要用右鍵開啟某些檔案時,在 Open With 選單裡有重覆的項目,此時可在 Terminal 裡輸入 :

/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -domain local\ -domain system -domain user

待執行完成之後 Relaunch Finder (control+option+滑鼠右鍵 點選 Finder ),即可刪除重覆的項目。

ref : http://itpixie.com/2011/05/fix-duplicate-old-items-open-with-list/#.UM3vUqWVgcQ