Monday, November 5, 2012

測量物件寬高

有時候在getWidth和getHeight無法起作用,
Romain Guy提到因為物件還沒畫出來,
因此此值會回傳0。

因同事的努力,
翻牆到有一位網友j研究了怎樣才會取到物件的寬和高。
用法是這樣的
  1.  final ImageView imageView = (ImageView) findViewById(R.id.imageview);        
  2.         
  3.       //------------------------------------------------方法一  
  4.       int w = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);  
  5.       int h = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);  
  6.       imageView.measure(w, h);  
  7.       int height =imageView.getMeasuredHeight();  
  8.       int width =imageView.getMeasuredWidth();  
  9.       textView.append("\n"+height+","+width);  
詳文請參見連結

Friday, September 7, 2012

ListView裡的OnItemClickListener失去作用(失效)

一、Problem
今天在實作ListView,自訂義一個Adapter繼承BaseAdapter後,
發現ListView原本的OnItemClickListener失去作用。

二、Description
上圖是ListView的其中一橫條,
為了讓內容豐富,
用了2個ImageView(圖片)和2個TextView(文字)元件。
有時候為了讓ListView的內容豐富,
我們常會自訂義一個Layout來讓ListView的內容多元,
但也因為多元,
如上圖所示,ImageView和TextView預設都有自己獲得Focus的能力,
這些元件搶走了原本List獲得焦點的能力。

每一條ListView在點擊時,
原本都擁有自己的Focus,
因此ListView的點擊才有效用。

如果我們希望List被點擊後,
讓OnItemClickListener仍有作用,
該怎麼做呢?

三、Solution
外國論譠描述要改善子元件不影響原來ListView獲得Focus最快的方式就是在自訂義的Layout中,附上一個屬性︰

下片這段程式碼就是上面List_Item內容的Layout佈局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/feeback_item_bg"
    android:gravity="center_vertical"
    android:descendantFocusability="blocksDescendants">

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="3dp"
        android:layout_marginLeft="10dp"
        android:text="題編"
        android:textColor="#FFFFFF"
        android:textSize="8sp" />


    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/feedback_q" />

<TextView
        android:id="@+id/textView3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:paddingBottom="5dp"
        android:singleLine="false"
        android:text="題目內容"
        android:textColor="#000000" />


    <ImageView
        android:id="@+id/imageView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/feedback_a" />

    <TextView
        android:id="@+id/textView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="2"
        android:inputType="text|textMultiLine"
        android:paddingRight="20dp"
        android:text="答案"
        android:textColor="#000000" />

</LinearLayout>

如上面紅色標註,
Android有提供一個快捷方式讓Layout裡的子元件全部失焦,
就是使用
android:descendantFocusability="blocksDescendants"

註︰
1.當我在使用這個屬性時,
仍然無法讓OnItemClickListener發揮作用,
後來才發現我的TextView裡原本有設定一個屬性android:inputType="textMultiLine"
外國網友討論說這是一個Android的BUG,
此屬性一旦宣告,
又會讓上面紅色的宣告失去效果。
我把inputType="textMultiLine"拿掉,
整個問題才被解決。 
2.最好用RelativeLayout的方式,使用僅一層Layout的架構客製這個list_item,
不僅在滑動有效率,也不會有這篇產生的問題。

Saturday, September 1, 2012

在EC2發送Android推播產生中文亂碼

一、Problem
這幾天試驗在亞馬遜Amazon AMI(CentOS)上發送Android的GCM(推播系統)給用戶,
卻一直遇到手機收到中文亂碼的問題。

二、Solution
  1. 安裝中文字元集 yum install fonts-chinese
  2. 設定系統語系檔 #vi /etc/sysconfig/i18n LANG="en_US.UTF-8" 修改為 LANG="zh_TW.UTF-8"
  3. 重新開機

Wednesday, August 29, 2012

購買in-app billing,Google Play視窗開了2次

今天遇到一件事,
至從增加Subscription服務後,
Dungeons在request購買時,
畫面會先跳出找不到項目,
然後又跳出in-app billing商品列表,
I encountered a problem when I was using Google In-app billing today that is when I request purchase, Google Play Billiing view shows twice.
First, It shown "Can't find Item" dialog then shown me the in-app billing product item what I set.

爬文才發現問題出在Google Play Billing的Sample Code:Dungeons.java出了一個邏輯上的錯誤,
那就是︰
Finally I figure out the cause is logic in in-app billing sample code:Dungeons.java.
Wrong code is on below:


 @Override
    public void onClick(View v) {
        if (v == mBuyButton) {
            if (Consts.DEBUG) {
                Log.d(TAG, "buying: " + mItemName + " sku: " + mSku);
            }

            if (mManagedType != Managed.SUBSCRIPTION &&
                    !mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_INAPP, mPayloadContents)) {
                showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
            } else if (!mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_SUBSCRIPTION, mPayloadContents)) {
                // Note: mManagedType == Managed.SUBSCRIPTION
                showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
            }

        } else if (v == mEditPayloadButton) {
            showPayloadEditDialog();
        } else if (v == mEditSubscriptionsButton) {
            editSubscriptions();
        }
    }

