Sunday, April 29, 2012

不要怕犯錯

我親愛的朋友︰
如果你現在看到這篇文章,
那是因為我有感而發。

我覺得每個人都應該為自己的未來負責,
所以我跟朋友成立哈工作團隊,
我們利用閒暇之餘做「屬於自己」的APP。

經營了快1年,
每隻APP的廣告營收,
1天不到10塊錢台幣,
下載量更是微乎其微,
我犯的錯是︰我天真的以為我的創意和獨特化,消費者會懂
但是,他們跟創意發想的我們,
其實落差很大。

我做了決策工具,
這個工具很棒,
概念是從管理學的決策Matrix矩陣而來,
好幾次我在求職或做重大決策時,
因為有它,
所以在決定一件事時,
至少能有一點方向。

因此我把這隻APP實作出來,
第1版,反應還不錯,
但是也是累積了快半年,
才達到1,000下載人次。
這跟我當時做的「統一發票愛我」,
很快就達到1萬下載人次,根本無法比。

對於這個數據不死心的我,
又跟我的夥伴提議要做第2版更新。
這麼一改,
花了2個月,
界面也全換了,
並且跟我的夥伴說︰
我們一定會大賺。」
結果,
我跌得很痛。
放到現在快半年了,
還沒到500下載人次。

應該說,
除了挑戰劉子千這隻APP以外,
別摔小安(手機摔了會叫痛)、師父開示(每天看到一則好話)、寫春聯祝福朋友、
手機遺失保鑣…
沒有一隻跌得不慘。
所以到後來,
我已經沒有勇氣再跟我夥伴說︰「這隻一定大賺」了。

在寫程式,
往往是先有一個點子,
然後去做,
做完之後發現不對,
於是砍掉重練,
練了又不對,
又砍掉再練
一直這樣反反覆覆,
不知道過了N次才能成功。

在企劃一個案子也是,
一直想要獨特化,
然後遇到消費者不懂,
不死心又再次獨特化,
消費者仍然不懂,
最後只好調整自己的心態,
於是消費者懂了

即將上線的百萬大學堂「藍牙對戰版」,
關於藍牙的串接,
這隻APP應該被我開了又關不下1000次,
藍牙開開關關一天也至少100次…
整個程式的分層架構刪掉又建3次,
動畫模組修改了4次,
但這些東西,
都是消費者看不到的努力,
也是我一次次不斷犯錯、修改後的結果。

很開心,
百萬大學堂上線1週,
即破台灣區萬人下載,
現在在台灣總排行榜第16名。

我親愛的朋友,
不要怕犯錯。
因為你的成功,
都是因為你犯錯而獲得到的寶貴經驗。
千萬不要怕犯錯

小鰻 2012/4/30

Tuesday, April 24, 2012

2款作品皆進入前50大

上星期同時間釋出了2款遊戲上架,
皆進入台灣區最新熱門下載50大。
真的很開心,
在這裡跟大家分享喜悅。


Friday, April 20, 2012

開啟Eclipse右側灰色標註

有時候我們想要看一個特定對象在一個Class中的所在位置,
開啟的方式是[windows]-->[Preferences]-->左邊的區塊選擇[Java]-->[Editor]-->[Make Occurrences],
然後將Make occurrences of the selected element in the current file選項打勾即可。

註︰Mac版是從上方工具列的Eclipse裡,找到Preference

Tuesday, April 17, 2012

Eclipse要使用native library

除了安裝NDK以外,
還照著這個網頁去安裝了這些擴充套件。

Sunday, April 15, 2012

從BluetoothChat範例程式學藍芽連線原理

撰寫時間︰2012/04/15 16:06
更新時間︰2012/04/14 10:34
文章更新次數︰1

一、前言
Bluetooth藍芽裝置在手機界已經存在很久了,
也早就成為連低階Android手機都有的基本配備。
官方也提供了一個利用藍芽連線互相對話的範例程式
讓我們能快速地了解藍芽在Android中的使用方式。
 
二、文章開始
首先,
我們先建立一個觀念︰
藍芽一定分成2個端點,
分別為被動的Server端主動的Client端

