2014-10-19

Android DexClassLoader 續

之前介紹的方法讓我們的程式可以在執行時期動態載入其它的程式碼,後來發現 Facebook 的 Buck 當中也有類似的功能 ( Exopackage ),但它實作的方法和之前介紹的方法不同,底下敘述使用 Buck 的方法,並套用在前一篇範例上的流程 :

1. 製作 dex 檔案 :
與前一篇介紹的方法相同

2. 程式執行時期下載 dex 檔案  :
建立一個 com.example.dexclassloader 之 Android Application Project 並將主要 Activity (MainActivity.java ) 替換如下 :
package com.example.dexclassloader;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.Menu;
 
public class MainActivity extends Activity {
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
   
  Thread works =new Thread(){
   @Override
   public void run(){
    //download and save the jar
    downloadDexFile("http://localhost/PayloadActivity.dex.jar", "PayloadActivity.dex.jar");
    
    //install dex jar
    List<File> dexJars =new ArrayList<File>();
    File libFile =new File(getFilesDir()+"/PayloadActivity.dex.jar");
    dexJars.add(libFile);
    SystemClassLoaderAdder.installDexJars(getClassLoader(), getDir("outdex", Context.MODE_PRIVATE), dexJars);
     
    //load the jar
    loadAndInvokeMethod();
   }
  };
  works.start();
 }
 
 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }
  
 private void downloadDexFile(String url, String filename){
     try {
 
         URL u = new URL(url);
         HttpURLConnection c = (HttpURLConnection) u.openConnection();
         c.setRequestMethod("GET");
         c.setDoOutput(true);
         c.connect();
          
         FileOutputStream f =openFileOutput(filename, MODE_PRIVATE);
         InputStream in = c.getInputStream();
 
         byte[] buffer = new byte[1024];
         int len1 = 0;
          
         while ((len1 = in.read(buffer)) > 0) {
             f.write(buffer, 0, len1);
         }
         f.close();
          
     } catch (Exception e) {
         //error dialog
         Log.d("mainactivity", "error -"+e.toString());
 
     }
 }
  
 private void loadAndInvokeMethod(){
  try{  
         //LOAD CLASS
         Class<?> payloadClass =this.getClassLoader().loadClass("com.example.PayloadActivity");
          
         //LOAD METHOD
         Method starterFunc =payloadClass.getMethod("starter", Activity.class);
          
         //INVOKE METHOD
         starterFunc.invoke(null, this);
          
  }catch(Exception e){
    
  }
 }
 
}
觀察得知原本 overrideClassLoader 函數的部分改為呼叫 SystemClassLoaderAdder.installDexJars 函數,此外呼叫載入的 dex 裡的類別函數方法也變得簡單。前一篇的作法是建立一個自己的 ClassLoader 去替換原本的 ClassLoader,這篇的作法是直接去修改現有的 ClassLoader 因此也比上一篇的方法少了 MyClassLoader.java 檔案。

(ps. 程式碼裡呼叫的 installDexJars 函數位於檔案 SystemClassLoaderAdder.java,可以在這裡取得)

3. MyApplication.java

完成步驟1, 2 之後程式就可以運作,但不算真正的完成。有時候啟動程式仍然會得到 java.lang.ClassNotFoundException 的錯誤訊息。

為何說是 "有時候" ,因為這跟使用者操作方式的流程有關,假若現在使用者在程式裡啟動了 Acitivty ,而這個 Activity 是屬於 dex 檔案後來載入的程式碼,啟動後在 Activity 的介面操作到一半切換到其它的 App,此時有可能因為系統記憶體不足,釋放原本在背景執行的 App 的記憶體,之後使用者欲切換到原來的 App,系統會自動重新啟動 App 並呼叫上次啟動的 Activity,此時的 ClassLoader 是系統建立的 ClassLoader 不是我們更改過的 ClassLoader 因此找不到 dex 檔案而發生 Class Not Found Exception。

幸好在系統啟動 Activity 之前我們還有機會讓系統執行我們的程式。那就是 Application 的 attachBaseContext 函數。

底下是新增加的程式 MyApplication.java :
package com.example.dexclassloader;

import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
import java.util.ArrayList;
import java.util.List;
import java.io.File;

public class MyApplication extends Application
{
  public void installPayloadDexFile(){

    String libPath =getFilesDir()+"/PayloadActivity.dex.jar";
    File libFile =new File(libPath);

    if (libFile.exists()==false)
      return;

    File optimizedDexOutputPath = getDir("outdex", Context.MODE_PRIVATE);
    List<File> dexJars = new ArrayList<File>();
    dexJars.add(libFile);
    SystemClassLoaderAdder.installDexJars(getClassLoader(), optimizedDexOutputPath, dexJars);

  }

  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
  }

  @Override
  protected final void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    installPayloadDexFile();
    
  }

  @Override
  public final void onCreate() {
    super.onCreate();
    installPayloadDexFile();
  }

  @Override
  public final void onTerminate() {
    super.onTerminate();

  }

  @Override
  public final void onLowMemory() {
    super.onLowMemory();

  }

  @Override
  public final void onTrimMemory(int level) {
    super.onTrimMemory(level);

  }

}