紅色那段的邏輯錯誤,
造成Google Play視窗被開啟兩次,
只要將那段改成
 if (mManagedType != Managed.SUBSCRIPTION &&
                    !mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_INAPP, mPayloadContents)) {
                showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
            } else if (mManagedType == Managed.SUBSCRIPTION && !mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_SUBSCRIPTION, mPayloadContents)) {
                // Note: mManagedType == Managed.SUBSCRIPTION
                showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
            }
即可改善Billing視窗開了2次的問題。 
If modify code as above will fix this bug.

Friday, August 10, 2012

輸出時出現Dalvik的UNEXPECTED TOP-LEVEL EXCEPTION: java.lang.IllegalArgumentException

今天將isLibrary的專案Build出來時,
遇到一個
UNEXPECTED TOP-LEVEL EXCEPTION:
java.lang.IllegalArgumentException: already added: Lcom/xxx/sample/R$attr;
的錯誤,
結果在Library裡將bin目錄底下的com/xxx/sample/刪掉,
就能正常Build出程式了。

Monday, July 9, 2012

Fragment筆記

撰寫日期︰2012/07/09 16:55
修改時間︰2015/10/26 14:07
修改次數︰5
一、前言
Android在3.0時就已經推出了Fragment(片斷)這個元件,可惜因為Android3.0以上的市占一直不高,因此一直沒有很完整的使用範例,但是因為最近我在用兼容套件,它能幫我們在Android2.x就使用Fragment,我也剛好用到,順便在這裡把我的學習心得記錄下來。

二、文章開始

1.淺談Fragment
Fragment,它屬於一個完整的Activity使用介面的其中一部份,想像一下,我們可以將多個fragments放在一個Activity底下,達到多區塊的程式能在一個或多個Activity間彼此靈活運用、互相溝通。(可以把它看成是一個子Activity,它們可以在很多個不同的Activity中同複使用)。

Fragment必需被崁在Activity裡面,它的生命週期也完全受到Activity的影響。什麼意思呢?假設今天Activity被Destroy()掉了,Fragment也會跟著被Destroy()掉。
然而,如果Activity運行中,你可以針對Activity裡的數個Fragment做個別獨立的控制,像是新增一個個的Fragment,甚至是移除掉它們。
當你執行fragment transaction時,你還能夠將你新增的Fragment添進back stack(背景堆疊,請見我2010年寫的文章)這個被Activity管理的機制中。每個back stack寫入時機皆發生在fragment transaction發生的時間點上,back stack允許使用者使用手機的返回鍵返回fragment transaction的狀態。

當你新增了一個fragment到你的Activity Layout的同時,它就被常駐進Activity裡ViewGroup的View階層中,它們還能夠自訂義應該要擁有怎麼樣的Layout。
你可以在Activity的Layout中去定義Fragment的擺放位置(使用<fragment>這個tag標籤),當然,也可以使用coding的方式,將Fragment放進Activity已存在的ViewGroup裡。然而,Fragment並不是一定需要成為Activity Layout的一部份的,因為Android也允許Fragment在一個無自屬UI狀態下,當成Activity的"隱性工作人員(invisible worker)"。酷吧!

2.設計原理
Android在3.0(API level 11)的時候介紹了這個Fragments元件,主要是用來支援在large screen(平板)裡多種動態及彈性的UI互動。因為平板的螢幕通常比手持裝置還要大,因此,也得到了許多能夠合併或在畫面中做改變的空間。Fragment因為能夠到幫你處理複雜的view階層(View hierarchy)運算,包括將一個Activity的layout傳至fragments中,因此你能在程式執行時期(Runtime)夠修改Activity的樣貌,也能夠讓Activity去管理並保留每一個fragment被改變的狀態,如同前面所說,存在back stack(背景堆疊)中。

提了那麼多,我在這裡做點假設。想像一下,有一隻APP可以在畫面的左邊顯示文章列表,右側則是顯示文章的內容,這2個都是fragment組成,而且還同時存在一個Activity中,酷吧?
每一個fragment都擁有各自的生命週期回呼函式(lifecycle callback),而且也能處理他們各自輸入事件,因此,fragment跟一般的Activity(一頁拿來讀文章列表,另一個Activity拿來讀內容)是很不一樣的。看看底下的圖1就知道了。
圖1. 此範例說明了如何在一個平板的Activity中,使用2個UI模組,但又能在手持裝置中分開使用。

你應該將每個Fragment模組化,並且讓它們能在Activity中重複使用。因為每個Fragment都定義了自己的layout、操作行為和自己的生命週期回呼函式。然而,因為你可以在多個Activity中去使用單一個Fragment,你就必須去設計得讓它們是能被重複使用的,並且盡量避免設計從A fragment直接手動存取B fragment。這件事很重要,因為一個被模組化的fragment能夠允許你在不同的螢幕中去做不同的排列組合。
當你的APP是設計給平板和手持裝置使用時,你就能利用你能使用的螢幕空間,去做最好且客製化的畫面呈現優化。舉例來說,手持裝置也許需要將多個fragment分散成一個一個的UI畫面,並讓它們存在各自的Activity中。但是因為你用了fragment,當你在用平板尺寸開發時,你就能崁進2個Fragments,然而,手持裝置卻因為空間不足,因此無法讓2個Fragments並存。所以手持裝置的Activity A只能放文章列表,Activity B則是放文章內容。也因為用了Fragment,這隻APP就能在手持和平板上共存,並且相容得很好。