底下是BluetoothChat的Sample Code程式流程︰

=========現在在BluetoothChat.java底下==============
BluetoothChat.java是這個範例程式的主頁面,可以在跟已連線的藍芽設備對話。

1.在onCreate()時,呼叫
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 
一起來看看官方文件裡怎麼說BluetoothAdapter
BluetoothAdapter represents the local Bluetooth adapter (Bluetooth radio).
The BluetoothAdapter is the entry-point for all Bluetooth interaction.
Using this, you can discover other Bluetooth devices, query a list of bonded (paired) devices, instantiate a BluetoothDevice using a known MAC address, and create a BluetoothServerSocket to listen for communications from other devices.
BluetoothAdapter是區域藍芽接口(藍芽廣播)。BluetoothAdapter也是所有藍芽交易互動的啟始點。用這個接口,我們可以偵測區域內有哪些其它的藍芽裝置、查詢已配對過的藍芽列表、用已知的MAC地址建立一個BluetoothDevice實體、建立一個BluetoothServerSocket來監聽是否有其它藍芽裝置傳來的通訊…等。

2-1.得到一個BluetoothAdapter實體之後,
在onStart()裡,
如果沒有啟動藍芽,則要求使用者開啟藍芽。
指令是︰
        
        if (!mBluetoothAdapter.isEnabled()) {
            Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
        // Otherwise, setup the chat session
        }else {
            if (mChatService == null) setupChat();
        }
 

2-2.透過setupChat()建立起基本的對話視窗和BluetoothChatService背景服務,並把主Thread的Handler傳給Service以供日後傳回message。

            mChatService = new BluetoothChatService(this, mHandler);
 

3.在onResume()裡,也做一樣的事,如果檢查沒有開啟藍芽BluetoothChatService背景服務,則再次開始該服務。
if (mChatService.getState() == BluetoothChatService.STATE_NONE) {
              // Start the Bluetooth chat services
              mChatService.start();
            }
 

=========現在進入BluetoothChatService.java裡==============

程式碼才剛開出來,
馬上就看到這個Service的程式架構中,塞了3個執行緒,
分別為︰
(1)AcceptThread
(2)ConnectThread
(3)ConnectedThread

馬上來看看它們在Service裡,分別擔任什麼樣的任務︰
// Cancel any thread attempting to make a connection
        if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}

        // Cancel any thread currently running a connection
        if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}

        // Start the thread to listen on a BluetoothServerSocket
        if (mAcceptThread == null) {
            mAcceptThread = new AcceptThread();
            mAcceptThread.start();
        }
 
1.在onStart()中,
檢查如果ConnectThread和ConnectedThread存在,則將他們關掉。
2.啟動一個AcceptThread(現在的流程是在藍芽開啟中的狀態,開啟了一個AcceptThread待命)。
這個AcceptThread存在的目的,是因為程式先假設每臺裝置都有可能想要跟它做藍芽連線。

來看一下這個程式一啟動後就執行的AcceptThread裡面做了些什麼︰
  /**
     * This thread runs while listening for incoming connections. It behaves
     * like a server-side client. It runs until a connection is accepted
     * (or until cancelled).
     */
    private class AcceptThread extends Thread {
        // The local server socket
        private final BluetoothServerSocket mmServerSocket;

        public AcceptThread() {
            BluetoothServerSocket tmp = null;

            // Create a new listening server socket
            try {
                tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
            } catch (IOException e) {
                Log.e(TAG, "listen() failed", e);
            }
            mmServerSocket = tmp;
        }

        public void run() {
            if (D) Log.d(TAG, "BEGIN mAcceptThread" + this);
            setName("AcceptThread");
            BluetoothSocket socket = null;

            // Listen to the server socket if we're not connected
            while (mState != STATE_CONNECTED) {
                try {
                    // This is a blocking call and will only return on a
                    // successful connection or an exception
                    socket = mmServerSocket.accept();
                } catch (IOException e) {
                    Log.e(TAG, "accept() failed", e);
                    break;
                }

                // If a connection was accepted
                if (socket != null) {
                    synchronized (BluetoothChatService.this) {
                        switch (mState) {
                        case STATE_LISTEN:
                        case STATE_CONNECTING:
                            // Situation normal. Start the connected thread.
                            connected(socket, socket.getRemoteDevice());
                            break;
                        case STATE_NONE:
                        case STATE_CONNECTED:
                            // Either not ready or already connected. Terminate new socket.
                            try {
                                socket.close();
                            } catch (IOException e) {
                                Log.e(TAG, "Could not close unwanted socket", e);
                            }
                            break;
                        }
                    }
                }
            }
            if (D) Log.i(TAG, "END mAcceptThread");
        }

        public void cancel() {
            if (D) Log.d(TAG, "cancel " + this);
            try {
                mmServerSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of server failed", e);
            }
        }
    }
 
