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

74 則留言 :

  1. Hello:

    I am not sure if you can understand it.
    Thanks for the tutorial. I tried to follow your tutorial, it is working for me but I am facing one problem. I created a sample project and created a single .jar instead of multiple .jar. It is working but UnityPlayer.UnitySendMessage is not working.
    I am getting "SendMessage: Object not found". Can you help me in this?

    Thanks
    Ashwani.

    回覆刪除
    回覆
    1. Thank you for your question. please make sure the name of GameObject in Unity must be the same with the first parameter of function "UnitySendMessage" in JAVA.

      刪除
  2. I did the same, but when I use a empty gameobject and attach the iabWrapper to it and pass the name of the object in the script it is not working. On the other hand I used Main Camera, it started working for main camera. I am not sure why is it not able to find the other game object.

    回覆刪除
    回覆
    1. looks very strange,... Do you mind to provide your code to let me check?

      刪除
  3. It is now working, I do not know how though :)

    回覆刪除
  4. 您好!
    關於官方指示的連結:http://developer.android.com/intl/zh-TW/training/in-app-billing/preparing-iab-app.html
    現在開啟是404了,另外新版的連結是:http://developer.android.com/training/in-app-billing/preparing-iab-app.html

    回覆刪除
  5. Can you tell me how to use two plugins at the same time?
    I want to use both facebook and in-app billing into my unity project.

    回覆刪除
    回覆
    1. i think you could just put them together, and the following items need to be done:
      1. make sure both plugins use the same bundle id
      2. merge the AndroidManifest.xml file
      3. select the overrideActivity.java file from the facebook project, and modify it to handle multiple "OnActivityResult" callback function

      刪除
    2. 作者已經移除這則留言。

      刪除
  6. Hello. I try to merge two plugins as well. However i have some problem.
    How will I do the step3 , multiple OnAcitivtyResult?
    Should I use different RequestCode?
    Should I create different call back function for FB and Google IAB?
    Can you please give me a hint, really appreciate.

    回覆刪除
    回覆
    1. hi~, we relay the requestcode, the resultcode and the data to facebook and iab plugins, they will pick up the correct data themselves, so... the overrideActivity.java file could be modified as the following:
      http://gist.github.com/phardera/6310238

      please let me know if there is any problem. thanks :)

      刪除
    2. Really Appreciate.. Thanks...

      刪除
  7. Thanks a lot.Hats off to you.
    macaronics You rock \m/

    回覆刪除
  8. 您好~
    我在步驟 2. 編譯 class IabHelper
    因為是用windows系統
    直接創了一個sources的檔案
    但是編譯不過
    不知道跟這個檔案是否有關係
    因為他好像都是跳出cannot find symbol
    不知道是少了什麼
    還是指令下錯
    我是java新手 不太懂這些東西

    謝謝您~

    回覆刪除
    回覆
    1. 您好,
      步驟2 編譯時出現 cannot find symbol 大概是因為找不到 iiabs.jar 或是 android.jar 的關係,所以大概有兩個地方要注意 :
      1. -cp 後面跟隨的 iiabs.jar 及 android.jar 其路徑正確
      2. 若在 windows 下執行 javac, 其 -cp 後跟隨的路徑是以 ";" 符號隔開 (osx 下是以 ":" 符號隔開)

      以步驟 2 為例, 若在 windows 下執行, 則可以輸入改為 :
      javac @sources -cp C:\Users\macaronics\android-sdks\platforms\android-
      10\android.jar;C:\Users\macaronics\UnityIabProj\Assets\Plugins\Android\iiabs.jar

      刪除
    2. 您好,感謝您百忙中抽空回覆

      目前我改用eclipse編jar檔,然後是對.java按右鍵->Export->Java下的JAR file產出您文章上所有需要的jar檔.

      然後將所有的檔案產出後,依照您上面圖片的路徑放置檔案,全部都做完以後,build檔,但是按下purchase按鈕後並沒有任何反應。

      debug後的結果是iabWrapper.cs內的init會因為是null被return掉,
      下載您的sample來做比對,原本有缺少的部分我也都放上了(src檔案內的java檔),miniJSON檔,其他跟iab有關的檔案也都設定了。

      main.cs內的
      iabWrapper.init(...);
      直接就走出來了...

      刪除
    3. 您好, init 失敗有可能是呼叫 init 的物件之生成順序比 iabWrapper 早的關係, 請在 unity 裡設定 iabWrapper 的 script execution order, 讓 iabWrapper 成為第一順位 (例如 : -500)。相關設定可以參考 :
      http://docs.unity3d.com/Documentation/Components/class-ScriptExecution.html

      刪除
  9. 作者已經移除這則留言。

    回覆刪除
  10. 您好~依照您說的方式,有進展了,十分感謝。

    現在在 new AndroidJavaObject(參數1, 參數2);
    這裡進去以後就沒有回應了。

    目前在我的理解上認為是這樣子的,不知道有沒有錯誤,因為正在找為什麼沒有回來的問題。
    ----------------------------------------------
    參數1是eclipse裡面的iabWrapper.java這個class的路徑
    參數2的new object[2]
    - 第一個是app的 public key
    - 第二個是做完以後回呼unity的game object name
    ----------------------------------------------

    // ----- 這是錯誤訊息 -----
    我從Eclipse上面看手機的log顯示

    Exception: java.lang.ClassNotFoundException:tw.com.freeman.test01.iabWrapper
    at UnityEngine.AndroidJNISafe.CheckException () [0x00000] in :0
    at UnityEngine.AndroidJNISafe. CallStaticObjectMethod (IntPtr clazz, IntPtr methodID, UnityEngine.jvalue[] args) [0x00000] in :0

    at UnityEngine.AndroidJavaObject._CallStatic[AndroidJavaObject] (System.String methodName, System.Object[] args) [0x00000] in :0

    at UnityEngine.AndroidJavaObject. CallStatic[AndroidJavaObject] (System.String methodName, System.Object[] args) [0x00000] in :0

    at UnityEngine.AndroidJavaObject.FindClass (System.String name) [0x00000] in :0

    at UnityEngine.AndroidJavaObject._AndroidJavaObject (System.String className, System.Object[] args) [0x00000] in :0

    at UnityEngine.AndroidJavaObject..ctor (System.String className, System.Object[] args) [0x00000] in :0

    at iabWrapper.init (System.String base64EncodedP

    // ----- --- --- --- -----

    因為在您的java檔內看起來運行應該都是正常的
    還是會跟manifest的設定檔有關呢?

    謝謝您。

    回覆刪除
    回覆
    1. 您好, 從您的 log 看來似乎是參數一指向的 iabWrapper class 找不到,
      以這篇文章裡的 iabWrapper.java 檔案為例,因為程式碼第一行的 package com.macaronics.iab; 以及該檔案的 class 為 iabWrapper,所以參數一是寫成 com.macaronics.iab.iabWrapper

      刪除
  11. 您好,感謝您的協助,已經製作出來了,原來是我路徑少一個Android資料夾。

    另外,我自己產生出來的檔案在unity上會無法編過,把res資料夾也一起丟進去才有辦法編譯成功,但是它裡面的icon會影響我在unity上的設定,順便提出分享,再次感謝您。

    回覆刪除
  12. 您好,依照您的說的方法實現了內購功能,十分感謝。但是有個疑問如果出現物品消耗不成功,那如何在程式中做處理。

    回覆刪除
    回覆
    1. 您好,
      您可以利用 mHelper.queryInventoryAsync 函式再次取得尚未消耗之物品的資料 (或許在每次程式剛啟動時執行檢查),並利用該資料再次進行消耗動作 !

      刪除
    2. 您好,
      我嘗試過您提供的方法,可是我在IabHelper.QueryInventoryFinishedListener 函式中調用 mHelper.consumeAsync 函式應用程式會發生崩潰。

      刪除
    3. 您好,
      consumeAsync 的呼叫是否在 runOnUiThread 裡呢 ? 若是的話, 是否可以提供 exception 資訊或是部分程式碼提供參考 ?

      刪除
  13. 您好,您在程式內把google傳回來的雙引號替換成單引號傳到unity那邊
    那如果要再從那邊把它用單引號替換回雙引號,是否會有機會出問題呢?

    因為格式內如果有單引號在內,似乎是合法的。

    回覆刪除
    回覆
    1. 您好,感謝您的提問,範例提供的是偷懶的作法,並不適用於具有單引號的 JSON 字串,若是要用在一般的場合,就只能用 JSONObject 按規矩的去產生對應的 JSON String 了

      刪除
  14. 您好,感謝您在百忙中抽空回覆,在使用了提供的方法後能夠實現內購買功能,但在調試中出現如下報錯,對于新手來說不知從何下手解決這個問題。希望得到您的建議。
    java.lang.Error: FATAL EXCEPTION [GLThread 4134]
    Unity version : 4.1.5f1
    Device model : motorola DROID RAZR
    Device fingerprint: motorola/XT912_verizon/cdma_spyder:4.1.2/9.8.2O-72_VZW-16/58:user/release-keys

    Caused by: java.lang.IllegalStateException: Can't start async operation (launchPurchaseFlow) because another async operation(launchPurchaseFlow) is in progress.
    at com.iyangs.android.util.IabHelper.flagStartAsync(IabHelper.java:793)
    at com.iyangs.android.util.IabHelper.launchPurchaseFlow(IabHelper.java:358)
    at com.iyangs.android.util.IabHelper.launchPurchaseFlow(IabHelper.java:324)
    at com.iyangs.bloodevilspvp.iabWrapper.purchase(iabWrapper.java:161)
    at com.unity3d.player.UnityPlayer.nativeRender(Native Method)
    at com.unity3d.player.UnityPlayer.onDrawFrame(Unknown Source)
    at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1516)
    at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1240)

    回覆刪除
    回覆
    1. 您好, 遇到這個情況通常是因為前一個 google iab 指令尚未處理完畢 (尚未回呼) ,就執行另一個 google iab 指令造成,您可能要做一些阻檔檢查,避免這個情況發生。

      刪除
  15. 首先要先多謝閣下無私分享這些心得,小弟之前使用過閣下的FB方法integrate入unity,近日想用Google的App-In Purchase使人能在自已的apps中購買虛疑物品。但發現要透過Google Wallet Merchant Account才能收到客人的付款(google checkout即將停止運作…);當signup這類account時發現未有小弟的國家(Hong Kong)可供填寫伸請,想請教閣下能怎樣解決?是否只能在amazon用in-app purchase(還好,amazon有native unity plugin呢...)?只有amazon in-app purchase的app(完全沒有 google in-app purchase),在google play供人下載會否被ban account?

    回覆刪除
    回覆
    1. 您好,
      1. 關於 google wallet merchant account 的問題, 因為時間有點久遠, 印像中當初申請時沒有碰到國家的問題 (那時還是舊的 google merchant), 但不論如何, 最後的匯款似乎是沒有國家的限制 (設定銀行帳號包含 BIC/SWIFT),
      2. google 的確有規定不能使用第三方支付管道, XD (但 google play 金流在中國地區無法使用喔)

      刪除
  16. 你好啊,其實我很不明白,我聽過用UNITY的ANDROIDJAVAOBJECT可以直接把JAR內的CLASS實體化,繼而使用該CLASS。如果是這樣,照道理.只要有IABhelper.java 一個檔,生成JAR, 這不就可以直接在UNITY中以ANDROIDJAVAOBJECT直接調用來實現內購嗎??

    回覆刪除
    回覆
    1. 您好, 您說的沒錯, 但這篇文章的範例多寫了一個 iabWrapper 主要是整合 unity 與 iabhelper 之間的互動 : )
      此外, 我這裡也非常有興趣想看看 unity 直接與 iabhelper 介接的程式 !!

      刪除
    2. HI! 我發覺似乎是沒有任何辦法在UNITY中建立INTERFACE類作為參數傳送給JAVA端呢......如此一來麻煩事來了,JAVA 端的FUNCTION經常需要傳入INTERFACE做為參數。但UNITY 中就算用C#,建立的INTERFACE我猜也不可能直接送給ANDROIDJAVAOBJECT.CALL中做為參數........所以網上例子全是多做幾重功夫.....

      刪除
  17. 請問一下
    我把您的範例裡的商品
    改成我的後
    執行也都沒問題

    但在購賣的視窗裡顯示
    this is test order, you will not be charged
    這要怎麼消掉
    謝謝

    回覆刪除
    回覆
    1. 您好, 有可能是您的 gmail 加入到 google play console 裡, 測試用 gmail 帳戶的關係, 試著在手機上登入其它 gmail 帳戶試看看

      刪除
    2. 謝謝回覆
      把測試帳號拿掉就沒了
      謝謝你幫我解決了這個困擾以久的問題

      後來發現
      如果是IAB V2 版沒有這個問題
      好像只有IAB V3 版才有

      刪除
  18. 你好 , 非常感謝你的教學。
    有一點我想請問一下 , 就是再次購買的問題..
    我想做的是單次購買 , 請問有辦法在再次購買失敗的回傳類似Apple的Restore嗎?
    謝謝

    回覆刪除
    回覆
    1. 您好, 其實我不太明白您的問題, 假若您指的 Apple Restore 是 Restore Transaction 的話, 那麼在 android 這邊可以使用 mHelper.queryInventoryAsync 來取得該使用者已購買之項目

      刪除
    2. 非常感謝你的回覆 , 是的 , 我指的是Restore Transaction
      但是如果在不重新編譯.jar的話 , restore可以做到嗎?

      還有一個問題是跟先前一個發問者一樣 , 想要合拼Facebook 跟 IAB 但是

      1. make sure both plugins use the same bundle id <--- 是指package="com.macaronics.iab" 裡的id嗎? 請問是如何做到?

      2. merge the AndroidManifest.xml file <--- 我不知道是不是第1步做錯了 , 我嘗試著合拼他 , 可是每次IAB取消購買都會Crash... 請問可以給一個合拼的範例嗎?

      3. select the overrideActivity.java file from the facebook project, and modify it to handle multiple "OnActivityResult" callback function <--- 請問是不是使用你提供的連結那個程式碼就可以了?

      因為個人對Android運作不太清楚 , 所以問題有點多 , 非常抱歉。

      刪除
    3. 您好,

      範例提供的 jar 檔應該是無法達到要求, 需要額外實作 queryInventoryAsync 的呼叫, 或是將購買失敗後的 purchase 訊息 (位於 onIabPurchaseFinished) 回傳至 Unity.

      1. 是 package name 沒錯, 一般來說是指這兩個 plugins 內有些相同檔案 (androidmanifest.xml, overrideactivity.js,...)在合拼時, 必須決定並統一使用一個 package name.

      2. 是否可以提供 crash 的資訊 ?
      3. 是的可以使用連結提供的程式碼.

      刪除
  19. 您好, 非常感謝您的分享
    目前照著上面的流程走已經弄得差不多了,
    也終於看到iab successfully initialized這行Log。

    只是對於下面這行指令的三個參數不太能理解
    iabWrapper.purchase("PRODUCT_SKU", 10001, "PRODUCT_SKU_AND_USER_ID_AND_DATE",

    第一個參數 PRODUCT_SKU 應該是填入後台設定的產品名稱
    第二個參數為何要指定10001這個數字呢?
    第三個參數應該是怎樣的格式, UserID與 Date指的又是什麼呢?

    小弟還是新手,請多包涵。

    回覆刪除
    回覆
    1. 您好,
      第二個參數可以代入任意的正整數, 當 purchsae 完成之後, google iab 會呼叫我們的 onActivityResult, 並回傳此正整數, 讓我們確定這個訊息來源是 google iab
      第三個參數是任意的字串, 範例是用玩家的 id + 日期, 此字串同樣會在 google iab 完成訂單之後附帶在訂單訊息裡, (假如訂單是代入玩家 id+日期, 那麼我們可以透過這個字串了解到是誰在什麼時侯丟出這個訂單)

      刪除
    2. 作者已經移除這則留言。

      刪除
    3. 非常感謝前輩的解答, 藉由msgReceiver()可以看到
      desc/developerPayload為自定義的第三個參數.
      現在也完成整個IAB的測試了。

      在整個購買過程中
      IabHelper.OnIabPurchaseFinishedListener()與IabHelper.OnConsumeFinishedListener()
      各別都會呼叫Unity iabWrapper.msgReceiver()一次
      內容差別僅在於[code]為2跟3

      還有一個問題想請教前輩,
      我希望在玩家購買商品之後,能夠透過自訂的server去驗證這筆訂單,並且做相對應的資料庫操作
      且該商品在GooglePlay後台的類型為"未納入管理的商品"
      那針對驗證訂單這個機制我應該如何設計會比較適當呢?

      是在本機端收到IabHelper.OnIabPurchaseFinishedListener之後再將訂單資訊傳給自己的server嗎?
      再次感謝前輩的解答。

      刪除
    4. 您好~
      是的, 將訂單訊息傳給自己的 server 之後由自己的 server 來做驗証 ( 可以參考這一篇 : http://phardera.blogspot.tw/2013/03/nodejs-google-play-in-app-billing.html ),待驗証無誤後再通知 client 做 consume 的動作 !

      刪除
  20. 您好 請問可以加您的SKYPE

    小弟對IAB好多疑問想請教一下

    小弟的SKYPE kofking99@giga.net.tw

    感恩

    回覆刪除
    回覆
    1. 您好~
      需要聯絡我可以用這個信箱喔 :phardera(at)live . ca
      謝謝~~~

      刪除
  21. Debug.Log("iab successfully initialized");

    這個log已經有了

    但我在我的程式裡面,按了某按鈕後
    iabWrapper.purchase("money100", 10001, "money100_Priest",
    delegate(object[] ret){
    if (false ==(bool)ret[0]){
    Debug.Log("purchase cancelled");
    }
    else{
    PurchasedItem("money100"); //購買成功要做的事情
    }
    });

    這段程式卻沒有把google Play 要付費的畫面叫出來,就完全沒有反應....
    Debug.Log("purchase cancelled"); <<第一次按iabWrapper.purchase 會有這個Log...
    但還是沒有轉跳到刷卡付錢的畫面...

    如果大大有時間可以替我解答一下嗎? 非常感謝!!

    回覆刪除
    回覆
    1. 您好,...
      您的狀況目前我這裡暫時無法確定是什麼原因, 但請確認....
      1. androidmanifest.xml 的 users-permission 有正確設定
      2. 是在手機還是在模擬器上執行 ? (必須在含有 Google Play App 的 Android 上執行)
      3. 是否有正確編譯 IInAppBillingService.java
      4. 使用 adb logcat 觀察呼叫 purchase 之後是否有出現錯誤訊息或 exception...

      刪除
    2. 感謝您,我發現是我的IadHelper裡面少了東西
      已經可以了^^ 謝謝大大百忙之中回我~~

      刪除
  22. 您好, 想再提問關於物品消耗的問題
    剛剛測試有出現購買成功的提示
    但是似乎還沒消耗該物品
    導致現在要測試購買同一個物品沒有回應

    前輩在先前的發文有提到要使用mHelper.queryInventoryAsync來消耗物品
    指的是IabHelper.java裡面的function嗎?
    而且queryInventoryAsync()有三個overloading。

    我目前的狀況是在Google Play後台有五個商品
    其中四項已經購買成功尚未消耗
    那我應該如何正確的消耗這些商品呢?

    回覆刪除
    回覆
    1. 您好, 其實範例當中就己經有包含物品消耗的部分了(iabWrapper.consume_inapp), 你可以先檢查呼叫這個函數所代入的參數是否正確以及其回傳的結果是否為 true 或 false, 若是 true 則代表成功.

      此外, 若是未成功消耗則再次購買時應該會出現提示已經購買該產品之類的訊息, 而不是未回應才對,...

      最後關於 queryInverntoryAsync 的部分, 指的是 iabhelper.java 沒錯, 關於 queryinventoryasync 這個 function, 簡單的流程大概是使用 function :

      public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener)

      其中 queryskudetails 代入 true, 它的 callback 會傳回 inventory, 此時用 getallpurchases 函數取得 inventory 裡所有的 purchase 之後, 各別將 purchase 再代入 consumeAsync 去做消耗的動作即可.

      刪除
  23. 您好~不好意思,想請教一下請問developerPayload的字串格式有限制嗎?
    因為我在做server驗證實有些字串會驗證不成功,不知道是否有人有這種問題
    像是裡面有包含/、<或中文字都會驗證不成功~
    因此想提出確認一下,謝謝^^

    回覆刪除
    回覆
    1. hi, 不知您的問題解決了嗎?
      理論上 payload 是可以使用任何字串的, 至於您所說的問題可能是因為範例程式將 iab 訊息回傳至 unity 時 (呼叫 UnityPlayer.UnitySendMessage 將 JSON 訊息傳至 unity 的 msgReceiver function),並沒有正確的產生 JSON 格式字串的關係 (這個範例偷懶)。

      刪除
  24. 大大您好,有關您發佈的unity-3d-google-play-in-app-billing-iab 教學,此篇文章對我幫助很大,很感謝您! 此階段也實作沒問題了,可否跟你要e-mail呢?因為自己的需求,需要修改實作的部分,想請教大大。 感謝!

    回覆刪除
  25. 您好 想請教一下
    小弟用了您的專案 商品也改成自己的
    用實機測試很正常,但是一直說 找不到該商品
    小弟的商品也有用上架 請問是哪有邊有出錯嗎?

    小弟是將APK發佈到 測試那邊還沒正式上架
    PUBLIC_KEY package 名稱 還有商品名稱都改成我自己的了
    一切都很正常 但是找不到商品

    回覆刪除
    回覆
    1. 您好, 關於這個情況可能要確定一下是否真的上傳的 apk 和實機測試的 apk 是同一個 apk 檔案, ....
      此外, Google Play 上面的設定有時不是立刻有效,... 可能得等一下再試看看,....

      刪除
  26. 您好 感謝回復
    問題已經解決了,就像您說的設定不是立刻有效
    謝謝!!!!

    回覆刪除
  27. 請問IInAppBillingService.aidl產生IInAppBillingService.java時需要將IInAppBillingService.aidl中的com.android.vending.billing名改成自己專案的package名嗎?

    回覆刪除
  28. 按照您的方法將所有檔案製作了一次,放入到工程中運行出現了java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.macaronics.iab/com.macaronics.iab.overrideActivity}: java.lang.ClassNotFoundException: com.macaronics.iab.overrideActivity錯誤。不知道哪裡出了問題?

    回覆刪除
    回覆
    1. 你好, 訊息指示 overrideActivity 這個 class 找不到,... 檢查一下 overrideActivity.java 這個檔案是否正確編譯且放在正確的位置~

      刪除
  29. 作者已經移除這則留言。

    回覆刪除
  30. 我還想問一下,為什麼我集成iap后上傳到Google後臺,點擊購買總是提示“系统无法找到您要购买的商品”呢,求大神解救~~
    貼一小段代碼:
    iabWrapper.purchase("3002", 10001, "PRODUCT_SKU_AND_USER_ID_AND_DATE",fun...)

    回覆刪除
    回覆
    1. 您好~
      發生這樣的情況首先確認上傳的 apk 檔案是否跟執行測試的 apk 檔案相同, 再來就是檢查 Product SKU 是否相同, 最後檢查 APK 是否設定為 "已發佈" ( 若不想公開則將 APK 設定為 BETA 或 ALPHA 測試階段),若以上設定都完成還是不行, 那就等等吧, 因為 Google Play 上的設定要生效, 有時得等上個一天 !

      刪除
  31. 你好 感謝你的教學 ,

    最近更新的Unity5在Touch的方面似乎有了一些改動 , 如果把

    寫給了IAB的話會導致Unity本身對Touch無法回應 (應該是onresult衝到)

    請問有解決方法嗎?

    回覆刪除
    回覆
    1. 抱歉 , 中間的那段CODE被吃掉 , 是 :

      "android.intent.action.MAIN"

      刪除
    2. hi, 您好, 我這裡在 Unity3D 5 上測試這個範例, 似乎是很正常哩,...

      刪除
  32. 您好 感謝教學
    我想請問 我在建置專案時出現
    CommandInvokationFailure: Unable to convert classes into dex format. See theConsole for details.
    C:/Program Files/Java/jdk1.8.0_92\bin\java.exe -Xmx2048M -Dcom.android.sdkmanager.toolsdir="C:/Users/Unity3DRD2/AppData/Local/Android/sdk\tools" -Dfile.encoding=UTF8 -jar "C:\Program Files\Unity5.3.5\Editor\Data\PlaybackEngines\AndroidPlayer/Tools\sdktools.jar" -

    stderr[

    PARSE ERROR:
    unsupported class file version 52.0
    ...while parsing com/macaronics/iab/iabWrapper.class
    1 error; aborting
    ]

    只要我有從新改過教學中提到的JAVA檔案 就會有錯
    只有使用您所提供的文檔 卻沒有任何問題
    PS 我好像不小心發到兩篇文 抱歉

    回覆刪除
  33. WIN7/Andriod 25.0.3/AndriodStudio2.3.2/jdk1.8_131 請問要怎麼打包多個java呢使用文字檔會報錯 javac invalid flag"?"
    如果使用 把全部檔名打在同一行則會有95 error

    回覆刪除
  34. error: cannot find symbol super.onActivityResult(requestCode, resultCode, data); ^ symbol: variable super location: class overrideActivity 16 errors

    回覆刪除