3.建立一個Fragment
要建立一個Fragment,你必須繼承Fragment(或者繼承Fragment的子類別)。Fragment類別的程式碼看起來跟Activity很像,它擁有和Activity雷同的call back(回呼函式),像是onCreate()、onStart()、onPause()和onStop()。事實上,如果你要將你已經開發的APP轉移植到fragment中,你只要將這些code從你Activity的callback函式轉放到各自的fragment回呼函式中就可以了。

在使用Fragment時,通常你會需要實作至少3個生命週期回呼函式,它們是︰

onCreate() - 這個回呼函式是系統在建立Fragment時會呼叫的函式,你應該在這個回呼函式中初始化一些必要的元件,好讓fragment之後在pause或stopped、resumed時能使用。
onCreateView() - 系統會在要畫fragment的使用介面的第1時間去呼叫這個回呼函式。如果你有自訂義的view,你必須在這個回呼函式中return 一個自訂義的view(而且必須回傳為root),當然,如果Fragment沒有自己的View,你就回傳null吧!
onPaue() - 當使用者要離開時,系統會呼叫它。當你要儲存什麼值時,就在這個時機去儲存吧!

大部份的APP應該實作至少上述3種回呼函式。但是其實每個Fragment都有更多的回呼函式是你應該要去處理的。等等還會提到。

三、其它

官方文檔已將Fragment翻譯成中本版,
前往閱讀。

Thursday, May 24, 2012

Google Play Subcription(auto renew)訂閱型服務上線!

Google Play訂閱型服務於美國時間2012/5/24發出了技術公報,
底下簡單列出幾項重點︰

  • 一定要先將APP上線(published)才能使用subscription服務
  • 一個APP內可含多種資費,售價一定要高於0.00
  • subcription的沙箱模式幾週後才會在開發者控制臺上線
  • 價格一旦訂了,就無法更改(更改的功能也要等幾週後開發者控制臺改版才提供)
  • 如果舊資費已上線,使用者也在使用了,後期如果調整資費,subcription仍會以舊資費向使用者扣款。
  • subcription的訂閱期間分2種-月租型年繳型
  • 透過Google Wallet跟使用者扣款
  • 每期的扣款都會寄一封e-mail通知使用者
  • Google會持續地跟同一張信用卡扣款
  • 關於退費,必須從Google Play-->[我的應用程式]裡去解約,不提供直接從APP做解約的動作
  • 當使用者按了退款,會算到週期結束才結束subcription服務(30或31天,視大小月而定)
  • 特殊狀況下,萬一使用者直接找廠商解約,還是可以透過自己的server並使用server-side API去跟Google做API方式的直接解約
  • subcription只支援有支援in-app-billing的國家
  • (台灣也算在內,請見官方in-app-billing支援國家列表)
  • Google Play app版本號必須在3.5以上,Android版號必須在2.2以上
  • 當使用者移除app時,Google會發出警告通知︰這隻APP仍在subcription服務下,並會詢問是否要解除該訂閱
  • subcription的退費模式遵照in-app-billing,需直接找廠商處理。
  • 即使退費了,服務仍會在使用者手機上維持1個月的cycle。
  • subcription一樣有30%的抽成

Friday, May 18, 2012

果合中介的廣告無法貼齊APP邊緣

就是這個xx
剛才發現果合的中介廣告,
陸出的廣告無法貼齊APP的邊緣,
多了5mm。

找很久才發現原因是因為果合後臺有提供一個close button,
那個button用了版面5pixel。

Saturday, May 12, 2012

NDK的相關知識

一、前言
JAVA在程式語言中,
算是高階語言(人類能理解的語言)。
由於JAVA的主要用途用在商業領域,
因此底層的架構是一層又一層的,
執行的效能可想而知就不如C(中階語言)的快速。

因此才有人會說︰
如果你寫的語言越接近組合語言(低階語言,也是電腦的語言),
執行起來當然就會是最快的。

Android因為提供了NDK,
因此可以將很多C語言的專案收納進來。
當然,
Google提供了NDK的支持,
主要也是因為其自由開放的特性,
更希望大量遊戲之類的專案,
能用進來Android領域被其推廣。

在遊戲畫面裡,
每個物件的處理都需要高度的演算,
這些演算如果使用JAVA來處理,
效能很差。
因此,
如果Android想要有遊戲的支持,
NDK的支持就變成了很重要的重點。

最有名用NDK的例子,
非Angry Bird莫屬。
Angry Bird在Android裡並非使用JAVA寫的,
而是C轉過來在Android上執行的。

二、文章開始
這裡先提及幾個可能需要了解的概念。

首先,
C語言會被放在Android Project的JNI目錄底下。

JNI目錄底下的.mk檔
告訴NDK建構系統(NDK build system)你的Native Code的相關資訊。

JNI目錄底下的.c檔
C語言檔。就好像JAVA檔的副檔名叫.java一樣

JNI目錄底下的.so檔
Android若要執行Native code,
並需先將C語言編譯且封裝成Library。
這個概念就好像我們在JAVA底下,
習慣將程式碼封裝成.jar給別的專案使用一樣。