我們看到了在AcceptThread裡面,埋入了一個BluetoothServerSocket。
看一下Bluetooth Android官方對它的解釋︰
BluetoothServerSocket represents an open server socket that listens for incoming requests (similar to a TCP ServerSocket). In order to connect two Android devices, one device must open a server socket with this class. When a remote Bluetooth device makes a connection request to the this device, the BluetoothServerSocket will return a connected BluetoothSocket when the connection is accepted.
BluetoothServerSocket是一個開放式的server socket,用來監聽任何傳進來的請求(原理類似TCP ServerSocket)。為了讓2隻Android devices能夠連線,其中一隻裝置必須開啟server socket。當遠端的藍芽裝置向手上這隻裝備請求連線後,這隻裝置上的BluetoothServerSocket會回傳一個accepted的BluetoothSocket給呼叫那一方。

因此我們知道,上面程式碼中
BluetoothSocket  socket = mmServerSocket.accept();
 
就是應證了BluetoothServerSocket會吐BluetoothSocket出來這件事。

回到一開始呼叫AcceptThread.start()的那個時間點,
也就是說,
程式在一啟動時,
都先要求使用者開啟藍芽,
然後隨時準備接收別臺藍芽裝置會傳送連線請求的事件。

我們取到了BluetoothSocket後,
看看這個BluetoothSocket能做些什麼。

首先,
在官方技術文件提到︰
BluetoothSocket represents the interface for a Bluetooth socket (similar to a TCP Socket). This is the connection point that allows an application to exchange data with another Bluetooth device via InputStream and OutputStream.
BluetoothSocket是一個Bluetooth socket的接口(原理類似TCP Socket)。這個連結點允許APP透過InputStream和OutpusStream互相交換資料。

因此我們得知,
BluetoothSocket可以讓我們做到資料交換的功能。

因為在Service onStart()呼叫AcceptThread.start()後,
馬上將藍芽狀態設定成setState(STATE_LISTEN);
因此,在switch迴圈中,
程式執行了connected()函式。

這段程式碼如下︰
                        case STATE_LISTEN:
                        case STATE_CONNECTING:
                            // Situation normal. Start the connected thread.
                            connected(socket, socket.getRemoteDevice());
                            break;

馬上來看看connected()函式做了哪些事
    /**
     * Start the ConnectedThread to begin managing a Bluetooth connection
     * @param socket  The BluetoothSocket on which the connection was made
     * @param device  The BluetoothDevice that has been connected
     */
    public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
        if (D) Log.d(TAG, "connected");

        // Cancel the thread that completed the connection
        if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}

        // Cancel any thread currently running a connection
        if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}

        // Cancel the accept thread because we only want to connect to one device
        if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;}

        // Start the thread to manage the connection and perform transmissions
        mConnectedThread = new ConnectedThread(socket);
        mConnectedThread.start();

        // Send the name of the connected device back to the UI Activity
        Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_DEVICE_NAME);
        Bundle bundle = new Bundle();
        bundle.putString(BluetoothChat.DEVICE_NAME, device.getName());
        msg.setData(bundle);
        mHandler.sendMessage(msg);

        setState(STATE_CONNECTED);
    }
 