觀察可知程式在啟動 Activity 之前首先呼叫 MyApplication.attachBaseContext 然後檢查是否存在檔案 PayloadActivity.dex.jar ,若有則載入 dex 並修改目前的 ClassLoader。

4. AndroidManifest.xml

同前一篇介紹的,除了要預先定義 Activity 到 AndroidManifest.xml 之外,若要讓 Class MyApplication 生效,還須定義 MyApplication 在 application 標籤內 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.dexclassloader"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />
 
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" 
        android:name="com.example.dexclassloader.MyApplication" >
        <activity
            android:name="com.example.dexclassloader.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.example.PayloadActivity" android:label="@string/app_name" />
    </application>
     
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 
</manifest>

ref:http://facebook.github.io/buck/article/exopackage.html
ref:http://developer.android.com/reference/android/app/Application.html
ref:http://stackoverflow.com/questions/9873669/how-do-i-catch-content-provider-initialize


2014-10-16

Android : 建立並安裝 自我簽署憑証 ( Self-Signed Certificate authority )

在安全性的需求之下我們使用 https 代替 http 連線,而 server 端開啟 https 服務需要設定相關的憑証,這些憑証要向具有公信力的機購購買,但在開發時期我們可以自己產生,根據這裡的資料,製作 X.509 version 3 的 self-signed CA certificates 可以避免大多數行動裝置無法安裝的問題,底下步驟是敘述製作以及在 Android 上安裝 self-signed CA certificates 的流程。

A. 製作 self-signed CA certificates

1. 建立一個檔案,例如 openssl.cnf,其內容如下 (openssl.cnf 檔案的重點在於 basicConstraints = CA:true 以及 alt_names) :
[req]
  distinguished_name = req_distinguished_name
  req_extensions = v3_req

[req_distinguished_name]
  countryName = Country Name (2 letter code)
  countryName_default = US
  localityName = Locality Name (eg, city)
  organizationalUnitName = Organizational Unit Name (eg, section)
  commonName = Common Name (eg, YOUR name)
  commonName_max = 64
  emailAddress = Email Address
  emailAddress_max = 40

[v3_req] 
  basicConstraints = CA:true
  keyUsage = keyEncipherment, dataEncipherment
  extendedKeyUsage = serverAuth
  subjectAltName = @alt_names

[alt_names]
  DNS.1   = *.mydomain1.com.tw
  DNS.2   = *.mydomain2.com.tw
  IP.1    = 192.168.11.1
其中 alt_names 裡的 DNS 以及 IP 就輸入這個憑証欲綁定的 FQDN 或 IP。

2. 使用底下的命令建立 key 跟 certificate :
openssl req -x509 -nodes -days 365 -newkey rsa:4096 -keyout ./privatekey.key -out ./certificate.crt -extensions v3_req -config ./openssl.cnf
建立的 privatekey.key 跟 certificate.crt 是給開啟 https 服務的伺服器使用。

B. 將 certificate.crt 轉換成 DER 格式 (供 Android 安裝使用) 

openssl x509 -in ./certificate.crt -outform der -out ./certificate_der.crt
可將 certificate_der.crt 擺在網路上,讓 Android 裝置的瀏覽器將它下載到手機中,直接點擊下載的檔案就可以開始安裝憑証。
(或者也可以擺在自己的 http server 上,將 crt 的 MIME type 設定成 application/x-x509-ca-cert 讓瀏覽器直接安裝憑証,或是將 crt 的 MIME type 設定成 application/octet-stream 讓瀏覽器把它視為檔案下載。)

ref:https://developer.android.com/training/articles/security-ssl.html
ref:http://www-01.ibm.com/support/knowledgecenter/#!/SSZH4A_6.0.0/com.ibm.worklight.help.doc/admin/c_ssl_config.html
ref:http://www.xinotes.net/notes/note/1094/
ref:http://stackoverflow.com/questions/7229361/how-to-force-mime-type-of-file-download
ref:http://davdroid.bitfire.at/faq/entry/importing-a-certificate
ref:http://www.akadia.com/services/ssh_test_certificate.html

2014-06-25

Android webview 處理 Line it intent

最近有個需求是要在 webview 上處理 LINE it 按鈕的連結,這裡做個筆記 :

1. 使用 shouldOverrideUrlLoading 攔截轉跳的 url 連結。
2. 攔截 intent:// 開頭的 url
3. 此時應該會有一個 intent 看起來像這樣 :

intent://msg/text/DUMMY_MESSAGE#Intent;scheme=line;action=android.intent.action.VIEW;category=android.intent.category.BROWSABLE;package=jp.naver.line.android;end

4. 兩行搞定

Intent iuri =Intent.parseUri(url, 0);
startActivity(iuri);

2014-03-10

Android DexClassLoader


本文述敘 DexClassLoader 之使用方法 ,概略內容如下 :

範例程式將在執行時下載 dex 檔案並於載入後呼叫 dex 提供的函數來開啟新的 Activity,此 Activity 將會利用 Toast 顯示一段啟動成功的訊息。