要將Native code編譯成.so檔,
就要使用make.exe。

make.exe
這個檔案是GNU Make的檔,
功能是將Native code轉成library,
讓其它的程式可以使用。
make出來的檔案,
在Windows環境底下副檔名會是.dll,
而在Linux底下,副檔名就是.so了。

Wiki對於make的解釋
軟體開發中,make是一個工具程式(Utility software),經由讀取叫做「makefile」的文件,自動化建構軟體。它是一種轉化文件形式的工具,轉換的目標稱為「target」;與此同時,它也檢查文件的依賴關係,如果需要的話,它會調用一些外部軟體來完成任務。它的依賴關係檢查系統非常簡單,主要根據依賴文件的修改時間進行判斷。大多數情況下,它被用來編譯原始碼,生成結果代碼,然後把結果代碼連接起來生成可執行文件或者庫文件

在make前,
Android的Project需要有build.xml檔方可編譯,
要在命令提示字元底下、該專案目錄裡,打上指令︰
android update project -p . -s -t android-8
此時,
專案目錄底下就會產生build.xml檔,
這時候,再去對C語言做編譯。

Android也提供了一個快捷的方式make,
就是在專案目錄裡JNI目錄裡,下指令︰
ndk-build

Android一收到ndk-build指令,
便去找系統的make.exe,
幫你把這個C語言打包成Library,
並放進專案目錄底下的libs目錄裡,
副檔名為.so。

ndk-build這個檔案放在NDK目錄底下,
一打開目錄就可以看到了。
要在JNI目錄裡執行ndk-build,
你需要先設定好環境變數。

找不到make.exe
make是在安裝Cygwin的時候,
順便安裝下來的套件,
不要到剛才的GNU make網站去抓安裝包,
因為如果是用這種方式安裝,
屆時在Cygwin裡執行make時,
Cygwin會不認識你自己安裝的make指令哦!

安裝make也蠻簡單的
只要Cygwin在安裝套件包時,
輸入make關鍵字就可以找到了。




文章未完哩....

Thursday, May 3, 2012

百萬大學堂 開發日記

 一、前言
百萬大學堂在上線的2個星期內,
下載量正式破5萬人
也持續了1個星期台灣區APP下載前10名。

在這個部落格裡,
將這隻APP開發過程的三兩事記錄下來,
跟玩家們分享。

二、文章開始
[百萬大學堂]誕生

話說,
本來在餐飲業的我,
這樣子傻傻、不怕死的轉進資訊產業,
而且也因為愛死了那隻小綠人,
所以完全都沒有想的就踏入了Android領域。

這樣子奮鬥也快2年了,
我很想問我自己︰
我的程度到哪裡了?
我的作品能被市場認同嗎?
如果沒有公司要我了,我有自己活下去的能力嗎?

在參與了一次台北GTUG(台灣Android開發者討論社群)的聚會後,
得知三星即將舉辦一場App大賽,
廣邀全台灣Android開發者進來這場比賽,
凡事獲得晉級者,
皆可拿到免費的Samsung Note和Samsung Tablet。
這是一個證明我自己的絕佳機會。
後來,
百萬大學堂沒有得到三星App競賽初評的青睞。

我︰
「可惡,本來看上了SamsungAPP大賽只要APP有晉級,
便可以拿到免費的Note和Tablet,
但是卻沒入圍。
Note那臺規格那麼奇怪,
如果有一臺Note,
至少之後的開發,
還能稍微照顧一下Note的使用者。
現在算了啦,
我們也沒錢再買Note了。
海倫,我們轉向,
把這隻APP趕快做成可運行版本,
在Google Play上架吧!」


版本更新歷史
v1.0.0 - 大學堂上架

海倫是[百萬大學堂]的視覺美編,
我(小鰻)是程式設計 + 企劃發想。

我︰
「海倫,天啊,
真的會瘋掉,
你知道嗎?
我剛才拿我剛做好的程式,
去西門町三星通路實測,
在Note上完全跑版耶!
在平板上更慘!」

海倫︰
「驚!」

我︰
「一定要想一個辦法解決Android解析度不同的問題,
不然海倫妳圖會畫死,
我程式介面也會醜爆!」

v1.0.1 - 改進調成靜音後進不了程式的BUG
在v1.0.0版上架Google Play了以後…
我︰
「啊!我是豬頭嗎?
當我在設定裡,將音量調成靜音後,
遊戲進不去了!
凌晨3點,
快點修BUG!」

v1.0.2 - 能夠調整遊戲時間
海倫︰
「小鰻,網友在幹譙不能調時間啦!
一直在我們的評論裡面罵髒話!」

我︰「發生什麼事?」

海倫︰
「剛才我看了一下,上線到現在2天了,
目前差不多有500人下載了,
但是網友都狂幹譙耶…」

我︰
「500了哦?那有機會追過劉子千哦!(心裡暗爽中)」

我︰
「哦,好啦,那我趕快追加一個能調整遊戲時間的版本。」




v1.0.3 - 防小白篇
海倫︰
「小鰻,下載量破1000了,
但是網友現在都在罵我們,
說遊戲超爛的啦!
而且都給我們1顆星,
還跟我們說題目很少…!」