為了避免重覆連線,
先檢查有沒有已存在的ConectThread、ConnectedThrad和AcceptThread。
如果有,一律先關掉。
然後,啟動ConnectedThread,
並將MESSAGE_DEVICE_NAME用handler(mHandler,還記得我們前面有提到在BluetoothChat.java傳了一個主Thread的Handler給Service嗎?)傳訊的方式,
將Client端的裝置資料傳回BluetoothChat.java,
讓Server端知道是誰在跟它做連結。

前面提到,
一旦取得了BluetoothSocket之後,
就可以開始執行互相傳遞資料的工作了 。
這個被啟動的ConnectedThread就是在做資料互傳的監聽工作
我們看看ConnectedThread做了些什麼
   /**
     * This thread runs during a connection with a remote device.
     * It handles all incoming and outgoing transmissions.
     */
    private class ConnectedThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;

        public ConnectedThread(BluetoothSocket socket) {
            Log.d(TAG, "create ConnectedThread");
            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            // Get the BluetoothSocket input and output streams
            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "temp sockets not created", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }

        public void run() {
            Log.i(TAG, "BEGIN mConnectedThread");
            byte[] buffer = new byte[1024];
            int bytes;

            // Keep listening to the InputStream while connected
            while (true) {
                try {
                    // Read from the InputStream
                    bytes = mmInStream.read(buffer);

                    // Send the obtained bytes to the UI Activity
                    mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
                            .sendToTarget();
                } catch (IOException e) {
                    Log.e(TAG, "disconnected", e);
                    connectionLost();
                    break;
                }
            }
        }

        /**
         * Write to the connected OutStream.
         * @param buffer  The bytes to write
         */
        public void write(byte[] buffer) {
            try {
                mmOutStream.write(buffer);

                // Share the sent message back to the UI Activity
                mHandler.obtainMessage(BluetoothChat.MESSAGE_WRITE, -1, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                Log.e(TAG, "Exception during write", e);
            }
        }

        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of connect socket failed", e);
            }
        }
    }
 
是的!
有看到嗎?
ConnectedThread正在用BluetoothSocket取得InputStream和OutputStream,
並透過旗下的write()和read()在做2隻藍芽裝置的溝通!

現在剩下ConnectThread還沒有去理解了,
查了一下ConnectThread被start的時間是發生在一開始對話頁面的menu鍵中!
原來ConnectThread的目的是要主動連接其它已開啟藍芽的裝置。
當使用者點擊Connect a device時,
會啟動ConnectThread,
開始尋找附近的藍芽裝置,
並對對方發出連線訊號,
對方監聽到你的配對要求後,
對方手機裡原本程式就開啟中的AcceptThread便答應你的請求,
然後開啟ConnectedThread,
並利用連結成功後得到的BluetoothSocket和你做藍芽傳輸溝通。

主動連線端是Client端,被動接收端是Server端,
就好像精子與受精卵…

三、總結
在這裡我把整個程式流程重覆敍述一次︰

在連線的一開始,兩隻手機的程式一開始都先建立一個AcceptThread
(因為誰都不知道誰最後會成為被動接收的Server端,誰又是主動的Client端),
然後都跟RFCOMM頻道索取這隻app專屬的BluetoothServerSocket實體。

Server方做了些什麼︰
用BluetoothServerSocket這個實體去等待Client端用ConnectThread發出的請求連線事件
連線若成功會得到這次藍芽溝通專用的BluetoothSocket。

Client方做了些什麼︰
Client端執行ConnectThread
1.Client端在與Server方連線(Connect a deivce)之前,
會先取得到Server端的身份證MAC address,
並用該address得到Server端的BluetoothDevice實體。
2.Client端藉由自己的MY_UUID和Server端的BluetoothDevice實體,
從RFCOMM頻道拿到這次藍芽溝通專用的BluetoothSocket。

兩方在這個時候都拿到這次藍芽溝通專用的BluetootheSocket
也都在此時知道了對方的BluetoothDevice實體(知道對方的身份)。
這時候雙方都同時開啟ConnectedThread,
彼此利用BluetoothSocket互相做資料傳輸。

註︰資料傳輸利用 InputStream和OutputStream。

Thursday, April 12, 2012

串接 Google Play In-app-billing 易犯的錯誤