1. 製作 dex 檔案 :
將底下程式碼另存成檔案 PayloadActivity.java :
package com.example;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;

public class PayloadActivity extends Activity {
  public static void starter(Activity parentActivity){
    Intent tmp_intent =new Intent(parentActivity, PayloadActivity.class);
    parentActivity.startActivity(tmp_intent);
  }
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Toast.makeText(this, "Instantiate PayloadAcitivty Successful", Toast.LENGTH_LONG).show();
  }
}
PayloadActivity class 提供靜態函數 starter,此函數被呼叫之後會啟動 Payload Activity。

把 PayloadActivity.java 編譯成 dex 檔案 (Mac OSX 環境)  :
首先將 java 檔案編譯成 class 檔案 (其中 path_to_sdk 指向 android sdk 之路徑):
javac ./PayloadActivity.java -cp /path_to_sdk/adt-bundle-mac-x86_64-20130917/sdk/platforms/android-10/android.jar -d .
將 class 檔案壓縮成 PayloadActivity.jar 檔案 :
jar cvfM ./PayloadActivity.jar com/
刪除執行編譯指令時產生的 com 資料夾 :
rm -rf ./com
將 jar 檔案轉成 dex 檔案 :
/path_to_sdk/adt-bundle-mac-x86_64-20130917/sdk/build-tools/android-4.3/dx --dex --output=PayloadActivity.dex.jar ./PayloadActivity.jar

將完成的 PayloadActivity.dex.jar 擺在網路空間。 ( 此範例假設可利用 url http://localhost/PayloadActivity.dex.jar 取得檔案 )

2. 程式執行時期下載 dex 檔案 :
 建立一個 com.example.dexclassloader 之 Android Application Project

主要啟動的 Activity 設定為 MainActivity

在 Eclipse 裡新創 Android Application Project,將主要 Activity ( MainActivity.java ) 替換如下 :
package com.example.dexclassloader;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;

import dalvik.system.DexClassLoader;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.Menu;

public class MainActivity extends Activity {
 
 public static ClassLoader payloadClassLoader =null;
 private Smith<ClassLoader> sClassLoader =null;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  Thread works =new Thread(){
   @Override
   public void run(){
    //download and save the jar
    downloadDexFile("http://localhost/PayloadActivity.dex.jar", "PayloadActivity.dex.jar");
    
    //override class loader
    overrideClassLoader();
    
    //load the jar
    loadAndInvokeMethod();
   }
  };
  works.start();
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }
 
 private void downloadDexFile(String url, String filename){
     try {

         URL u = new URL(url);
         HttpURLConnection c = (HttpURLConnection) u.openConnection();
         c.setRequestMethod("GET");
         c.setDoOutput(true);
         c.connect();
         
         FileOutputStream f =openFileOutput(filename, MODE_PRIVATE);
         InputStream in = c.getInputStream();

         byte[] buffer = new byte[1024];
         int len1 = 0;
         
         while ((len1 = in.read(buffer)) > 0) {
             f.write(buffer, 0, len1);
         }
         f.close();
         
     } catch (Exception e) {
         //error dialog
         Log.d("mainactivity", "error -"+e.toString());

     }
 }
 
 private void overrideClassLoader(){
  try{
         Context mBase =new Smith<Context>(this, "mBase").get();
         Object mPackageInfo =new Smith<Object>(mBase, "mPackageInfo").get();
         sClassLoader =new Smith<ClassLoader>(mPackageInfo, "mClassLoader");
 
         ClassLoader mClassLoader =sClassLoader.get();
 
         MyClassLoader cl =new MyClassLoader(mClassLoader);
         sClassLoader.set(cl);
  }catch(Exception e){
   
  }
 }
 
 private void loadAndInvokeMethod(){
  try{
         String libPath =getFilesDir()+"/PayloadActivity.dex.jar";
         File optimizedDexOutputPath = getDir("outdex", Context.MODE_PRIVATE);
         DexClassLoader dcl=new DexClassLoader(libPath, optimizedDexOutputPath.getAbsolutePath(), null, getClass().getClassLoader());
         payloadClassLoader =dcl;
 
         //LOAD CLASS
         Class<Object> payloadClass =(Class<Object>)dcl.loadClass("com.example.PayloadActivity");
         
         //LOAD METHOD
         Method starterFunc =payloadClass.getMethod("starter", Activity.class);
         
         //INVOKE METHOD
         starterFunc.invoke(null, this);
         
  }catch(Exception e){
   
  }
 }

}