我︰
「這樣不行,
大學堂是我們的孩子,
我們做爸媽的,
有義務保護好孩子。
海倫,
你趕快上去,留一篇中肯一點的評論,
至少能稍微阻止玩家一直負評下去。
我也會添加"開學典禮",
藉由開學典禮告訴玩家可以設遊戲時間、我們有意見回報的功能。
才2個人是能想出多少題目,
你至少幫我上去止血一下。」

海倫︰
「嗯。」

v1.0.4 - 添加成績單回饋
我︰
「海倫喔~
我從第1版就跟你說,
玩家應該不會只想要看到成績,
畫面應該要有多一點的回饋才對,
上次跟你說的如果玩家得到100分,
小綠人會扭屁股,
如果得0分會3條線,
什麼時候才會畫給我啦!」

海倫︰
「哈哈,我選擇性忘記…」

我︰
「…」
「對了,記得喔,小綠人扭屁股的時候,
屁股要加上小叮噹的尾巴。」

海倫︰
「為什麼?那樣不會很奇怪嗎?」

我︰
「我也不知道,但做就對了。」

海倫︰
「哦…」

為什麼小綠人扭屁股會有小叮噹的尾巴
其實我到現在都還是不知道。
但是海倫畫的那個扭屁股,
我第1次看到動畫時,
連笑了5分鐘,
愛死你了,海倫~


v1.0.5 - 藍牙對戰版上線
海倫︰
「小鰻,嚇到我了啦,
網友開始激增,
而且還是負評不斷!」

我︰
「有什麼辦法,
看到他們一直說題目很少,
我現在就卡在藍牙連線對戰開發,
根本就沒時間找題目啊,
你知道藍牙層一直讓我原本的遊戲當掉
超麻煩的!
厚~
為什麼每次我都要挖洞給自己跳啦!
現在也只能趕趕趕,
快點把藍牙連線版釋出,
才有時間去找題目啦!」

海倫︰
「嗯…」

我︰
「在藍牙連線畫面,
海倫我希望用最簡單的流程,
讓玩家能夠完成藍牙連線的動作,
所以我會希望風格是簡單的,
玩家幾乎不用學習,
就能夠操作。」

海倫︰
「哦…(深思中...,其實是放空中。她自己說的)」

我︰
 「等等!海倫!
這2隻對抗的小綠人,
不應該只是對看,
他們中間應該要有火花!」

海倫︰
「厚~對看就有感覺了!幹嘛還要畫火花啦!」


我︰
不行!要有火花!

後來我把程式畫面實作出來後,
海倫她根本就超愛的,
她說電波有"死光"的感覺。

v1.0.8 - 張君亞傳奇(之所以打"張君亞",是因為"張君雅"侵權。)
我︰
「海倫,
當初我們一次把主題都擺上去書架上,
玩家好像都只玩前面一、兩個單元就不玩了,
這感覺不太對哦!
是不是有什麼方法可以鼓勵他們往後玩,
而不要都只玩前面1、2個單元就跟我們說遊戲很爛、沒深度啊?」

海倫︰
「(思考中)其實是再度放空中…」

我︰
「啊!
我想到了,
尋找張君雅
我們做一個單元,
內容是問玩家有關台灣的泡麵文化的相關問題,
主題取名為尋找張君雅。」

海倫︰
「好像還不錯唷!」

我︰
「可以把張君雅的名牌藏在其它主題的黑板或哪裡,
讓玩家去找它,
收集完後就可以開啟張君雅單元了。」

海倫︰
「不然我可以畫張君雅的笛子、書包…」

我︰
「嗯,先這樣吧…」

隔了一天,
早上6點,
睡夢中驚醒,
傳了WhatsApp給海倫︰
「海倫,
張君雅這件事情我們有一個地方不對,
我們要玩家收集的,
不應該是笛子、書包…,
應該要是筷子、湯匙和泡麵!
因為,
張君雅只吃泡麵!
這是上天賦予她的使命!」

海倫︰
「哦~。」

我︰
「筷子任務題藏進我是萬事通單元裡
問筷子的歷史。
湯匙則藏在另一個單元中,
玩家如果回答到任務題且答對,
就會獲得湯匙和筷子!」

玩家們,準備收集「筷子、湯匙和泡麵」吧!

希望這個創意,你們會喜歡哦!

 張君雅一夕爆肥!!!!
話說,
海倫畫給我的張君雅小妹,
本來是一個瘦子,
睡了一天醒來後︰
「海倫,那個張君亞有問題!
張君亞應該是個胖子!」

後來,海倫畫了胖子張君亞給我。
我︰
「哈哈哈,海倫你昨晚是讓張君雅吃了什麼,才一個晚上,胖那麼多!!」

三、總結
百萬大學堂,
仍在陸續改版中,
海倫也持續被我逼瘋中。

未完,待續。

四、後記
聯成電腦專訪百萬大學堂和我的求學經過


給我靈感

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()
原來是這個原因=.=||

Sunday, March 11, 2012

談談Android的螢幕解析度

撰寫時間︰2012/03/12 13:15
更新時間︰2012/03/23 11:54
文章更新次數︰3