撰寫時間︰2012/04/13 11:41
更新時間︰2012/11/05 11:04
文章更新次數︰11

一、前言
之前提到,
臺灣目前雖然無法購買付費型APP,
但卻可以使用In-app-billing機制來獲利。

Google Play的In-app-billing機制很完善,
因此在機制底下的規矩也很多。
新手在串接時,
可能因此發生了一堆奇奇怪怪的錯誤。
這邊我把遇到的問題跟大家分享,
這些都是我很寶貴的出錯經驗。

二、文章開始
我在串接Google Play in-app-billing時發生過的問題及錯誤如下︰

1.關於應用程式產品內ID值的問題

(1)應用程式產品內ID沒有照規則走
雖然Google Play In-app-billing Document已經很明確的教導我們ID值的命名規則是︰
產品IDs是以唯一性的方式跨越應用程式命名空間。產品ID必須啟始字元必須為小寫或數字,而且組成的字串也都只能有小寫(a-z)、數字(0-9)、下 底線(_)和小數點(.)。以"android.test"開頭的產品ID命名被保留,任何以android.test開頭的命名方式皆不可用。附帶一 提,當您在建立產品ID後,是無法修改的,而且您也無法重覆使用同一組產品ID。

有時候為了搶時間,這個ID值沒有照規範走就定義在程式碼中並上傳成草稿APK至Google Play Publisher。
直到要在Publisher後臺添加 應用程式產品內ID值 時,才發現自己沒有遵照規範。
因此程式碼內的 應用程式產品內ID值 又要再改一次,再重新上傳一個新的草稿APK。
更糟的是︰
如果你的APP有做package name控管,那麼你的package name一定會超出控管。
因為同樣的package name的APP,即使你從後臺刪除又重新上傳,Google Play都會把它們當成是同一隻APP。這使得你package name需要重新命名才能完成上傳,因而package name已經不是你原本希望的名稱了。

(2)設定Publisher後臺 應用程式產品內ID值 的[受管理]與[不受管理]分類時請小心!

由於 應用程式產品內ID值 分成受管理和不受管理類,
這個值如果沒有設定好就儲存或發佈,
後來發現設定錯了,
即使刪除,
都不能再在同一個APP內設定同一個產品ID了。
這很麻煩,
因為程式還要為了這個不小心的錯誤,
重新改程式碼中對應的ID值、重新上傳草稿APK、重新測試…

2.點擊購買流程,iap視窗彈出「這個版本的應用程式還無法用付款功能。」
這算是一個新手錯誤。

如果查看一下LOG,收到的LOG應該是RESULT_DEVELOPER_ERROR
官方文件對這個LOG的定義如下︰
此回應指出您的APP試圖發送iap請求,但是APP的AnddroidManifest.xml裡卻沒有宣告 com.android.vending.BILLING權限。也可能是因為應用程式沒有正確的被簽署,或者您發送了一個非正確格式的請求,像是忘了傳 Bundle的key值或者是使用了一個無法被識別的請求類型。

官方文件曾經說,
要測試iap內容,
有幾個條件需要實作︰
(1)手機的primary account要設成test account
如果不是,請重設為原廠設定,這是官方建議我們的。
(2)test account要設定在Google Play Publisher的編輯個人資料的測試帳戶底下。
因為是測試iap,因此我們上傳的草稿apk不用被發佈,只要儲存即可。
但是 應用程式內產品ID 一定要被發佈。
但是這樣要怎麼找的到這些沒有被發佈的應用程式內ID呢?
答案就是要將手機的account設到這邊當成test account。
(3)androidManifest.xml裡要宣告
<uses-permission android:name="com.android.vending.BILLING" />
而且重點是,不僅要宣告這行,還要把這行放在<manifest>和<application>中間
(4)請確認裝置上的版編、版號、keystore與上傳的草稿APK的版編、版號、keystore一致
因為我們不能用debug.keystore上傳app,
因此,在做內部測試時,
請確認線上的草稿app的keystore是和裝置上要測試的版本的keystore是一致的。