觀察 MainActivity 的 onCreate 函數可知程式啟動時將從 url ( http://localhost/Payload.dex.jar ) 下載檔案 (downloadDexFile 函數),並將檔案暫存在裝置記憶體裡。

完成後使用 DexClassLoader 載入 dex 檔案並執行檔案裡提供的 starter 函數 (loadAndInvokeMethod 函數)。
下一節解說函數 overrideClassLoader 的功能。

3. 從 dex 檔案啟動 Activity (修改 ClassLoader) :

藉由 DexClassLoader 載入的程式在啟動 Activity 時 (Activity Class) 不會搜尋 DexClassLoader 所載入的 dex 檔案,因此會出現找不到 Class 的 Exception。為解決這個問題,透過修改 ClassLoader ,我們可以讓程式先搜尋 dex 檔案,若找不到目標則再搜尋預設的 jar 檔案。

底下是覆寫 loadClass 函數部分的程式碼 :

package com.example.dexclassloader;

public class MyClassLoader extends ClassLoader {
 public MyClassLoader(ClassLoader parent){
  super(parent);
 }
 
 @Override
 public Class<?> loadClass(String className) throws ClassNotFoundException{
  if (MainActivity.payloadClassLoader !=null){
   try{
                Class<?> c = MainActivity.payloadClassLoader.loadClass(className);
                if (c != null){
                  return c;
                }
   } catch (ClassNotFoundException e) {
   }
  }
  
  return super.loadClass(className);
 }
}

此外 overrideClassLoader 函數中的 Smith Class 可以幫助我們取得 ClassLoader,其程式碼可以參考這裡

4. AndroidManifest.xml

雖然可以透過 dex 檔案更新並且啟動 activity,但注意的是此 activity 在安裝 apk 之前,必須先定義在 apk 的 AndroidManifest.xml 檔案裡,AndroidManifest.xml 檔案內容如下 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.dexclassloader"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.dexclassloader.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.example.PayloadActivity" android:label="@string/app_name" />
    </application>
    
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

</manifest

ref:http://stackoverflow.com/questions/6857807/is-it-possible-to-dynamically-load-a-library-at-runtime-from-an-android-applicat
ref:http://stackoverflow.com/questions/2519760/android-how-to-use-dexclassloader-to-dynamically-replace-an-activity-or-service


2014-01-17

install build-essential


輸入底下指令即可 :
sudo apt-get install build-essential

底下是在沒有網路情況下的安裝步驟 :
1. 取得 deb 檔案 :
sudo apt-get -qq --print-uris install build-essential linux-headers-$(uname -r) | cut -d\' -f 2 > urls.txt

2. 將 urls.txt 檔案複製到有網路的裝置,輸入底下指令取得所有 deb 檔案 :
wget < urls.txt

3. 將所有 deb 檔案複製到原裝置的 apt-get 暫存資料夾 (假如是利用USB隨身碟) :
sudo cp /media/USERNAME/DRIVER_NAME/* /var/cache/apt/archives/

4. 安裝 build-essential :
sudo apt-get install build-essential linux-headers-$(uname -r)

ref:
http://askubuntu.com/questions/334136/how-do-i-install-build-essential-without-an-internet-connection

2013-12-28

Unity 3D + Local Notification for Android

在 Android 底下使用 Notification Service 配合 Alarm Service 即可使系統在指定時間產生通知訊息。底下敘述如何使用 Android 的 Notification 並且將其編譯成 Plugin 與 Unity 整合。

Plugin 主要只有一個檔案 ( AlarmReceiver.java ) :
package com.macaronics.notification;

import java.util.Calendar;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.util.Log;
import com.unity3d.player.UnityPlayer;

public class AlarmReceiver extends BroadcastReceiver {
    public static void startAlarm(String name, String title, String label, int secondsFromNow){
        Activity act =UnityPlayer.currentActivity;
        Log.i("Unity", "startAlarm...");
      
        Calendar c = Calendar.getInstance();
        c.add(Calendar.SECOND, secondsFromNow);
        long alarmTime = c.getTimeInMillis();
        Log.i("Unity", "alarm time +"+secondsFromNow);
        
        // Schedule the alarm!
        AlarmManager am = (AlarmManager)act.getSystemService(Context.ALARM_SERVICE);
        Intent ii =new Intent(act, AlarmReceiver.class);
        ii.putExtra("name", name);
        ii.putExtra("title", title);
        ii.putExtra("label", label);
        am.set(AlarmManager.RTC_WAKEUP, alarmTime, PendingIntent.getBroadcast(act, 0, ii, 0));
    }
    
    //<receiver android:process=":remote_notification" android:name="AlarmReceiver"></receiver>
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d("Unity", "Alarm Recieved!");
      
        NotificationManager mNM = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        
        Bundle bb =intent.getExtras();

        Class<?> cc = null;
        try {
          cc = context.getClassLoader().loadClass("com.unity3d.player.UnityPlayerProxyActivity");
        } catch (ClassNotFoundException e1) {
          e1.printStackTrace();
          return;
        }
            
        final PackageManager pm=context.getPackageManager();
        ApplicationInfo applicationInfo = null;
        try {
          applicationInfo = pm.getApplicationInfo(context.getPackageName(),PackageManager.GET_META_DATA);
        } catch (NameNotFoundException e) {
          e.printStackTrace();
          return;
        }
        final int appIconResId=applicationInfo.icon;
        Notification notification = new Notification(appIconResId, (String)bb.get("name"), System.currentTimeMillis());
        
        int id =(int)(Math.random()*10000.0f)+1;
        PendingIntent contentIntent = PendingIntent.getActivity(context, id, new Intent(context, cc), 0);
        notification.setLatestEventInfo(context, (String)bb.get("title"), (String)bb.get("label"), contentIntent);

        Log.i("Unity", "notify("+id+") with "+(String)bb.get("title")+", "+(String)bb.get("label"));
        mNM.notify(id, notification);
    }
}
其中函數 startAlarm 主要負責在 Alarm Service 設定 Alarm 事件的啟動時間,接收 Alarm 事件的對象 (com.macaronics.notification.AlarmReceiver ) ,傳遞的資料 (name, title, label),當 Alarm 事件呼叫啟動 onReceive 函數之後,onReceive 則是依照傳遞過來的資料利用 Notification Service 來產生 Notification。

編譯 AlarmReceiver.java (OSX 環境) :
#!/bin/sh

ANDROID_JAR=/Users/macaronics/Desktop/applications/adt-bundle-mac-x86_64-20130917/sdk/platforms/android-13/android.jar
UNITY_JAR=/Applications/Unity4.2.1/Unity.app/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar

javac ./*.java -cp $ANDROID_JAR:$UNITY_JAR -d .
jar cvfM ../AlarmReceiver.jar com/
rm -rf ./com

其中 ANDROID_JAR 是 Android SDK 底下的 android.jar 路徑,UNITY_JAR 是 Unity 底下 classes.jar 的路徑,javac 指令將目前資料夾底下所有的 java 檔案 compile 成 class 檔案並置於相對應於 package name 的資料夾路徑底下,jar 指令則是將資料夾打包成 jar 檔案 (值得注意的是 package name 與資料夾路徑必須要一致,例如 package name 為 com.macaronics.notification,則其 class 檔案須置於 ./com/macaronics/notification/ 底下。 )

設定 AndroidManifest.xml,在 AndroidManifest.xml 的 application 底下新增 :
<receiver android:process=":remote" android:name="com.macaronics.notification.AlarmReceiver"></receiver>
底下為 AndroidManifest.xml 完整內容 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="preferExternal" android:theme="@android:style/Theme.NoTitleBar" android:versionName="1.0" android:versionCode="10">
  <supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" android:anyDensity="true" />
  <application android:icon="@drawable/app_icon" android:label="@string/app_name" android:debuggable="false">  
    <receiver android:process=":remote" android:name="com.macaronics.notification.AlarmReceiver"></receiver>
    <activity android:name="com.unity3d.player.UnityPlayerProxyActivity" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity android:name="com.unity3d.player.UnityPlayerActivity" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" >
    </activity>
    <activity android:name="com.unity3d.player.UnityPlayerNativeActivity" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" >
      <meta-data android:name="android.app.lib_name" android:value="unity" />
      <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" />
    </activity>
  </application>
  <uses-feature android:glEsVersion="0x00020000" />

  <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17" />

</manifest>
將編譯完成的 AlarmReceiver.jar 以及 AndroidManifest.xml 置於 Unity 專案資料夾的 Assets/Plugins/Android/ 底下。

建立一個 C# Script 測試結果,其內容如下 :
using UnityEngine;
using System.Collections;

public class AlarmReceiver : MonoBehaviour {

  // Use this for initialization
  void Start () {

  }
 
  // Update is called once per frame
  void Update () {
 
  }

  AndroidJavaObject nativeObj =null;
  void OnGUI(){
    if (GUI.Button(new Rect(Screen.width*0.5f-90.0f, 100.0f, 180.0f, 100.0f), "Create Notification")){
      if (nativeObj ==null)
        nativeObj =new AndroidJavaObject("com.macaronics.notification.AlarmReceiver");

      nativeObj.CallStatic("startAlarm", new object[4]{"THIS IS NAME", "THIS IS TITLE", "THIS IS LABEL", 10});
    }
  }
}
當使用者按下 Create Notification 按鈕之後 Plugin 會建立一個 10 秒後顯示 Notification 的 Alarm 事件。

本範例的完整 Unity 程式碼可以在 http://github.com/phardera/unity3d_notification_android 下載。

2013-11-30

BeagleBone Black with AnTuTu Benchmark

底下敘述在 BeagleBone Black 上執行 Android AnTuTu App (4.1.1) 的流程 。

A. 製作 Android SDCard :
參考這裡提供的 Android 映象檔,只要將它放在 microSD 卡上就可以直接用了,將下載下來的壓縮檔解開,然後在該資料夾底下輸入 :
sudo./mkmmc-android.sh /dev/sdX MLO u-boot.img uImage uEnv_beagleboneblack.txt rootfs.tar.bz2
注意這行指令必須要在 Linux 環境下執行 ! 但本文要敘述在 mac osx mavericks 環境下 ( 利用 MacBook Air 3,2 內建之 SD Card Reader,但值得注意的是,目前 VirtualBox 尚不支援 USB 3.0,因此較新的 Macbook 可能不適用 ) 的製作流程 :
1. 安裝 VirtualBox,並在上面安裝 linux 作業系統,這裡使用的是 ubuntu server 13.10。
2. 安裝 VirtualBox Extension Pack。

3. 在 Settings --> Ports --> USB 底下將 Enable USB 2.0 (EHCI) Controller 打勾。
4. 新增 Empty Filter

5. 關閉 VirtualBox
5. 在 Terminal 底下輸入 :
sudo launchctl list | grep diskarbitrationd
6. 將前面顯示的數字 (如圖為 20) 代入至底下指令 :
sudo kill -SIGSTOP 20

7. 插入 SD 卡 !
8. 啟動 VirtualBox 之 Linux Virtual Machine
9. 點選底下 USB 圖示,然後選擇  Apple Internal Memory Card Reader。
若是安裝正確,在 /dev 底下會出多 sdb

10. 在 linux terminal 底下取得 這裡 提供的檔案 (可用 wget 指令),將檔案解壓縮後在該資料夾底下輸入 :
sudo./mkmmc-android.sh /dev/sdb MLO u-boot.img uImage uEnv_beagleboneblack.txt rootfs.tar.bz2

11. 恢復 mac osx sdcard reader 之運作,在 Terminal 底下輸入 :
sudo kill -SIGCONT 20

B. 將 sd card 安裝至 beaglebone black 然後按住 Boot 按鈕並插上電源後等待 android 字串出現到螢幕上為止。
C. 底下是 Android 4.2.2 畫面擷圖。

D. 系統資訊擷圖。


D. 底下是 Antutu 4.1.1 跑分結果。

E. 底下是 Antutu 4.1.1 顯示的系統資訊。



ref:http://www.ezequielaceto.com.ar/techblog/?page_id=910
ref:http://www.ezequielaceto.com.ar/techblog/?p=958
ref:http://superuser.com/questions/373463/how-to-access-an-sd-card-from-a-virtual-machine

2013-11-07

Cocos2d-x JNI 筆記

底下整理幾個常用的情況 :
  1. 新增 C++ 檔案
  2. 查看 Java Method Signature
  3. Java 呼叫 C++ 函數
  4. C++ 呼叫 Java 函數
  5. Java 呼叫 C++ 時傳遞數字變數
  6. C++ 呼叫 Java 時傳遞數字變數
  7. Java 呼叫 C++ 時傳遞字串
  8. C++ 呼叫 Java 時傳遞字串
  9. Java 呼叫 C++ 時傳遞矩陣
  10. C++ 呼叫 Java 時傳遞矩陣
  11. Java 呼叫 C++ 時傳遞回呼函數
  12. C++ 呼叫 Java 時傳遞回呼函數

1. C++ 新增 .h/.cpp 檔案 :
   i. 在 Classes 資料夾內新增 .h/.cpp 檔案
   ii. 編輯 proj.android/jni/Android.mk :
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := cocos2dcpp_shared

LOCAL_MODULE_FILENAME := libcocos2dcpp

#新增的檔案加在 LOCAL_SRC_FILES
LOCAL_SRC_FILES := hellocpp/main.cpp \
                   ../../Classes/AppDelegate.cpp \
                   ../../Classes/HelloWorldScene.cpp \
                   ../../Classes/AMGamebarUtil.cpp

LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes

LOCAL_WHOLE_STATIC_LIBRARIES += cocos2dx_static
LOCAL_WHOLE_STATIC_LIBRARIES += cocosdenshion_static
LOCAL_WHOLE_STATIC_LIBRARIES += box2d_static
LOCAL_WHOLE_STATIC_LIBRARIES += chipmunk_static
LOCAL_WHOLE_STATIC_LIBRARIES += cocos_extension_static

include $(BUILD_SHARED_LIBRARY)

$(call import-module,cocos2dx)
$(call import-module,cocos2dx/platform/third_party/android/prebuilt/libcurl)
$(call import-module,CocosDenshion/android)
$(call import-module,extensions)
$(call import-module,external/Box2D)
$(call import-module,external/chipmunk)
2. 查看 Java Method Signature :
javap -s -classpath <path to jar file> <package name>.<class name>
javap -s -classpath ./AMGameBar.jar com.appmaster.AMCocos2dxInterface
javap -s com.appmaster.AMCocos2dxInterface (尚未壓縮成jar, 相對路徑 com/appmaster/AMCocos2dxInterface.class)
3. Java 呼叫 C++ 函數 :
   i. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  //1.定義
  private static native void nativeLoginHandler();
  ...
  
  static public void callNative(){
    //2. 呼叫 c method
    nativeLoginHandler();
  }
}
   ii. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

extern "C"
{
  //c method 定義 :
  //Java_<package name (replace point with under line)>_<class>_<method>
  void Java_com_appmaster_AMCocos2dxInterface_nativeLoginHandler(JNIEnv* env, jobject thiz)
  {
    CCLOG("I HAVE BEEN TRIGGERED BY JAVA !");
  }

  ...
}
4. C++ 呼叫 Java 函數 :
   i. C++  檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

  JNIEnv *env  =NULL;
  jclass classid;
  jmethodID methodid;

  JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
  if (NULL == jvm) {
      return;
  }

  //setup env
  jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
  if (ret !=JNI_OK) {
      env =NULL;
      return;
  }

  classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
  if (!classid){
      return;
  }

  methodid =env->GetStaticMethodID(classid, "initGameBarLib", "()V");
  if (!methodid){
      return;
  }

  env->CallStaticObjectMethod(classid, methodid);
   ii. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  static public void initGameBarLib(){
    ...
  }
}

5. Java 呼叫 C++ 時傳遞數字變數 :
   i. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  private static native void nativeMethod(int val);
  ...

  static public void run(){
    nativeMethod(9527);
  }
}
   ii. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

extern "C"
{
  void Java_com_appmaster_AMCocos2dxInterface_ nativeMethod(JNIEnv* env, jobject thiz, jint val)
  {
    int nval =(int)val;
    CCLOG("val=%d", nval);

    ...

  }

  ...
}
6. C++ 呼叫 Java 時傳遞數字變數 :
   i. C++  檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

  JNIEnv *env  =NULL;
  jclass classid;
  jmethodID methodid;

  JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
  if (NULL == jvm) {
      return;
  }

  //setup env
  jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
  if (ret !=JNI_OK) {
      env =NULL;
      return;
  }

  classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
  if (!classid){
      return;
  }

  methodid =env->GetStaticMethodID(classid, "magicNumber", "(I)V");
  if (!methodid){
      return;
  }

  env->CallStaticObjectMethod(classid, methodid, (jint)9527);
   ii. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  static public void magicNumber(int val){
    ...
  }
}
7. Java 呼叫 C++ 時傳遞字串 :
   i. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  private static native void nativeMethod(final string data);
  ...

  static public void run(){
    nativeMethod("hello C++");
  }
}
   ii. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

extern "C"
{
  void Java_com_appmaster_AMCocos2dxInterface_ nativeMethod(JNIEnv* env, jobject thiz, jstring data)
  {
    const char* pData =env->GetStringUTFChars(data, 0);
    CCLOG("data=%s", pData);

    ...

    env->ReleaseStringUTFChars(data, pData);
  }

  ...
}
8. C++ 呼叫 Java 時傳遞字串 :
   i. C++  檔案如下 :
#include <jni/JniHelper.h>
#include <jni.h>

...

  JNIEnv *env  =NULL;
  jclass classid;
  jmethodID methodid;

  JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
  if (NULL == jvm) {
      return;
  }

  //setup env
  jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
  if (ret !=JNI_OK) {
      env =NULL;
      return;
  }

  classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
  if (!classid){
      return;
  }

  methodid =env->GetStaticMethodID(classid, "initGameBarLib", "(Ljava/lang/String;Ljava/lang/String;)V");
  if (!methodid){
      return;
  }

  jstring arg0 =env->NewStringUTF(pArg0); //char* pArg0
  jstring arg1 =env->NewStringUTF(pArg1); //char* pArg1
  env->CallStaticObjectMethod(classid, methodid, arg0, arg1);
  env->DeleteLocalRef(arg0);
  env->DeleteLocalRef(arg1);
   ii. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  static public void initGameBarLib(String arg0, String arg1){
    ...
  }
}
9. Java 呼叫 C++ 時傳遞矩陣 :
   i. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  private static native void nativeLoginHandler(int[] array);
  ...
  
  static public void run(){
    int[] array ={9, 5, 2, 7};
    nativeLoginHandler(array);
  }
}
   ii. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

extern "C"
{
  void Java_com_appmaster_AMCocos2dxInterface_nativeLoginHandler(JNIEnv* env, jobject thiz, jintArray ptr)
  {
      jsize length =env->GetArrayLength(ptr);
      if (length>0){
          jint* intarr =env->GetIntArrayElements(ptr, 0);
          for (int i=0;i<length;++i){
              printf("val[%d] =%d", i, intarr[i]);
          }

          env->ReleaseIntArrayElements(ptr, intarr, 0); 
      }
  }

  ...
}
10. C++ 呼叫 Java 時傳遞矩陣 :
   i. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

  ...

  int* praw_int =new int[4];
  praw_int[0] =9;
  praw_int[1] =5;
  praw_int[2] =2;
  praw_int[3] =7;

  jintArray arrdata =env->NewIntArray(4);
  env->SetIntArrayRegion(arrdata, 0, 4, (jint*)praw_int);

  ...

  JNIEnv *env  =NULL;
  jclass classid;
  jmethodID methodid;

  JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
  if (NULL == jvm) {
      return;
  }

  //setup env
  jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
  if (ret !=JNI_OK) {
      env =NULL;
      return;
  }

  classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
  if (!classid){
      return;
  }

  methodid =env->GetStaticMethodID(classid, "initGameBarLib", "([II)V");
  if (!methodid){
      return;
  }

  env->CallStaticObjectMethod(classid, methodid, arrdata, 4);

  delete [] praw_int;

}
   ii. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  
  static public void initGameBarLib(int[] data, int num){

    ...

  }

}
11. Java 呼叫 C++ 時傳遞回呼函數 :
   i. JAVA 檔案如下 :
package com.appmaster;

import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;

public class AMCocos2dxInterface
{
  //傳遞回呼函數名稱 (字串)
  private static native void nativeAsyncMethod(final string funcString);
  ...
  
  static public void callbackFunc(){
    //i will be callback from c++
  }

  static public void run(){
    nativeAsyncMethod("callbackFunc");
  }
}
   ii. C++ 檔案如下 :
#include "cocos2d.h"
#include <jni/JniHelper.h>
#include <jni.h>

...

extern "C"
{
  void Java_com_appmaster_AMCocos2dxInterface_ nativeAsyncMethod(JNIEnv* env, jobject thiz, jstring funcString)
  {
    const char* pFuncString =env->GetStringUTFChars(funcString, 0);
    
    ...

    //callback
    JNIEnv *env  =NULL;
    jclass classid;
    jmethodID methodid;

    JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
    if (NULL == jvm) {
        env->ReleaseStringUTFChars(funcString, pFuncString);
        return;
    }

    //setup env
    jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
    if (ret !=JNI_OK) {
        env =NULL;
        env->ReleaseStringUTFChars(funcString, pFuncString);
        return;
    }

    classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
    if (!classid){
        env->ReleaseStringUTFChars(funcString, pFuncString);
        return;
    }

    methodid =env->GetStaticMethodID(classid, pFuncString, "()V");
    if (!methodid){
        env->ReleaseStringUTFChars(funcString, pFuncString);
        return;
    }

    env->CallStaticObjectMethod(classid, methodid);

    env->ReleaseStringUTFChars(funcString, pFuncString);
  }

  ...
}
12. C++ 呼叫 Java 時傳遞回呼函數 :
(ps. 底下的實作並未真正的把 C function pointer 傳遞到 java 環境)
   i. C++ (AMGamebarUtil.h)檔案如下 :
#include "cocos2d.h"

 ...

//定義回呼函數
typedef void (*onLoginCallback)(const cocos2d::CCString* pStrAccountID, const cocos2d::CCString* pStrNickname);

class AMGamebarUtil
{
public:
  static void initGameBarLib(onLoginCallback pCBFunc1);

}

 ...

   i. C++ (AMGamebarUtil.cpp)檔案如下 :
#include <jni/JniHelper.h>
#include <jni.h>
#include "AMGamebarUtil.h"

 ...

onLoginCallback pLC =NULL;

//預先建立 function 讓 java 回呼
extern "C"
{
  void Java_com_appmaster_AMCocos2dxInterface_nativeLoginHandler(JNIEnv*  env, jobject thiz, jstring aid, jstring nickname)
  {
    const char* pAid =env->GetStringUTFChars(aid, 0);
    const char* pNick =env->GetStringUTFChars(nickname, 0);

    //若是回呼函數已指定則回呼之
    if (pLC !=NULL){
      pLC(
        cocos2d::CCString::createWithFormat("%s", pAid),
        cocos2d::CCString::createWithFormat("%s", pNick)
      );
    }

    env->ReleaseStringUTFChars(aid, pAid);
    env->ReleaseStringUTFChars(nickname, pNick);
  }
}

void AMGamebarUtil::initGameBarLib(onLoginCallback pCBFunc1)
{

  //呼叫 JAVA
  JNIEnv *env  =NULL;
  jclass classid;
  jmethodID methodid;

  JavaVM* jvm = cocos2d::JniHelper::getJavaVM();
  if (NULL == jvm) {
      CCLOG("Unity - AMGamebarUtil.cpp, Failed to get JNIEnv. JniHelper::getJavaVM() is NULL");
      return;
  }

  //setup env
  jint ret = jvm->GetEnv((void**)&env, JNI_VERSION_1_4);
  if (ret !=JNI_OK) {
      env =NULL;
      return;
  }

  if (env ==NULL){
    return;
  }

  classid =env->FindClass("com/appmaster/AMCocos2dxInterface");
  if (!classid){
      return;
  }

  methodid =env->GetStaticMethodID(classid, "initGameBarLib", "()V");
  if (!methodid){
      return;
  }

  //REGISTER CALLBACK FUNCTION
  pLC   =pCBFunc1;

  //CALLING METHOD
  env->CallStaticObjectMethod(classid, methodid);

}
   i. C++ (.cpp)執行如下 :
//實作回呼函數
void onLoginCallbackHandler(const cocos2d::CCString* pStrAccountID, const cocos2d::CCString* pStrNickname)
{
    CCLOG("onLoginCallbackHandler(), aid=%s, nickname=%s", pStrAccountID->getCString(), pStrNickname->getCString());
}

 ...

//實際呼叫 initGameBarLib 並傳遞回呼函數指標
AMGamebarUtil::initGameBarLib(&onLoginCallbackHandler);


   ii. JAVA 檔案如下 :

public class AMCocos2dxInterface
{
    //預先定義回呼之 c++ method
    private static native void nativeLoginHandler(final String aid, final String nickname);

    static public void initGameBarLib(){
       ...
       
       //回呼 c++ method
       nativeLoginHandler("THIS_IS_AID", "THIS_IS_NICKNAME");
    }
   
    ...
}

ref:http://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html
ref:http://stackoverflow.com/questions/1232236/retrieving-byte-array-of-unknow-length-from-java-store
ref:http://www.lyceemermoz.com/iris/jlp/util/java/jdk/graphic/graphic.cpp
ref:http://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/types.html

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