一、前言
Android手機有多種解析度,
視覺或美工在設計版面時,
倒底該怎麼做,
才能讓每種不同規格的Android手機,
表現出其最大效能?

二、文章開始
讓我們先從Android的圖形資源檔說起。

左邊這張圖是Android置放圖片的資料夾,
如果以手機來說,
至少可以分成4種資料夾,
分別為drawable、drawable-hdpi、drawable-mdpi和drawable-ldpi。


因為這樣的資料夾分類,
讓每種解析度的手機,
能聰明的依照它們自己的螢幕密度(density),
找到他們需要的圖檔應該要在哪個目錄取得
關於這4個資料夾的使用方式,
以及該放什麼圖片進去,
我會在稍後提及。

註︰

需要先在AndroidMenifest.xml裡將Any density設為true,
手機才會依自己的螢幕密度(density)去找適當的drawable資料夾,
否則會一律從drawable這個預設目錄去取得圖形資源。
(如果又沒有設drawable資料夾,
則會從其它的圖形資料夾下找到該圖形資源,
然後依照density的比例去自動縮放。

如果你現在看不懂我說的,
無妨。
因為等一下我還會再說明。)

Android官方技術文件中提供目前Android的解度析有以上這些規格(但實際上有更多)
要如何理解這張表格,
小鰻當時也花了很久的時間在摸索,
當然,要怎麼知道手機的density,
Google很多人都有教了,
不在此特做說明。

以欄位來看,
第1欄屬於ldpi的手機規格至少有︰
QVGA(240x320)、WQVGA400、WQVGA432、WVGA800、WVGA854...。

 Android Design Guide建議我們在處理每種解析度的圖片問題時,
可以使用底下2種方式︰
1.先從mdpi的規格去製圖,然後再使用photoshop之類的圖片編修軟體,向上調整尺寸和向下調整。
2.從最大尺寸去製圖(假設你想開發手機App,可以從Galaxy Nexus的density320開始繪製),然後再向下縮小。

Android的最基本尺寸規格是Medium Density(160) - mdpi
任何其它的Android尺寸,
都是由mdpi去向上或向下延伸換算出來的,
因此我個人是比較推薦使用第1種 - 從mdpi去調整圖形大小的方法來製圖。

photoshop是Google很推薦的繪製工具,
因為使用這套軟體,
可以自由的拉大和縮小圖形也不會失真(以專案檔的方式來說的話)。

如同我前面所說,
如果要設計手機版面,
至少會有4種drawable資料夾,
這裡解釋一下這4個資料夾的用法。

1.drawable
drawable資料夾相當於drawable - nodpi,
如果手機從這個資料夾抓出圖片,
那麼系統繪製圖片的方式,
是"圖片有多大就畫多大",
以1:1的方式去繪製。

所以,如果使用這個資料夾,
通常還要在程式裡做很多螢幕解析度的換算,
不然會遇到圖片在小手機,
根本完全跑版、超出手機螢幕大小的問題。

2.drawable - mdpi
mdpi(density160)是Android標準的規格尺寸,
任何其它dpi,都是從mdpi去演算及延伸的,
強列建議先由mdpi去排版製圖,
然後再去調整其它的解析度。

3.drawable - hdpi
前面說到,
Android的標準尺寸是mdpi(density160),
而hdpi則是density240,
240(hdpi density) /160(mdpi density) = 1.5。
這樣的比例換算下來,
螢幕顆粒密度比原本的mdpi還要精密,
因此,
你會發現圖片在mdpi中看是正常大小,
但到hdpi的手機來看,
圖片被明顯縮小了。

因此,
這個資料夾裡的圖片,
你必須畫得比drawable - mdpi的圖檔還大。
大幾倍?
答案是...150%

這個值就是剛才240/160=1.5算出來的值,
你必須將mdpi的那張圖,
用photoshop放大150%另存新檔,
放在drawable - hdpi來用,
才能顯示跟drawable - mdpi一樣的畫面大小。

3.drawable - ldpi
ldpi的Density為120,
120/160 = 0.75,
因此,
使用photoshop將剛才mdpi的尺寸,
縮小成75%
放進drawable - ldpi目錄,
將能讓呈現出來的畫面,
跟mdpi的畫面一樣。

如果要製作不同解析度的icon,你需要對每一種dpi去客製化,才能在每種手機上呈現一樣的結果。


如果你問我︰
「天啊!
我一定要花那麼多的工夫在製做不同尺寸的解析度嗎?」
這個答案是可選擇的。

why?
如果你今天開發的App,
畫面只繪製mdpi,
那有沒有辦法在ldpi和hdpi正常顯示呢?
答案是...可以。

把drawable目錄刪掉,
剩下其它3個目錄(drawable - ldpi、drawable - mdpi、drawable - hdpi),
且將繪好的mdpi圖形,
丟進drawable - mdpi,
如果程式發現現在使用的手機,
是ldpi或hdpi時,
系統會自動去換算放大或縮小,
也不會讓原本設計給mdpi的圖片尺寸,
在hdpi或ldpi中跑版。
但...這就牽扯到視覺(美工)人員最在意的品質問題。

品質是視覺(美工)人員的生命,
他們的職責就是替產出的作品負責,
畫面上的每1 mm,
都是比自己的小孩還要親的小孩。
當然,
也是有視覺(美工)人員是比較...隨遇而安的心態啦...。

