Thursday, August 18, 2011

Test in-app purchase測試你的內部應用程式購買機制

在使用測試in-app purchase時,
有以下幾點需注意︰
1.官方強調︰測試in-app purchase(以下簡稱iap)請勿使用模擬器!
2.如果你要測試的是預保留產品ID(reserved product IDs),那就不需要使用測試帳號。如︰android.test.purchased這組預設ID是讓你拿來測試購買後會發生哪些回傳值。
3.確認你裝置上的Android Market版本是否適合運行iap


真實測試你的iap物品
1.需要在軟體發佈網站裡記錄一組測試帳號

2.上傳的apk不需要被發佈,但是iap內的物品必須被發佈!
注意︰Android Market上面這個未發佈的apk的版本編號以及簽章(Signature)需以裝置上的版本一致
3.購買時帳目會真實被記進tester的Google checkout裡,
之後user跟開發者提出退費請求(可能寄e-mail或什麼的),
開發者就可以在Google Checkout Merchant account使用退費機制(refund)。

此時Google checkout會寄出該user已經退費的訊息給Android Market,
Android Market再寄出訊息給user手上的應用程式。
如果你的應用程式當初有寫收到退費的訊息時要做什麼處置,
那麼程式就會在這個時候開始動作。
譬如︰沒收紅藥水、魔法藥水。


4.請用釋出版本的signature,而非debug的signature
5.將一臺裝置的帳號設成測試帳號

  1. Open Settings on your device.打開裝置裡的設定
  2. Touch Privacy.點擊隱私設定
  3. Touch Factory data reset.點擊重設為原廠設定
  4. Touch Reset phone.點擊重設手機按鈕
Note: The only way to change the primary account on a device is to do a factory reset, making sure you log on with your primary account first.
將測試帳號設為主帳號的唯一方式,就是使用重設為原廠設定。
這樣做才能確保你是用主帳號登入Android Market

========實測結果=========
1.當你真的去買了iap內的東西,測試帳號裡的Google Checkout裡會有消費記錄
2.若馬上移除掉apk,這筆iap消費記錄也會馬上被退款(未免也太迅速了吧⋯)
3.若你要重新購買時,Android Market的iap視窗會回應你︰系統正在處理您的訂單,稍候幾分鐘即可取得您的商品。

4.後來,過了40分鐘,才能又購買這筆iap的產品

Saturday, August 13, 2011

in-app purchase匯入步驟

以下文章從Billing Android- implementing in-app billing取材整理


1.將Billing Sample src裡的com.android.vending.billing複製到你的專案裡
2.在AndroidManifest.xml裡的user-permission添加
com.android.vending.BILLING
註︰如果沒有宣告就請求billing機制時,將會收到
RESULT_DEVELOPER_ERRO反應

3. 建立BroadcastReceiver和Service(見步驟4)

BroadcastReceiver - 用來處理從Android Market傳來的Billing訊息 
Service - 綁剛才從billing sample src複製過來的IMarketBillingService.aidl

哦!對了。AndroidManifest.xml裡的BroadcastReceiver要設 intent filters 

      
        
        
        
      
    
4.攢寫Service
這個Service將要做哪些事?
(1)綁定IMarketBillingService.aidl
(2)向Android Market發佈Billing請求
  • CHECK_BILLING_SUPPORTED requests
  • REQUEST_PURCHASE requests
  • GET_PURCHASE_INFORMATION requests
  • CONFIRM_NOTIFICATIONS requests
  • RESTORE_TRANSACTIONS requests
(3)同步處理以上這些請求的回應