官方文件Testing In-app Billing單元有提到︰
上傳您的草稿APP至發佈網站。您無需發佈您的APP才能執行端點測試和真實產品ID的消費。您僅需以草稿的方式將您的APP上傳。然而, 您必須將您的APP簽署上那把你平時釋出APP時,專用的金鑰。而且,您上傳的APP版號必須和你裝置上要執行測試的APP的版號一致。如果想知道關於如 何上傳APP至Android市集,請見 Uploading applications

我曾經將草稿APK上傳了,
但是就是一直回應我這個版本的應用程式還無法用付款功能
我將這行AndroidManifest.xml裡的宣告改變位置,
然後將package name和 應用程式內產品ID 全部重新定義過,
也都無法解決,
最後才發現我裝置上的APK的版編版號沒有和線上的『已發佈』APK一致
這些細節如果沒仔細去看,
真的會很折磨人。

註︰
2012/11/05
有時候為了不要讓服務直接上線(還在內測階段),會上一隻假的APK檔上架Google Play來測試裝置上DEBUG模式的真實APK。如果上傳的假APK裡Android Manifest屬性和手上實測的DEBUG模式真實APK裡的Android Manifest屬性差太多,可能也會造成即使線上和手上裝置版本號一致,但仍show出此版本無法購買的問題。此時建議直接再上傳一個新版號做測試即可。

3.payload很好用,但是使用它是有條件的。
我們知道在request Purchase時,
可以附一個payload給Google Play,
屆時如果交易成功,
傳回來的Json裡面會傳回這個payload。
因此這個payload變得很好用,
因為我們可以拿這個值來做虛擬幣加值的依據之類的。
但,
這個payload只會在正式金流交易下回傳過來,
如果你請求購買的 應用程式內產品IDandroid.test.purchased之類的,
很抱歉,
傳回來的Json是不會附帶這個payload的。

4.不要將你的購買成功後的程式動作放在Response_OK後面。
    @Override
        public void onRequestPurchaseResponse(RequestPurchase request,
                ResponseCode responseCode) {
            if (Consts.DEBUG) {
                Log.d(TAG, "331 "+request.mProductId + ": " + responseCode);
            }
            //付費成功不是在這裡處理。這裡只是一般購買請求Google Play是否答應的接收處
            if (responseCode == ResponseCode.RESULT_OK) {
                if (Consts.DEBUG) {
                    Log.d(TAG, "335 purchase was successfully sent to server");
                }

//             MainActivity.addMoney();//不是在這裡加值

//              logProductActivity(request.mProductId, "sending purchase request");
            } else if (responseCode == ResponseCode.RESULT_USER_CANCELED) {
                if (Consts.DEBUG) {
                    Log.d(TAG, "340 user canceled purchase");
                  
                }
//                logProductActivity(request.mProductId, "dismissed purchase dialog");
            } else {
                if (Consts.DEBUG) {
                    Log.d(TAG, "346 purchase failed");
                }
//                logProductActivity(request.mProductId, "request purchase returned " + responseCode);
            }
        }
 
在官方的iap教學文檔中,
我們實作了PurchaseObserver。
這個PurchaseObserver被呼叫的時間點,
是在Google Play對iap購買交易有完成的成功回應時,
在ResponseHandler.java呼叫purchaseResponse(), 找我們實作的PurchaseObserver底下去執行程式相關的動作。
但是,
ResponseCode.RESULT_OK只是代表我們可以執行iap購買,
不代表交易成功了。
還記得in-app-billing交易流程圖嗎?
ResponseCode.RESULT_OK只是這個交易flow的第2條而已。
真正表示你交易成功會發生在第7點︰PURCHASE_STATE_CHANGED。