讓系統去自動縮放圖片大小
壞處有下︰

1.如果手機解析度比mdpi還大(如hdpi),
那麼這張圖將被拉大,
畫面將沒那麼精美了。

 2.如果手機解析度比mdpi還小(如ldpi),
那麼這張圖將被縮小,
看起來似乎沒什麼問題,
但你要知道,
使用者手上拿的這種ldpi手機,
通常都是效能極差的Android手機(我們稱它為Android低階機,講好聽叫入門機),
以台灣來說,
通常都是電信業者綁門號,
 0元就能帶回家的Android手機。

這種品質的手機,
你在開發時,
會發現它的所有硬體支援度都比中高階的Android手機還差。
如果iPhone開發工程師已經在開發下一款產品時,
老闆為了要旗下的產品支援所有Android手機,
你會發現你還在調校這些低階手機。

現在,
你把mdpi大小的圖片塞進這隻效能極差的ldpi手機中,
因為效能極差,
沒多久,
你就會遇到記憶體不足(我前面的文章有提到)的問題!

三、結論
無論你是視覺(美工)人員或者是Android程式設計師,
不管是螢幕畫面還是手機效能,
最好都還是替不同的螢幕解析度去客製化不同的圖片大小,
一來控制畫面品質,
二來也能確保程式穩定度。

這些細心,
每一種規格的Android手機使用者都能依各自的可用資源看到你呈現出來的產品,
何樂而不為?

四、備註
我開發過的手機和平板規格︰
1.Nexus one(800x480, density 240)
2.Nexus S(800x480, density 240)
3.Galaxy Nexus(1280x720, density 320)
4.HTC Hero(480x320, density 160)
5.Sony Arc(854x480, density 240)
6.遠傳小精靈(320x240, density 120)
7.HTC野火機1代(320x240, density 120)
8.Samsung SII(800x480, density 240)
9.HTC Flyer平板(1024x600, density 160)
10.Samsung Tab 7.7(1024x600, density 170)
11.HTC Desire(800x480, density 240)
12.HTC Incredible(800x480, density 240)
13.HTc Sensation(960x540, density....)
還有更多...

以上如果規格或Density有錯,
煩請告知。
感謝

其它導讀︰
1.處理多螢幕解析度問題-PartII(外文)

Tuesday, March 6, 2012

Android刷機工具 Odin



刷Kernal的方式
如果kernal是.zip,使用recovery mode刷入;如果Kernal是.tar,則用Odin,不要勾選Re-partition(不分區),放入PDA中直接刷!

小技巧

  1. 刷Rom前、後最好都雙Wipe(進入Recovery mode執行Wipe Data和Wipe cache partition)
  2. 先刷完Rom後再刷kernal
  3. 通常kernal都是放進PDA裡刷


Monday, March 5, 2012

[技術公報]Android App打破50MB的上傳限制了

Android Apps Break the 50MB Barrier


Android applications have historically been limited to a maximum size of 50MB. This works for most apps, and smaller is usually better — every megabyte you add makes it harder for your users to download and get started. However, some types of apps, like high-quality 3D interactive games, require more local resources.
Android app先前將最大檔案容量限制在50MB,這個方案在許多的App上運作的很好 - 因為你所編譯並產出的每megabyte都會讓你的使用者需要花時間下載後才能啟動。然而,有一些類型的App,像是高畫質的3D互動遊戲,卻需要更大量的本地端資源。

So today, we’re expanding the Android app size limit to 4GB.
因此,我們今天將Android App的容量限制提高至4GB

The size of your APK file will still be limited to 50MB to ensure secure on-device storage, but you can now attach expansion files to your APK.
APK檔容量其實仍然限制在50MB,這是為了確保裝置儲存的安全性。但是你現在還可以添加擴充檔至您的APK中。
  • Each app can have two expansion files, each one up to 2GB, in whatever format you choose.每個App有2個擴充檔,每一個擴充檔最高是2GB,沒有檔案格式上的限制
  • Android Market will host the files to save you the hassle and cost of file serving.
    Android市集會將這2個充檔保留在Android Server中,一方面省去了你開發上的麻煩,也節省你的開發成本。
  • Users will see the total size of your app and all of the downloads before they install/purchase.
    使用者會在他們安裝/購買前,看到你App的總量和總下載次數。
On most newer devices, when users download your app from Android Market, the expansion files will be downloaded automatically, and the refund period won’t start until the entire download completes. On older devices, your app will download the expansion files the first time it runs, via a downloader library which we’ve provided below.
在大多數新型裝置中,當使用者從Android市集下載了您的App,擴充檔也會跟著被自動下載,此時,15分鐘的退費期間將不會被算在內,15分鐘的限制將會在全部的檔案都下載完成後才會開始計算。然而,在一些舊有的裝置中,你的App會在下載並執行App的第1次啟動後,才開始透過我們底下提供的downloader函式庫下載擴充檔。