============================
(1)綁定IMarketBillingService.aidl詳細說明
  1. 建立一個Service並實作serviceConnection,然後將底下的Code放進OnCreate()
  2. try {
      boolean bindResult = mContext.bindService(
        new Intent("com.android.vending.billing.MarketBillingService.BIND"), this,
        Context.BIND_AUTO_CREATE);
      if (bindResult) {
        Log.i(TAG, "Service bind successful.");
      } else {
        Log.e(TAG, "Could not bind to the MarketBillingService.");
      }
    } catch (SecurityException e) {
      Log.e(TAG, "Security exception: " + e);
    }
     
  3.  建立callback(將以下程式碼貼進自創的Service裡)
  4. /**
      * The Android system calls this when we are connected to the MarketBillingService.
      */
      public void onServiceConnected(ComponentName name, IBinder service) {
        Log.i(TAG, "MarketBillingService connected.");
        mService = IMarketBillingService.Stub.asInterface(service);
      }
     
  5.  可以開始用mService去 sendBillingRequest()任何需求了!
 記得將developer發佈頁面裡的公開金鑰放進Security.java裡的base64EncodedPublicKey變數中

    Monday, August 8, 2011

    台灣購買Android軟體

    由於臺北市政府跟Google因軟體7天退費乙事正面交鋒中,
    但是還是有很多網友很支持開發者,
    想花錢去購買軟體。

    CJAY部落客有教我們怎麼將自已虛擬成外國用戶來購買Android Market的軟體,
    有興趣的看一看吧!

    步驟應該不會太難才是!

    2011/08/13註︰
    Market Enabler雖然能讓USER購買付費軟體,

    1.裝置需要ROOT
    2.仍然不能支援IN-APP-PURCHASE

    實測後只有依照CJAY教的方式,
    才能實現在台灣使用in-app-purchase付費

    2011/08/25 11:00註︰
    現在又變成Market Enabler可以使用IN-APP-PURCHASE,
    而CJAY的則不行⋯

    2011/08/25 15:50註︰
    現在Market Enalber和CJ的vpn都能用IN-APP-PURCHASE了

    天啊,台北市政府,還我乾淨的測試空間!
    話又說回來,為什麼要在這個時間點要求我做IN-APP-PURCHASE
    (踢)

    Monday, August 1, 2011

    Android proguard issue - Can't read [proguard.ClassPathEntry@1a0c10f] (No such file or directory)

    If you encounter the problem that can't read something when you proguard your program
    as title write:
    java.io.IOException: Can't read [proguard.ClassPathEntry@1a0c10f] (No such file or directory)

    There will be two things you have to do:

    1.Download the newest ProGuard version from official website, unzip all its' file in sub-directory and replace to your-andoird-sdk\tools\proguard\

    It'll appear the exactly path where you got issue.

    2.Export project again,
    Now it'll show the error is:
    java.io.IOException: Can't read [/Users/simon/Android/?q?HSDK/FET_SMarket_SDK/FET_APP_Store_SDK_2.3.0.jar] (No such file or directory)

    3.When I back to that directory, ?q?HSDK isn't my directory's name, the name actually is "電信SDK".
    Now, I got the answer that I can't use Chinese in my path, Otherwise I will get this problem.

    原來讓我卡住一天,不能發佈並混淆程式的原因是︰
    我用了中文目錄成為路徑,
    ProGuard不能辨認中文!

    上一篇︰

    關於程式碼混淆proguard

    相關文章︰

    Sunday, July 31, 2011

    Android反組譯 - Reverse engineer

    以下文章轉載自此部落格

    先前聽說許多關於 Android 程式很容易被逆向工程,我也抱持這樣的觀點去戳 Android 開發者 XDD 直到今天開完會,前輩跟我說 Android 產生出的 dex file 很難閱讀,因此我就去找一下相關資料,找到一篇 2007/11/28 How to decompile .dex file on Android 文章,上頭也是敘述要去解讀 dex 有點不好讀,但隨後找到 2009/08/08 decompiling DEX into Java sourcecode 這篇文章,細看一下原來還有 dex2jar 的方式!這下可好了...
    所以,咱們來試試吧!我在 Ubuntu 10.04 的環境下測試
    準備工具:
    • dex2jar
      • A tool for converting Android's .dex format to Java's .class format
    • JD-GUI
      • JD-GUI is a standalone graphical utility that displays Java source codes of “.class” files. 
    • 一份編譯好的 apk 檔案
    接著就把 dex2jar 解壓縮出來,並執行
    $ cd dex2jar-0.0.7.7-SNAPSHOT
    $ sh dex2jar.sh MyWidget.apk
    然後就會看到一個對應的 jar 檔:MyWidget.apk.dex2jar.jar
    解壓縮 JD-GUI 去開啟 MyWidget.apk.dex2jar.jar 檔案,不囉唆,程式碼就可以看了!整個過程不用 30 秒吧!打快一點可能 3 秒喔 XD

    這...還真的叫我難以投入 Android 開發啊 *誤* 不過仔細一看,之前在 MyWidget 寫的程式,有些地方式用 R.layout.main 的地方,已經直接被換成數值了,但我想這樣也不會太難閱讀,畢竟整個 source code 都丟在你眼前了!更別提 dex2jar 仍然很活躍地進行維護及開發,真的開感謝一下對岸,第一次看到簡體中文有那種莫名的感動 XD 太多東西都嘛是英文先衝啦
    至於 dex2jar 的原理,可以參考這邊:介绍dex2jar的核心思想,有原理才會有相對應的解法喔!
    順便筆記一下 iPhone 的:
    arm-elf-objcopy -I binary -O elf32-littlearm iPhoneApp out.elf && arm-elf-objdump -marm9 -D out.elf
    但組語嘛,雖然學過,但我想我應該看不下去 XD
    最後,關於逆向工程的事情,如果程式碼非常冗長,那一樣不好啃啊!所以,玩玩就好。

    以上文章轉載自此部落格

    關於程式碼混淆proguard

    在了解程式碼混淆前(官方翻譯成模糊程式碼),
    建議可以先去觀看漁郎的教學


    在了解程式碼混淆前,
    也許你會問我︰「為什麼我要學程式碼混淆?」
    很簡單,答案如下︰
    1.讓使用反編譯工具想看你程式碼的人,閱讀你的程式碼更困難
    2.優化你的程式,節省空間

    如果你的開發版本在Android2.3以前
    那麼Proguard的使用方式是”手動”混亂你的程式碼
    請在你的專案目錄底下,打上︰
    android update project -p E:\_Eclipse-3.5.2\workspace\Wallpaper -t 8
    或在那個專案目錄下鍵入android update project -p ./ -t android-8
    其中-t 8指的是即將編譯的Android版本編號
    8代表要編譯成Goole Map + Android2.2以上用的Application
    android-8 代表要編譯成Android2.2以上用的Application
    灰色字體取決於你是否在當下目錄,
    若在當下目錄使用proguard,那麼灰色字體就是打上「./」
    當完這行指令後,
    你的螢幕會變成以下︰
    意思是説,己經在這個專案底下加入build.xml檔和proguard.cfg檔了

    回到Eclipse環境中,對你的專案按下F5重整,
    你就會看到這2個檔案。

    在Eclipse專案下default.properties裡面
    應該要有這2行︰
    target=android-8(代表在2.2執行)
    proguard.config=proguard.cfg(指定混淆腳本是被放在這個專案的根目錄)

    如果build.xml出錯,
    回報我找不到help文檔,
    那麼把xml標籤的defualt="help"刪了吧。

    此時將application export出來,
    應該會多一個proguard目錄才對。
    如果做完apk後,
    按了F5這個目錄都沒出來,
    代表你並沒有混淆成功。

    註︰
    -t 8 不可能打成 -t 9,
    為什麼呢?
    Android在2.3後(版本代號9),
    正式將ProGuard納入了SDK,
    所以如果你是在2.3環境開發時,
    不用再打上面那行指令。
    現在這行指令只會在2.2(含)以下需要了。

    更詳細的內容,
    請參閱於Android2.3推出的Proguard文檔

    下一篇︰

    無法混淆的相關問題

    相關文章︰

    作品 - 管理學決策模式

    花了3個星期,
    這隻程式終於在蹺課數次,
    下班後熬夜數天,
    沒日沒夜的趕出來了。

    謝謝Camangi Janis當初在公司時,
    不藏私的教同仁管理學的決策模式。

    這讓我在人生的幾個重要決策中,
    因為這些管理學者的技術,
    我的每一個決定,
    都不再是隨便的做出來,
    而是經過條理分析。

    話不多說,
    請參考教學影片

    請至Android Market下載︰
    管理學決策模式

    或使用QRCode搜尋

    Saturday, July 30, 2011

    分享才是王道︰從你的APK發佈Facebook訊息!

    這個部落格的存在,
    主要是因為小鰻本人記憶力非常不好,
    我必須要有一個工具記住我以前學過的東西,
    哈,離題。
    今天要講如何讓你的APK
    擁有發佈Facebook訊息的功能


    底下列了大概要做哪些事的步驟︰

    一、Facebook網站端
    1.在Facebook註冊開發者身份
    2.在Facebook開發者頁面新增應用程式
    3.輸入Key Hash


    二、Eclipse端
    1.下載Facebook源碼
    2.創建Facebook的基本SDK


    三、Code端
    1.將Facebook Library套入到自己的專案
    2.將manifest.xml加上Internet permission
    3.攢寫並使用Facebook Code




    ==========正文開始===============


    一、Facebook網站端


    1.在Facebook註冊開發者身份
    我們必須跟 Facebook註冊成為開發者的身份,
    之後才有權利跟Facebook伺服器做溝通。


    2.在Facebook開發者頁面新增應用程式
    要利用Facebook API跟Facebook伺服器溝通前,
    我們必須先創立專屬的應用程式
    因為等等我們寫Code呼叫API時,
    會需要我們這裡申請到的應用程式ID




    3.輸入Key Hash
    官方文件這麼說明︰
    We now need to export the signature for your app so that Facebook can use to ensure users are only communicating with your app on the Android. This is done by running the keytool.
    原來Key Hash是Facebook和我們的應用程式做溝通的橋樑。



    怎麼生成這個Key Hash呢?
    首先,我們必須搞懂Android Signature這個東西,
    由於篇幅問題,這裡跳過,
    你可以去參考Android Guide裡的資料
    但我可以跟你說,
    一般我們的開發過程,
    會有2種Signature,
    一種是Debug用的Signature,
    另一種是真正要發佈程式至Android Market用的正式Signature。

    在這裡,我先示範抓出Debug Signature的方法,
    如果之後要用發佈的Signature,
    記得底下這步要做啊!


    (1)開啟Terminal終端機,輸入


    註︰如果你是用Windows,請在Mysgit產生的Git Bash視窗裡輸入上面這行指令

    要怎麼查你的debug.keystore在哪裡呢?
    在Eclipse中,點選[Windows]-->[Preferences]
    出現底下的視窗
    沒有Keytool指令?
    Keytools在C:\Program Files\Java\jdk1.6.0_26\bin\裡
    拜託環境變數設好再來。


    沒有openssl?
    我在Ubuntu和Mac下,因為是Linux環境,
    只要apt install openssl就好了,很快
    但Windows下openssl要怎麼匯入,
     請知道的人提供一下,謝謝
    註︰Android真的不適合在Windows環境開發,
    你會發現很多東西都要用到Linux


    總之,剛才那段keytool指令,
    在terminal裡應該要能生成出Key Hash。
    示意圖如下︰




    (2)將生成的值複製並貼入Facebook的Key Hash



    二、Eclipse端

    1.下載Facebook源碼
    請使用Git的方式,將Facebook源碼下載下來
    Windows用msygit
    指令為
    git clone http://github.com/facebook/facebook-android-sdk.git


    2.創建Facebook的基本SDK
    我們現在要將剛才git clone下來的源碼新增進Eclipse裡。

    在Eclipse工具列[File]-->[New]-->[Android Project]裡,
    從一個已存在的目錄去新建專案,
    而路徑請導到剛才clone下來的目錄底下的facebook目錄


     此外,你可以按照官方文件的教學,
    同樣方式導入另一個資料夾的Sample Application,
    去看看官方教你的API使用方式。
    當然,我接下來的教學裡也會說明。
    This library includes two sample applications to guide you in development.
    • simple: A bare-bones app that demonstrates authorization, making API calls, and invoking a dialog.
    • stream: This slightly beefier application lets you view your news feed.
    To install a sample application into Eclipse (3.5):
    • Create the sample application in your workspace:
    • Select File -> New -> Project, choose Android Project, and then click Next.
      1. Select "Create project from existing source".
      2. Choose either examples/simple or examples/stream. You should see the project properties populated.
      3. Click Finish to continue.
    • Build the project: from the Project menu, select "Build Project".    

    三、Code端
      1. 將Facebook Library套入到自己的專案
      (1)點擊[Project]-->[Propertites]


      底下那裡應該有打勾

      (2)看一下Package Explore,Facebook是不是順利匯進來了呢?




      2.將manifest.xml加上Internet permission


      耶!可以開始寫Code了


      3.攢寫並使用Facebook Code
      將FB專案成功的匯入我們的正在開發的專案夾後,
      剩下的就是如何攢寫Code去呼叫Facebook API了。
      Facebook的API有新(Graph API )舊(Legacy REST API:)兩種版本,
      官方已經不建議我們用舊的了。


      底下會以Graph API做示範。


      因為Facebok支援Single-Sign-On(SSO),
      所以如果使用者在用我們APK的當下有登入Facebok的話,
      基本上就不需要重複登入了。


      在攢寫程式碼的一開始,
      還記得我們教學一開始要求在Facebook Developer裡新增應用程式裡的應用程式ID嗎?
      這裡就要開始用到了。
      package com.greatap;
      
      import android.app.Activity;
      import android.content.Intent;
      import android.os.Bundle;
      import com.facebook.android.*;
      import com.facebook.android.Facebook.*;
      
      public class MyGreatActivity extends Activity {
      
          Facebook facebook = new Facebook("你的應用程式ID");
      
          @Override
          public void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.main);
                 //跟Facebook伺服器要求User可以透過我們的程式跟Facebook做
                 //發文的動作。
              facebook.authorize(this, new String[] { "email", "publish_stream" },
                 //跟伺服器申請我們要能發文和用e-mail,
                 //這裡可以任我們新增修改
              new DialogListener() {
                   @Override
                    public void onComplete(Bundle values) {
               //當認證成功後我們當然是要發文,
               //發文有2種模式,
               //一種用post或get,
               //另一種用Dialog的模式,
               //Dialog模式會呼叫"即將會發什麼文章至Facebook"的視窗給使用者預覽
                try {     
                     Bundle parameters = new Bundle();
      
                    //使用者想說的話 
                     parameters.putString("message", "可以預寫使用者要發的黑體文字");
                     //連結所想呈現的文字
                     parameters.putString("name", "連結裡所以呈現的文字");
                     //連結網址
                     parameters.putString("link","https://market.android.com/details?id=com.hardworking.managingdecision_lite&feature=search_result");
                    //灰色註解
                     parameters.putString("description","在灰色註解區塊想寫些什麼?");
                  //使用Dialog的方式發文
                 facebook.dialog(Result.this, "feed", parameters, new DialogListener(){
      
             @Override
             public void onComplete(Bundle values) {
              // TODO Auto-generated method stub
              
             }
      
             @Override
             public void onFacebookError(FacebookError e) {
              // TODO Auto-generated method stub
              
             }
      
             @Override
             public void onError(DialogError e) {
              // TODO Auto-generated method stub
              
             }
      
             @Override
             public void onCancel() {
              // TODO Auto-generated method stub
              
             }
                      
                     });
                    
      }
      
                 @Override
                 public void onFacebookError(FacebookError error) {}
      
                 @Override
                 public void onError(DialogError e) {}
      
                 @Override
                 public void onCancel() {}
            }
      );
            
      
          @Override
          public void onActivityResult(int requestCode, int resultCode, Intent data) {
              super.onActivityResult(requestCode, resultCode, data);
      
              facebook.authorizeCallback(requestCode, resultCode, data);
          }
      }
       


      認證那段的程式碼facebook.authorize會讓使用者出現底下的畫面


      寫到這裡,這樣子你會了嗎?
      對了,更多的Facebook API指令,
      都可以在這裡查到哦!

      Thursday, July 21, 2011

      ListView滑動時背景變黑

      當我們將ListView背景設定成一個我們想要的圖案時,
      在上下滑動時,往往背景會變成黑色,
      顯然這不是我們想要的畫面。 

      解決的方法︰
      在Layout的ListView標籤裡加入
      android:cacheColorHint="#00000000" 
      的屬性。

      Wednesday, July 20, 2011

      Android in-app Purchase 應用程式內結帳(筆記暫停更新)

      2011-08
      本然想既然台灣目前不能上架付費版apk,
      那麼就來改走in-app purchase路線,
      結果⋯

      Before you get started with in-app billing, be sure to review the following requirements and limitations.
      在你開始使用內部app金流時,請確定已經看過以下的限制和需求
      • In-app billing can be implemented only in applications that you publish through Android Market.內部app金流只能透過Android Market實作並且發佈
      • You must have a Google Checkout Merchant account to use Android Market In-app Billing.你必須有一組Google Checkout帳號
      • If your device is running Android 3.0, in-app billing requires version 5.0.12 (or higher) of the MyApps application. If your device is running any other version of Android, in-app billing requires version 2.3.4 (or higher) of the Android Market application.如果你的裝置是運行Android 3.0的話,內部app金流需要MyApps這隻程式版本5.0.12(或更高)。如果你的裝置是運行其它版本,那麼需要Android Market版本在2.3.4(或更高)
      • An application can use in-app billing only if the device is running Android 1.6 (API level 4) or higher.能使用內部app金流機制的程式必須是在Android1.6(或更高)。
      • You can use in-app billing to sell only digital content. You cannot use in-app billing to sell physical goods, personal services, or anything that requires physical delivery.
      • Android Market does not provide any form of content delivery. You are responsible for delivering the digital content that you sell in your applications.
      • You cannot implement in-app billing on a device that never connects to the network. To complete in-app purchase requests, a device must be able to access the Android Market server over the network. 
      研究了一下手上幾臺測試機的Android Market版本號︰
      手機型號 系統版本 Android Market版本
      Motorola X720 2.1 2002306
      Nexus One 2.3.4 2.3.6
      Nexus S 2.3.4 2.3.6
      HTC Hero 2.1 2002306
      HTC Incredible 2.3.3 2.3.4
      HTC Legend 2.1 2002306

      有發現什麼事嗎?XD

      =====稍微做一下筆記(底下會持續更新)======
      1.必須在permission加入com.android.vending.BILLING
      如果沒加入,publish頁面會無法新增in app product

      2.需要申請測試帳號
      3. 應用程式不需要被發佈,但in app product一定要發佈才能測
      4. 發佈時請用Release版的signature
      5. 測試程式的版本號需與market這個測試apk版本一致
      6.一定要用實機測試billing,不能用模擬器
      7.實機的帳戶請設成測試帳戶
      設定方法︰
      1. Open Settings on your device.
      2. Touch Privacy.
      3. Touch Factory data reset.
      4. Touch Reset phone.
      5. After the phone resets, be sure to sign in with one of your test accounts during the device setup process.
      8.確認實機的Android Market版本編號是否在2.3.4以上;
      如果是平板Android 3.0以上請確認是否有MyApps這隻程式且版本在5.0.12以上。
      =========================
      In-app Billing Overview頁面重點︰
      1. In-app purchase使用asynchronous message loop
      2. 我們的應用程式不會和Android Market直接互動,而是透過發送請求的方式。


      一個典型的in-app purchase仰賴以下三大元件︰
      1. Service - (範例程式裡的BillingService),負責處理從程式裡發送出來到Android Market in-app billing Service的購買訊息、
      2. BroadcastReceiver - (範例程式裡的為BillingReceiver),負責接收從Android Market來的所有非同步(asynchronous)交易回應。
      3. Security - (範例程式裡的為Security),負責執行安全相關的工作任務。像是數位簽章驗證(signature verification)以及nonce generation(?)

      也許你還會併入另外兩個元件來處理in-app purchase︰
      1. Handler - (範例程式裡的為ResponseHandler),處理購買通買通知訊息、錯誤、或其它的狀態訊息
      2. observer - (範例程式裡的為PurchaseObserver),負責發送callback到我們的應用程式,因此我們能夠更新我們的程式內的購買資訊畫面。
      商品型態分為二種︰
      1. 經由使用者帳戶管理 - 若商品選擇該方式,那麼購買資訊等⋯,將會被儲存於Android Market後臺。這種性質的商品,若使用者換裝置,並重新安裝你的軟體,該購買都會保存。
      2. 不管理 - 不會將購買資訊儲存於Android Market後臺。舉例來說︰遊戲裡的魔法藥水。

      處理複雜的in-app pruchase交易流程
       上圖由上至下是整個應用程式內結帳的流程順序。
      一旦你使用到了該機制,
      整體的程式碼互動性就和這個架構開始息息相關。
      官方文件提到,即使你沒有呼叫REQUEST PURCHASE去呼叫出購買視窗,
      我們都一定要處理IN_APP_NOTIFY這個broadcast receiver(參閱In-app Billing Overview下方文章)。
      為什麼呢?
      1. 因為Android Market可能會傳來讓程式內部交易的記錄,我們必須要處理
      2. 使用者可能多機一帳號,我們要將該資訊連結至新裝置的程式裡

      Android Market也會藉由IN_APP_NOTIFY通知程式該in-app purchase已退費的事情。
      退費狀態會尾隨著PURCHASE_STATE_CHANGED這個BroadCast Receiver以JSON的方式傳進來︰purchaseState值回傳2(表示已退費)


      JASON資料被綁在PURCHASE_STATE_CHANGED裡傳進來

      詳細內文請看Android Document

      Thursday, June 30, 2011

      無法讀到指定解析度的目錄的問題

      今天在開發時遇到當我希望程式去依手機規格︰hdpi去跑drawable-hdpi的目錄時,
      程式卻不理我們而老是讀取drawable-mdpi目錄圖檔的問題。

      其實這個問題發生在這裡︰
      請參閱Android SDK文件<supports-screens>
      android:anyDensity
      Indicates whether the application includes resources to accommodate any screen density. Older applications (before API Level 4) are assumed unable to accomodate all densities and this is "false" by default. If the application has set either minSdkVersion or targetSdkVersion to "4" or higher, the default value for this is "true". Otherwise, it is "false". You can explicitly supply your abilities here. Based on the "standard" device screen density (medium dpi), the Android framework will scale down application assets by a factor of 0.75 (low dpi screens) or scale them up by a factor of 1.5 (high dpi screens), when you don't provide alternative resources for a specifc screen density. The screen density is expressed as dots-per-inch (dpi). Android在新開一個專案時, Manifest裡的minSdkVersion預設為2, 難怪我當時一直無法讓程式去跑drawable-hdpi解析度的drawable目錄

      Friday, May 27, 2011

      運行CTS時的問題收集

      韌體在撰寫後,
      為了檢測是不是符合Google的要求,
      通常都會在交給Google認證前,
      做CTS(Compatibility Test Suite︰兼容度測試套件)測試。

      在CTS測試前,硬體製造商須先遵從CDD(Compatibility Definition Documents︰兼容度定義文件),
      然後再做CTS測試。
      如果根本沒有符合CDD,那麼做再多的CTS,也完全沒用的(過不了Google認證)!
      詳細的內容都可以在這裡看到。

      目前我遇過的問題有︰
      1.在cts_host >start --Plan Android 時,
      遇到了ShellCommandUnresponsiveException。
      詳細內容如下︰
      Exception in thread "Thread-5" com.android.ddmlib.ShellCommandUnresponsiveException
          at com.android.ddmlib.AdbHelper.executeRemoteCommand(AdbHelper.java:408)
          at com.android.ddmlib.Device.executeShellCommand(Device.java:276)
          at com.android.cts.TestDevice$1.run(TestDevice.java:1718)

      這個原因是SDK版本本身的BUG造成的,換個SDK版本即可。

      Tuesday, May 24, 2011

      Build Android OS

      Android的骨子裡是Linux Kernel(以Linux做為核心的系統),
      最近因為在研究Android的Launcher,
      左右滑動、軟體清單、Widget...
      這些都是Launcher底下的內容

      開始要學會make(製作)自己的Android System。

      主要原因是Launcher也是APK - 放在預設系統內的一隻APK,
      而Google釋出的Source Code裡,Launcher用了很多Android Library以外的系統原生類別(Class),
      所以必須make出一個Android系統來搭配Launcher,才看的出執行結果。

      在製作自己的Android OS時,
      如果遇到了︰
      You are attempting to build with the incorrect version
      of java.

      Your version is: java version "1.6.0_22".
      The correct version is: 1.5.



      原來是Android Froyo(含)以下在make時,要求使用JDK5,
      而非你現在可能在用的JDK6。
      所以必須將系統的OS切換成JDK5。

      指令為︰

      $ sudo update-java-alternatives -s java-1.5.0-sun


      剩下的文章等手邊案子差不多再繼續吧...XD

      Wednesday, April 13, 2011

      在你的Apk裡使用GoogleMap

      使用GoogleMap有幾個Google要我們遵守的,一旦沒遵守會發生超多問題︰

      1.SDK需使用含Google APIs [Android x.x]的,而非單純的 [Android x.x]

      因為我們使用Google API s[Android 2.2]  (而非只是[Android 2.2]) ,maps.jar會直接被匯入到APIs裡,而不用自己另外匯入maps.jar

      2.
      
       
      這行要宣告在AndroidManifest.xml的區塊裡,放在別的區塊都不行



      3.必須使用含有Google APIs的模擬器


      4.跟Google申請APIkey
      將手邊正在用的KeyStore取出MD5,跟Google申請專屬的Google Map金鑰。
      並且在你的layout.xml裡宣告。
      如︰
      
      

      Tuesday, April 5, 2011

      客製化你專屬的View - View與OnDraw()應用

      文章更新時間︰2013/06/04
      文章更新次數︰3

      一、前提

      話說人是貪婪的,
      這句話用在沉浸於3C的使用者來說,
      真是滿恰當的。

      一開始就以這段文字做開頭,
      跟今天的主題很有關係,
      一般初學者剛開始學的,
      大部份都是如何建立一個TextView、EditText、Button等等的元件。

      想要在我們的畫面上叫出一個TextView文字框,我們有以下2種方式︰
      1.在程式碼裡
      TextView text = new TextView(this);
      text.setText("test");
      我們的Layout.addView(text);  //將TextView新增至我們的Layout
      
      2.在xml裡宣告
      <TextView
          android:layout_width="fill_parent"
          android:layout_height="warp_content"
          android:text="test"/>
      
      於是,我們可以很順利的得到這個畫面

      但是這些基礎元件用久了,我們或使用者,
      開始想要搞東搞西,想要客製化

      以下是這篇分享文最後會呈現的樣子︰

      ㄜ...這不就是再加一行字,並且把字的大小和顏色改一改而已嗎?
      有什麼好說的呢?

      二、文章開始

      如果你這麼想,可就大錯特錯囉!
      快來看看我在layout資料夾裡面的main.xml放了些什麼︰
      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout
          xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res/com.test.testcustomize"
          android:orientation="vertical"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          >
      <TextView  
          android:layout_width="fill_parent" 
          android:layout_height="wrap_content" 
          android:text="@string/hello"
          />
      <com.test.testcustomize.LabelView
          android:layout_width="fill_parent"
          android:layout_height="wrap_content"
          app:text="Reddd" />
      </LinearLayout>
      
      有注意到嗎?多了
      xmlns:app="http://schemas.android.com/apk/res/com.test.testcustomize"
      
      app:text="Reddd" 
      
      這時候你會問︰
      app:text="Reddd"
      這個標籤怎麼來的?

      從這裡︰

      只要在res/values/裡新增attrs.xml這份文件,宣告我們自訂的屬性,
      然後在main.xml裡導入這份文件,
      再加上之後會提到LabelView.class的一些設定,
      就可以達到藍色字的效果了。

      也許你會說︰
      「天啊!
      為什麼我們要把一個原本可以很簡單設定大小和顏色就能做到的事,
      搞得那麼複雜?」
      這就是我今天要說的「客製化」。

      不知道你有沒有注意到,畫面中藍色的那行字,
      只在main.xml裡很簡單的宣告文字內容,
      app:text="Reddd"
      卻呈現了藍色、而且更大的字體。

      Android允許我們自訂View,
      呈現我們自己想呈現的基本樣式。

      技術文件說︰
      如果我們想要有自己的View,
      那我們就自己寫一個類並繼承View。
      所以在這份文件裡,我多寫了一個LabelView來繼承View。

      該類別Code如下(可快速跳過這段)︰
      package com.test.testcustomize;
      
      import android.content.Context;
      import android.content.res.TypedArray;
      import android.graphics.Canvas;
      import android.graphics.Paint;
      import android.util.AttributeSet;
      import android.util.Log;
      import android.view.View;
      
      public class LabelView extends View {
          private Paint mTextPaint;
          private String mText;
          private int mAscent;
          
          public LabelView(Context context) {
              super(context);
              initLabelView();
          }
          
          public LabelView(Context context, AttributeSet attrs) {
              super(context, attrs);
              initLabelView();
              TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
              CharSequence s = a.getString(R.styleable.LabelView_text);
              if(s!=null){
                  setText(s.toString());
              }
              setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF));
      
                  int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
                  if (textSize > 0) {
                      setTextSize(textSize);
                  }
              a.recycle();
          }
      
          private void initLabelView(){
              mTextPaint = new Paint();
              mTextPaint.setAntiAlias(true);
              mTextPaint.setTextSize(24);
              mTextPaint.setColor(0xFF000000);
              setPadding(3,3,3,3);
          }
      
          private void setText(String str){
              mText = str;
              requestLayout();
              invalidate();  //每呼叫一次就會重新繪圖onDraw()
          }
      
            /**
           * Sets the text size for this label
           * @param size Font size
           */
          public void setTextSize(int size) {
              mTextPaint.setTextSize(size);
              requestLayout();
              invalidate();  //每呼叫一次就會重新繪圖onDraw()
          }
      
          /**
           * Sets the text color for this label.
           * @param color ARGB value for the text
           */
          public void setTextColor(int color) {
              mTextPaint.setColor(color);
              invalidate(); //每呼叫一次就會重新繪圖onDraw()
          }
      
          /**
           * @see android.view.View#measure(int, int)
           */
          @Override
          protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
              setMeasuredDimension(measureWidth(widthMeasureSpec),
                      measureHeight(heightMeasureSpec));
          }
      
          /**
           * Determines the width of this view
           * @param measureSpec A measureSpec packed into an int
           * @return The width of the view, honoring constraints from measureSpec
           */
          private int measureWidth(int measureSpec) {
      
              int result = 0;
              int specMode = MeasureSpec.getMode(measureSpec);
              
              int specSize = MeasureSpec.getSize(measureSpec);
              
              if (specMode == MeasureSpec.EXACTLY) {
                  // We were told how big to be
                  result = specSize;
              } else {
                  // Measure the text
                  result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
                          + getPaddingRight();
                  if (specMode == MeasureSpec.AT_MOST) {
                      // Respect AT_MOST value if that was what is called for by measureSpec
                      result = Math.min(result, specSize);
                  }
              }
      
              return result;
          }
      
          /**
           * Determines the height of this view
           * @param measureSpec A measureSpec packed into an int
           * @return The height of the view, honoring constraints from measureSpec
           */
          private int measureHeight(int measureSpec) {
      
              int result = 0;
              int specMode = MeasureSpec.getMode(measureSpec);
              int specSize = MeasureSpec.getSize(measureSpec);
      
              mAscent = (int) mTextPaint.ascent();
              Log.i("tag", "Height Ascent: "+mAscent);
              if (specMode == MeasureSpec.EXACTLY) {
                  // We were told how big to be
                  result = specSize;
              } else {
                  // Measure the text (beware: ascent is a negative number)
                  result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
                          + getPaddingBottom();
                  Log.i("tag", "Height mTextPaint.descent(): "+mTextPaint.descent());
                  if (specMode == MeasureSpec.AT_MOST) {
                      // Respect AT_MOST value if that was what is called for by measureSpec
                      result = Math.min(result, specSize);
                  }
              }
              return result;
          }
      
          @Override
          protected void onDraw(Canvas canvas) {
              super.onDraw(canvas);        
              canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint);
          }
          
      }
      
      
      Code很長,請聽我一一道來。

      這段Code的基本骨架是這樣的︰
      1.一開始,宣告2個建構式︰
      //建構式1
          public LabelView(Context context) {
              super(context);
              initLabelView();  //設定文字的最初樣式
          }
      
          //建構式2    
          public LabelView(Context context, AttributeSet attrs) {
              super(context, attrs);
              initLabelView();  //設定文字的最初樣式
      
              TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
              CharSequence s = a.getString(R.styleable.LabelView_text);
              if(s!=null){
                  setText(s.toString());
              }
              setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF));
      
                  int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
                  if (textSize > 0) {
                      setTextSize(textSize);
                  }
      
              a.recycle();
          }
      
      技術文件裡提到,
      如果我們想對我們想呈現的View初始化,
      那麼我們就應該在建構式裡做基本的設定

      先不看第2段建構式的龐大內容,
      這2個建構式都有一個共通點︰
      都呼叫了
      initLabelView();
      該函式的內容如下︰
      private void initLabelView(){
              mTextPaint = new Paint();
              mTextPaint.setAntiAlias(true); //設定反鉅齒
              mTextPaint.setTextSize(24);    //設定預設大小為24
              mTextPaint.setColor(0xFF000000); //設定預設的顏色為黑字體
              setPadding(3,3,3,3);   //設定在版面上下左右各距離3
          }
      
      我們在輸入文字的一開始,
      馬上呼叫了Paint,
      並對這個我們自訂的View做了初始化.

      回到第2個建構式︰
      //建構式2    
          public LabelView(Context context, AttributeSet attrs) {
              super(context, attrs);
              initLabelView();
      
              TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
              CharSequence s = a.getString(R.styleable.LabelView_text);
              if(s!=null){
                  setText(s.toString());
              }
              setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF));
      
              int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
              if (textSize > 0) {
                   setTextSize(textSize);
              }
            
              a.recycle();
          }
      
      第6行︰將我們自己做的attrs.xml呼叫進來
      第7行︰取出我們在main.xml裡設定的字
      第8行︰如果有取到字,則呼叫setText()函式
      第11行︰設定文字最後會呈現的顏色,如果在main.xml裡面沒有設定的話,
      就用這裡的預設值︰0xFF0000FF,設為藍色
      第13行︰設定文字大小
      第18行︰讓我們自訂的attrs可以重覆被使用(因為以後也可能被用到啊!)

      Android在畫出一個View時,
      原理是這樣子的︰
      1.不斷的呼叫我們程式裡的onMeasure()函式去量測現在要畫的View的尺寸會有多大︰
      /**
           * @see android.view.View#measure(int, int)
           */
          @Override
          protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
              setMeasuredDimension(measureWidth(widthMeasureSpec),
                      measureHeight(heightMeasureSpec));
          }
      
      2.最後呼叫onDraw()繪圖

      當我在試驗時,程式為了畫出藍色字體,
      至少呼叫了3次的onMeasure()才呼叫onDraw()。

      所以我們得知︰
      系統在開始onDraw()前,
      會不斷的呼叫onMeasure()去跟Layout問現在要畫的藍色字體,
      範圍是多大。

      所以,我們就要將尺寸的算法在這裡交待一下了︰
      /**
           * 量測這個View的寬
           * @param measureSpec A measureSpec packed into an int
           * @return The width of the view, honoring constraints from measureSpec
           */
          private int measureWidth(int measureSpec) {
      
              int result = 0;
              int specMode = MeasureSpec.getMode(measureSpec);       
              int specSize = MeasureSpec.getSize(measureSpec);
              
              if (specMode == MeasureSpec.EXACTLY) {
                  // We were told how big to be
                  result = specSize;
              } else {
                  // Measure the text
                  result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
                          + getPaddingRight();
                  if (specMode == MeasureSpec.AT_MOST) {
                      // Respect AT_MOST value if that was what is called for by measureSpec
                      result = Math.min(result, specSize);
                  }
              }
      
              return result;
          }
      
      /**
           * 量測這個View的高
           * @param measureSpec A measureSpec packed into an int
           * @return The height of the view, honoring constraints from measureSpec
           */
          private int measureHeight(int measureSpec) {
      
              int result = 0;
              int specMode = MeasureSpec.getMode(measureSpec);
              int specSize = MeasureSpec.getSize(measureSpec);
      
              mAscent = (int) mTextPaint.ascent();
              Log.i("tag", "Height Ascent: "+mAscent);
              if (specMode == MeasureSpec.EXACTLY) {
                  // We were told how big to be
                  result = specSize;
              } else {
                  // Measure the text (beware: ascent is a negative number)
                  result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
                          + getPaddingBottom();
                  Log.i("tag", "Height mTextPaint.descent(): "+mTextPaint.descent());
                  if (specMode == MeasureSpec.AT_MOST) {
                      // Respect AT_MOST value if that was what is called for by measureSpec
                      result = Math.min(result, specSize);
                  }
              }
              return result;
          }
      
      第6行︰一開始系統傳進來的引數measureSpec是從main.xml裡抓過來的
      第9行︰在MeasureSpec.getMode()裡會抓到的尺寸模式,有以下3種
       Mode
       Value
       備註
      main.xml裡相對應的設定
      AT_MOST
       -2147483648
       子視圖可自行調整尺寸大小
      android:layout_width或height 設為"wrap_content"時
      EXACTLY     
       1073741824
       父Layout已經很明確的定義該子視圖的大小
      android:layout_width或height有自訂大小或為"fill_parent"時
      UNSPECIFIED
       0
       父Layout未指定大小,子視圖可自行調整。

      第10行︰你在main.xml裡真實設定的尺寸。

      幾次的onMeasure()呼叫完後,就跑去呼叫onDraw()開始繪圖了。

      @Override
          protected void onDraw(Canvas canvas) {
              super.onDraw(canvas);        
              canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint);
          }
      挪,結束了。

      三、結論

      1.在res/values/新增一個自訂的attrs.xml
      2.在main.xml裡使用他們
      3.繼承View,並在
      (1)建構式 裡宣告初始值
      (2)在onMeasure()裡交待清楚這個自創的View應該有多大
      (3)在onDraw()裡開始繪圖

      這樣子,你就能為所欲為,畫出任何你預設想呈現出來的View了。

      有點複雜,但搞懂了就簡單了。

      備註︰
      來源︰百度知道





      1.基準點是baseline
      2.ascent:是baseline之上至字符最高處的距離
      3.descent:是baseline之下至字符最低處的距離
      4.leading:是上一行字符的descent到下一行的ascent之間的距離,也就是相鄰行間的空白距離
      5.top:指的是最高字符到baseline的值,即ascent的最大值
      6.bottom:指最低字符到baseline的值,即descent的最大值

      Sunday, April 3, 2011

      Android從不會到會 - 一位從餐飲業跳到資訊業的心路歷程

      相信很多想要跳進Android開發世界的初學者在入門的階段都無從學起,而這條路我也是走的挺辛苦的。現在有了一點小小的心得,希望在這裡分享給剛入門的朋友們。

      我本來的職業是餐飲業的煮麵學徒兼副店,之所以會跳來這個領域
      1.臺灣餐飲業人員逐漸大陸化
      2.餐飲業越來越看不出我存在的優勢

      後來想一想,手上那臺Windows Mobile手機真難用,
      可是手機不應該僅止於此,於是跑去聯成學JAVA,也很確定要開發手機。

      在聯成遇到貴人好老師(Thanks for 顧老師Piano Man),辛苦了半年左右得了一張基本的SCJP證照,
      也很辛苦的進了一家公司開始,
      對我而言,才是戰場的開始。

      Android設計不用太深的JAVA理論,
      而Google在讓開發人員跳槽進來,也設定了很低的門檻。

      但對我而言,有一個很重要的障礙︰
      「英文很爛。」

      不知為何,對那隻Android機器人就是情有獨鍾,
      就是想開發Android。

      於是我開始從Gasolin的網路電子書下手。
      Gasolin的這個網站,讓我從英文很爛 + 對Android什麼都不懂,變成了開始懂什麼叫Android Activity的生命週期等等的Android基本概念。經由這個網站製作BMI量測的專案的教學,建立了我對Android的基本認識。

      註︰
      2011-09-23
      Gasolin其實有將這個電子書出成實體書,
      大家還是可以多多買實體書,
      最近他的寶寶剛誕生,
      養家不容易的!

      我跟你們說,在程式開發初期,你會遇到一堆超級奇怪的現象︰
      1.Eclipse怎麼用?我在補習班學的是NetBeans。


      2.什麼叫外掛?Eclipse怎麼掛Android都掛不起來?
      3.為什麼專案一匯入Eclipse就是出現紅色警嘆號或XX
      4.為什麼叫不出顏色
      5.為什麼就是不能安裝Android SDK

      總之,你會在剛踏入Android的初期遇到超多超多的問題,
      但是你都得努力的爬文、不計任何代價、用任何方法,
      都要去克服他們。
      我覺得因為有這些過程,才能磨練出一位程設師之後在開發過程遇到困難時的耐力。

      讀完了Gasolin的電子書後,接下來要如何下一步?
      我遇到了盧育聖Sam的網站
      Sam在帶領新人的一開始,
      希望我們打好JAVA的基礎,
      希望我們讀熟Android技術文件再來做開發。

      這種建議不否認絕對是個好建議,
      但對我這種英文很爛的人來說,
      簡直是災難一場

      這時候心裡想,
      真的該堅持下去嗎?
      這條路超辛苦的。

      我相信當一個人有心的時候,
      老天爺絕對不會給他絕路。
      此時,我發現到,
      有一個東西可以幫助英文很爛 + 對Android什麼都不懂的我,
      那就是Mars的視頻

      Mars的視頻是一個很棒的東西,
      他幫你把你本來應該從技術文件看到的基礎知識,
      用影像的方式一步一步帶你實作和講解,
      他講的內容對我來說都不會很難。
      (可能我當時手上已經買了6本左右的Android書了)

      我覺得在學習的過程中,
      有一點很重要,
      就是要有一個練習的環境 + 好的工具書。

      在一家Android環境的公司磨練 + 工具書,
      幫我慢慢的建立起"模仿"的能力。

      工具書我推薦一定要買SDK範例大全2

      這是一本幫什麼都不懂的你,
      開始學會模仿的書。

      註︰
      2011-09-23
      現在又有新版了︰SDK開發範例大全3

      所以,邊看書、邊打成Code、邊查英文、邊做專案、參與論譠(大陸最大的Android網站eoe),
      就成了我程式學習的起初所做的事。

      我從Mars的視頻裡,
      真的是驚嘆他能一看技術文件變馬上翻譯成中文的能力,
      我理解到如果Android技術如果要更上一層,
      你的英文底子一定要有。

      於是,下班後跑去補習班學英文、聽廣播、跟外國人聊天、看電影學英文…

      但是,這麼做並沒有幫我看懂技術文件,
      因為技術文件裡的字彙,
      只是英文的其中一個領域。
      在我學英文的範圍裡,是沒有學到這個領域的。

      苦了我,拚了那麼久英文文件仍看不懂。

      我又去找為什麼我看不懂技術文件的原因。

      1.我完全沒有文法概念,所以一個句子遇到倒裝或子句時,我看不懂其意。
      2.一堆生字,造成我一個字也看不下去。
      3.句子與句子之間的介系詞片語,如︰for a while, in particular,..我看不懂。

      於是又再報了一家補習班,專攻閱讀與文法這一塊。
      補了1、2個月,效果也慢慢出來了。

      但真正讓我理解我應該要怎麼面對外來語言的文章的,
      是呂英沖的書籍︰你用對方法學英語了嗎。


      我是在國家圖書總館無意間看到這本書的,
      書裡提倡我們一般在聽英文MP3時,
      都是先唸英文,再聽中文解釋。
      但這個方式錯了。
      因為英文是我們不熟的語言,
      當我們聽到  Apple 蘋果  時,
      對於外文完全不熟的我們,聽到Apple腦袋馬上打了個問號,
      後來聽到蘋果的中文解釋,才知道原來剛剛那個字是蘋果,
      此時,剛剛那個字怎麼發音你也忘了。
      所以呂先生說,先唸英文再唸中文的教材,是FOR老外閱讀的。
      我們應該先聽蘋果,再聽到Apple。否則會產生記憶斷點

      他又提到語言一定是先聽,後講,後來產生文字,最後才寫。
      所以,在我所要求的讀懂Android技術文件裡,
      我得先會聽和講英文。

      直覺告訴我呂先生心理+記憶學的經歷,能讓我在學英文的過程中,
      得到更多的啟發,
      於是又繼續往他的其他書籍追相關資料。

      我追到外全外文學習法。

      這本書提到我們是怎麼看懂一個句子的。

      我看到小明在沙發睡覺。

      1.看到 「我」這個字時,我們心裡會開始默唸「ㄨㄛˇ」,並跟大腦索取什麼叫「我」,
      於是你得到我就是「自己」的意思。
      然後,你將「我」這個字,存在你腦中的短暫記憶裡。
      2.依此類推,你一一的將「看到」、「小明」、「在」、「沙發」、「睡覺」這些字放進了短暫記憶。
      3.最後你看到了句點。
      4.大腦開始將你短暫記憶裡儲存的「我」、「看到」、「小明」、「在」、「沙發」、「睡覺」組出一個句意。於是你知道什麼叫「我看到小明在沙發睡覺」。

      在這個理解的過程中,如果你不知道什麼叫「看到」,
      或者如果你的短暫記憶裡,漏記了「看到」,
      而只記得「我」、「小明」、「在」、「沙發」、「睡覺」,
      那這個句意是出不來的。

      原來我們要看懂一個句子,我們經過了一個
      心裡默唸→懂詞意→記憶→組詞成句
      的過程。

      英文也是如此。
      An apple a day, keeps the doctor away.

      你是不是也用一樣的原理,在理解這個句子呢?

      搞懂了人類理解句子的原理,
      長期的廣播 + 補習 + 背Android技術字 + 真正去寫過程式碼產生了開發經驗
      我才終於慢慢看懂技術文件。

      謝謝你看完這篇文章,
      我相信你絕對是一位有耐心的人。

      為了得到一個目的,
      我付出了相當大的代價。

      這個過程中,
      我發現我不斷在做的事情是︰
      1.我缺什麼?
      2.該如何補足?
      3.方法對嗎?
      4.不斷檢視這3點。

      寫到這邊,我覺得這不僅用在學Android,
      也能用在學任何的東西上面。

      如果你真的有心,我相信沒有事能難的倒你的。
      一起加油!

      下一篇︰Android開發者應有的認知

      Monday, February 28, 2011

      記憶體超出OutOfMemoryError

      最近在開發上常常遇到
      java.lang.OutOfMemoryError: bitmap size exceeds VM budget,
      甚至有時候程式執行到一半時,
      程式會突然重開,甚至關閉電源。

      後來發現會出現這些狀況,大部份原因是因為記憶體用太兇了。
      在Android裡,每new出一個實體時、做了太多的指派(Reference),
      在heap裡都會占用記憶體。

      據我目前所知,大部份的Android硬體裝置,
      native heap都分配了16MB左右的空間,
      但是隨著每臺硬體配備的不同,真正能用的記憶體大小,
      又會低於16MB。
      一占超出了這個使用空間,就會很容易丟出OutOfMemoryError這個錯誤訊息。

      我目前遇到最常發生的時候就是在new一個圖檔Bitmap時,
      Android在載入Bitmap時,每個圖點的換算是很耗記憶體的,
      圖檔尺寸越大,所占的記憶體也越大
      Android在繪圖時,每一個圖檔的算法是︰

      圖檔的長*寬*圖檔類型

      假設我今天有一張圖

      該圖長︰177像素
      寬︰111像素
      圖檔類型︰RGB8888(每個8代表8bit,共有32bits=4Bytes)



      那麼,這張圖記憶體的占用大小就會是
      (177*111*4)/1024 = 76KB

      我真實去試過,把原圖縮小後,記憶體占用量也真的降低了,
      所以,去對圖檔做壓縮或者事先調整尺寸、甚至靈活的運用Bitmap類裡的recycle()這個回收函式,都是不錯的解決辦法。
      Bitmap的使用要小心。

      另外,我們常常會看到很多範例書裡,在寫ListView的Adapter時,
      Adapter裡的getView()函式中,
      常會有類似的以下片斷

      @Override
      public View getView(int position,View convertView, ViewGroup parent){
          ViewHolder holder;
          if(convertView == null){
           convertView = mInflater.inflate(R.layout.file_row, null);//告訴系統我們有自己畫的View
           holder = new ViewHolder();
           holder.text = (TextView) convertView.findViewById(R.id.text);
           holder.icon = (ImageView) convertView.findViewById(R.id.icon);
      
            convertView.setTag(holder);
          }else{
                  holder = (ViewHolder) convertView.getTag();
          }
      
               holder.text.setText("test");
                  .
                  .
                  .
            return convertView;
         }
      //用來存放每個格子的資料的實體類別
      private class ViewHolder{
               TextView text;
               ImageView icon;
      }
            
      

      ListView的每一個格子,都是Android系統呼叫getView()這個函式後,
      去畫出來的。
      getView()這個函式決定你每一格格子要畫成什麼鬼樣子(View)。

      當我們要開始畫ListView時,
      系統就先去看看convertView裡有沒有之前畫好的View,
      想當然,一開始畫時怎麼可能會有畫好的View呢?
      convertView當然會回傳null,
      於是程式開始跑convertView == null 這個判斷式
      告訴Android我們每一格ListView要畫成我們自訂的View: file_row(第5行)
      (file_row裡我畫了ImageView、TextView等等的組合。你要讓每一個格子變成什麼樣子,就寫成什麼樣子)

      而holder則是幫我們記住每個格子裡的資訊內容。

      像我手上這臺Tablet一次能畫出5個View,
      像剛才說的,系統會從convertView裡去找有沒有之前畫好的View,
      ok,程式一次抓了5個convertView,都呈現成null,也就是之前並沒有畫好並產生出來的實體View。

      於是,程式開始一口氣畫了5個View(並建立了5個holder實體,程式第6行)。
      當我們看到系統將ListView畫出了5個格子並呈現給你看後,
      使用者此時將手指往上滑動,ok,第1格、第2格、第3格格子陸續消失,
      第6格、第7格、第8格也準備要出現了。

      於是,Android又回去找convertView,
      找到之前畫好的View,也new出來的holder實體,
      於是,reuse,然後只是把holder裡面的內容改一改。

      我們就看到第6格、第7格、第8格...又是新的資料。
      但其實,那些實體是一直被reuse的。

      =================================
      好,有沒有想過這些View和實體為什麼要reuse?
      我曾經沒有寫這個判斷式
      if(convertView == null){
             .
             .
             .
            }else{
             .
             }
      
      而把每一個getView,都寫成新的View
      convertView = mInflater.inflate(R.layout.file_row, null);
           holder = new ViewHolder();
           holder.text = (TextView) convertView.findViewById(R.id.text);
           holder.icon = (ImageView) convertView.findViewById(R.id.icon);
      
            convertView.setTag(holder);
      
      也就是不斷new出實體和畫新的View。
      問題來了,
      這就是為什麼我會遇到記憶體超出的原因。

      原來Android系統,擁有著這種reuse機制。
      利用有限的資源,去做無限的事。

      相關文章︰
      1.使用MAT(記憶體分析)工具檢查Memory Leak
      2.Memory Leak經驗分享-Drawable篇
      3.何謂Memory Leak(記憶體洩漏)

      Sunday, January 23, 2011

      Android 電量控管和效能之路

      在android SDK的Dev guide裡︰

      Designing for Performance單元

      提到了很多攢寫程式碼需要考量到手機電量的問題
      其中有幾點是需要注意的。

      1.Avoid Creating Objects

      • When extracting strings from a set of input data, try to return a substring of the original data, instead of creating a copy. You will create a new String object, but it will share the char[] with the data. 叫我們少用New String而用StringBuilder
      • An array of ints is a much better than an array of Integers, but this also generalizes to the fact that two parallel arrays of ints are also a lot more efficient than an array of (int,int) objects. The same goes for any combination of primitive types.  最好避免二維陣列的使用,而使用多個一維陣列

      2.Prefer Static Over Virtual

      If you don't need to access an object's fields, make your method static. Invocations will be about 15%-20% faster. It's also good practice, because you can tell from the method signature that calling the method can't alter the object's state. 盡量使用Static method


      3.Avoid Internal Getters/Setters

      On Android, this is a bad idea. Virtual method calls are expensive, much more so than instance field lookups. It's reasonable to follow common object-oriented programming practices and have getters and setters in the public interface, but within a class you should always access fields directly.  有時候我們做Getter/Setter是為了OO,但是若在程式內部,就免了吧!

      4.Use Static Final For Constants

      如果有些field不會變,加上Final增加存取的速度!


      5.Use Enhanced For Loop Syntax

        public void two() {
              int sum = 0;
              for (Foo a : mArray) {
                  sum += a.mSplat;
              }
      
      使用超迴圈,能產生最大的效能。

      6.Avoid Enums Where You Only Need Ints

      you should consider static final int constants instead.   避免使用Enum。


      7.Use Floating-Point Judiciously

      As a rule of thumb, floating-point is about 2x slower than integer on Android devices. This is true on a FPU-less, JIT-less G1 and a Nexus One with an FPU and the JIT. (Of course, absolute speed difference between those two devices is about 10x for arithmetic operations.)  避免使用Float。


      8.Know And Use The Libraries


      9.Use Native Methods Judiciously



      Tuesday, January 4, 2011

      AlertDialog的ListView全白問題


      new AlertDialog.Builder(this)
      .setAdapter(new ArrayAdapter(getApplicationContext(), android.R.layout.select_dialog_item,
      new String[]{"1","2","3"}),
      new DialogInterface.OnClickListener(){}
      .show();
      中,若介面使用android.R.layout.simple_list_item_1,會出現螢幕全白
      所以必須使用紅字的介面