因此交易成功要在onPurchaseStateChange()函式裡實作,
程式如下︰
 @Override
        public void onPurchaseStateChange(PurchaseState purchaseState, String itemId,
                int quantity, long purchaseTime, String developerPayload) {
            if (Consts.DEBUG) {
                Log.d(TAG, "286 onPurchaseStateChange() itemId: " + itemId + " " + purchaseState);
            }

//            if (developerPayload == null) {
//                logProductActivity(itemId, purchaseState.toString());
//            } else {
//                logProductActivity(itemId, purchaseState + "\n\t" + developerPayload);
//            }
            
            //購買完成會呼叫這裡
            if (purchaseState == PurchaseState.PURCHASED) {
             Log.w(TAG, "297 onPurchaseStateChange: PURCHASED");
//             Toast.makeText(IapPage.this, R.string.purchased_success, Toast.LENGTH_SHORT).show();       

             MainActivity.addMoney(purchaseTime, developerPayload);      
//              mOwnedItems.add(itemId);
            }
 

5.老王不能自己買瓜
剛開始串接IAP的新手有時候會遇到「找不到項目」的錯誤,
看LOG回覆的錯誤訊息,並且參照Reference通常都能找出錯誤原因,
我曾遇過的錯誤是因為RESULT_ERROR,查了一下In-app Billing Reference後,發現問題是因為我自己是販售者,我仍然又用販售者的身份去購買in-app Billing的商品,自己跟自己買東西,這件事在Google Wallet是不被允許的。 
因此,如果你現在用Test Account購買商品,記得手機登入的Primary Account不能跟販售者的account相同。 


6.別急
許多人在設定和實作的過程中,都急著想看到可以購買的結果。
但是,如果你是第1次為這隻APP發佈in-app-billing商品時,
Google是需要花時間去處理的(我的實測是2個小時候才找的到該商品,因為中午吃了一個便當,今天吃7-11的義大利麵)。
所以,你發佈出去不代表馬上就能找到應用程式內商品,
等一下他們吧!

註︰最近Google好像不提供不發佈APK、僅發佈iab商品測試消費了,
一定要將apk發佈,才能找的到iab商品(見下圖)。2012/08/14



三、結論
就是這些了。
這些經驗花了我半年 + 卡住數次得來。
它們都是我在串接Google Play iap時常遇到的錯誤。

如果之後還有機會遇到任何的錯誤,
我會把經驗分享上來。
(當然會希望不要再有了!!)

只希望之後大家在串接時,
不要再犯我犯過的錯了。

四、附註
1.官方的Sample Code主程式中unregister observer時的時間點錯了
(範例文件將unregister放在onStop()去執行),
因為我們不知道系統什麼時候會讓Activity進入onStop()狀態,
造成原本應該監聽iap後續動作的observer被系統終止了。
這會造成交易完成後,我們實作的observer有時候會無法順利被呼叫。
最好是移到onDestroy()再去執行ResponseHandler.unregister(myPurchaseObserver);

SurfaceView 的onSurfaceDestroy()的系統呼叫時機

一開始以為surfaceDestroy()這個SurfaceView.callback實作函式一定會在Activity onPause()呼叫後被執行,結果發現不然。

基本上SurfaceView的callback函式與Activity5大進程之間的運行過程為︰
onCreate()-->onStart()-->onResume()-->surfaceCreated()-->onPause()-->surfaceDestroy()-->onStop-->onDestroy()

如果不能掌握surfaceDestroy()被呼叫的時間點,
就很難確保下次該Activity有沒有重新執行surfaceCreate(),
這會造成程式執行上很重大的問題…

查了一下倒底surfaceDestroy()的呼叫時間點為何?
後來在Android Developer看到了一段話︰
The way SurfaceView works is that its Surface is created when the view
is attached to a window, and destroyed when it is detached.  There is
no provision for you to do destroy it at other points, nor is that a
good idea because if the system ever needs to show that part of your
window you will end up with a big hole in the window (where the
SurfaceView exists, but without a Surface to display).

原來SurfaceView的運作時機是建構在Activity的View是否有"黏(attached)"到window上判定。
我們知道,
window是Android最底層的視窗管理元件(看一下我之前PO的這篇),
view是後來才黏上去的產物,
當系統判定當下的Activity的view"擺脫(detached)"了window後,
SurfaceView的surfaceDestroy()函式才會被呼叫。

想半天為什麼Intent另一個Activity出來時,
畫SurfaceView的這個Activity為什麼onPause()後沒有呼叫surfaceDestroy()
原來是這個原因=.=||