While you can use the two expansion files any way you wish, we recommend that one serve as the initial download and be rarely if ever updated; the second can be smaller and serve as a “patch carrier,” getting versioned with each major release.
雖然您可以依照你希望的方式來使用這2個擴充檔,我們仍建議您將其中一個擴充檔用在初始化下載,若是要更新App,盡量不要用它。另一個擴充槽則可設計成被置放較小的檔案,提供"更新補丁(patch carrier)",用來做每個版本主要更新的內容。

Helpful Resources有用的資源

In order to make expansion file downloading as easy as possible for developers, we're providing sample code and libraries in the Android SDK Manager.
為了能簡單且有效的讓開發者使用擴充檔這個資源,我們在Android SDK Manager提供了簡單的範例程式和函式庫。
  • In the Google Market Licensing package, an updated License Verification Library (LVL). This minor update mostly adds the ability to obtain expansion file details from the licensing server.
    在Google市集的許可套件中,一個更新的許可驗証函式庫(LVL)。能過這個小更新取得擴充檔的詳細資訊。
  • From the Google Market APK Expansion package, the downloader service example. The library makes it relatively simple to implement a downloader service in your application that follows many of our best practices, including resuming downloads and displaying a progress notification.
    從Google市集APK擴充套件來的downloader服務範例。函式庫非常容易從downloader服務去實作,因為這是從我們很多很棒的實作中去完成的 - 功能包含了續傳下載和訊息通知欄的下載進度顯示。
Because many developers may not be used to working with one or two large files for all of their secondary content, the example code also includes support for using a Zip file as the secondary file. The Zip example implements a reasonable patching strategy that allows for the main expansion file to “patch” the APK and the patch file to “patch” both the APK and the main expansion file by searching for asset files in all three places, in the order patch->main->APK. 
由於許多的開發者也許不會用他們次要的擴充檔來放置1~2個大型檔案,因此範例程式用zip格式來做次要檔的放置。zip範例的實作是合理的策略,允許主要的擴充檔以"patch" APK的方式來擴充,(恕刪)...........順序是ptch-->main-->APK。

Expansion File Basics擴充檔基本概念

Expansion files have a specific naming convention and are located in a specific place for each app. As expansion files are uploaded to the publisher site, they are assigned a version code based upon the version of the APK that they are associated with. The naming convention and location are as follows:
擴充檔在每一個app中,都擁有特定的命名慣例和置放的位置。擴充檔被更新至publisher網站,也被指定到相關連的某個version code以上的apk中。命名慣例和位置依照︰
Location: <shared-storage>/Android/obb/<package-name>/
Filename: [main|patch].<expansion-version>.<package-name>.obb
Example: /sdcard/Android/obb/com.example.myapp/main.5.com.example.myapp.obb
Expansion files are stored in shared storage. Unlike APK files, they can be read by any application.
擴充檔被儲存在分享空間。它們不像APK檔,因此可以被任何的APP自由的讀取。

Downloading and Using the Expansion Files下載並使用擴充檔

When the primary activity for the app is created, it should check to make sure the expansion files are available. The downloader library provides helper functions (for example the “Helpers” class in the code below) to make this easy.
當主要activity被建立後,應該要檢查並確保是否有可用的擴充檔。downloader函式庫提供了一個helper功能(範例程式中被命名為"Helper"類,下方的程式中也可看到),讓開發者方便使用。

boolean expansionFilesDelivered() {
 // get filename where main == true and version == 3
String fileName = Helpers.getExpansionAPKFileName(this, true, 3);
// does the file exist with FILE_SIZE?
      if (!Helpers.doesFileExist(this, fileName, FILE_SIZE, false))
                return false;
        }
        return true;
    }
}






















If the file does not exist, fire up the downloader service with  DownloaderClientMarshaller.startDownloadServiceIfRequired(). The downloader will perform an LVL check against the server. This check will deliver the names of the files, file sizes, and the file URLs.
如果擴充檔沒有放在SD卡的指定位置,可以使用DownloaderClientMarshaller.startDownloadServiceIfRequired()來啟動downloader服務。downloader會針對server執行LVL檢驗,檢驗會傳遞檔案名稱、檔案大小和檔案的URLs。

Once that check has been completed, it will begin downloading the files. You don’t have to use our download solution, but you might want to because we:
一旦檢查機制完成,就會開始下載檔案。也許你不需要使用我們提供的下載方案,但是您可能還是會用到,因為我們提供了︰
  • Include a notification UI that provides progress and estimated completion time in layouts customized for ICS and pre-ICS devices
     訊息通知介面,能在ICS和ICS版本之前的裝置提供了進度條和下載時間估算。
  • Resume large files safely
    提供安全性的續傳檔案。
  • Handle redirection with appropriate limits
    重新定位apk的適量限制。
  • Run in the background as a service
    提供背景服務下載。
  • Pause and resume downloads when WiFi is not available
    當WiFi失效時,提供暫停和續傳的功能。
Enjoy! We can’t wait to see what kinds of things developers do with this! For more information about how to use expansion files with your app, read the APK Expansion Files developer guide.
好好享用吧!我們迫不及待想看到開發者使用這些功能!更多擴充檔的資訊,請閱讀開發者導引的APK Expansion Files單元。

[This post wasn’t actually written by anyone, but bashed out by a posse of engineering and product-management people. Heavy bashers included Dan Galpin, Ilya Firman, Andy Stadler, Michael Siliski, and Ellie Powers.]