2010年12月28日 星期二

Android 開發筆記 - 關於反組譯 Android 程式,還真的不用 30 秒就搞定!

先前聽說許多關於 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


dex2jar2JD-GUI


這...還真的叫我難以投入 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


最後,關於逆向工程的事情,如果程式碼非常冗長,那一樣不好啃啊!所以,玩玩就好。


2010年12月26日 星期日

Mac mini 拆殼 + 更換記憶體

關於更換 Mac mini 裝備的事宜,可以參考很多網路上的資料,最好也看一些影片!這整個過程不是痛苦兩字可道盡 XDD 畢竟一台 Mac mini ,可是花了 19990 購買的 :P 心臟真的要大顆一點...


這個由來是因為最近 DDR3 1066 2GB 的記憶體一條才 650 元,所以心一橫就買了兩條,幫自己的 Mac mini 更新啦!在官方的 spec 中,敘述是最大支援到 4GB (兩條RAM),但有一些影片跟文章說明只需更新 EFI 可達到支援 8GB (一條插 4GB),思考了良久,覺得用不到 8GB 這麼多,再加上買兩條 4GB 的記憶體增加花費,故最後只敗了兩條 2GB 的金士頓囉!至於更換硬體設備,有錢的話可以去找蘋果店家,花錢請他們更換囉。這篇純粹個人記錄用,不見得適用於其他人 :P 請留意!


列一下參考資料:



回到主題,在此僅記錄個人的拆機過程,請先多看看其他人的拆機過程,少說看個三篇吧!我是 2010/04 買的 Mac mini ,現在已經算是舊版的,版本應該是 2009 年初版,比 2010 年中的版本還要厚喔。


基本工具


準備工具,我使用一隻水果刀 + 鐵尺,做為開路先鋒!用水果刀從細縫插入(記得刀鋒朝外),硬撐出可以讓鐵尺插入的空間,此後就把水果刀拔掉了。開殼方式類是上述影片 "How to open mac mini..." 裡頭的過程,但不一樣的是我使用寬度短小的鐵尺!所以十分不方便。請小心使用水果刀跟鐵尺,不要以為鐵尺不會傷人,在擠壓過程中,我被鐵尺割到了 :P 所幸只是很小的皮肉傷,也讓我之後移動鐵尺時都會包一張衛生紙。至於插入到真的把整個底盤弄出來,大概花了 15 分鐘吧 XD 一直不斷地"輕輕弄" ,最後被傷到後獸性大發,就比較敢用力!只是我用得是寬度小的鐵尺,必須在一旁移動和扳開,導致 Mac mini 的底部有明顯被磨擦的痕跡。


插入鐵尺+拔掉水果刀
先透過水果刀撐出較大的縫隙,再用鐵尺插入


右邊
終於把右邊都先敲開了,已經可以看到磨損的樣貌!原先想要把一根根"輕輕地"扳開,但搞到最後啥都不管,直接在中間移動嘗試,硬把鐵尺彎大力一點直到整個底盤被弄出來,這真的是經驗論!心臟要大顆啊。只要一邊被撬開後,其他兩邊都會很自然地弄好 XD


底部

左邊

快拆完
等到右、底、左都被撬開後,接著可以把他反過來,從面板那邊慢慢地扳開囉 !


拆完殼
直到最後殼與機子分開了,接著要把機子上方的板子移開,但移開前要先把左邊兩處跟右上方的土金色移開,好像是天線的樣子。左邊兩處就只要慢慢往上移就行了,右上方那個要先去把底部的卡榫輕壓,就會彈跳出來。


天線跟彈簧
左下位置的天線與彈簧,都要保存好


注意有條線被膠帶貼住
左下的天線有被膠帶黏起來,我是有把它輕輕撕開,把天線移開,這樣才方便把機殼上方移掉


底部的樣貌
機殼上方有一顆硬碟,除了剛剛提到的 3 個天線外,此殼還有 4 處的小螺絲要拆,以及與機殼連線的排線喔!等到這些都清光後,再慢慢地把殼子往上移開來,由於底部還有像 PCI 卡的連接方式,所以需要慢慢地往上,這些細節請多看一下影片。


更換完記憶體
終於移開機子上方的部分,可以看到記憶體的擺置位置,我也順手換掉兩條原裝的 SAMSUNG 記憶體啦!


最後再慢慢把東西還原,收工!而成果嘛,如下圖:
兩條記憶體 + 白色屑屑 底部嚴重損傷
兩條被替換的記憶體、Mac mini 的損傷以及一台被更新成 4GB 的 Mac mini 啦!線都接好後,可以先開機檢查(左上角的[蘋果]->[關於這台Mac]->[更多資訊]->查看記憶體是不是顯是 4GB 囉),再把機殼蓋上,畢竟機殼對我這個新手來說真的太難拆了。


買了新螢幕

工作之後,錢花在 3C 上頭越來越不手軟!記得大學時期,第一次打工的錢,就花了 1500 買一張 AGP 8X 的顯示卡,彌補了一下前一年組的主機,當時為了省錢只用了一張 PCI 顯卡撐著,所以賺到錢就買上想到顯卡的升級。


去年底,主機掛了後,我也花了快兩萬元在主機身上, 今年四月,花了快兩萬五買了 Mac mini 跟周邊對應產品(這台可不是年中轟動的標錯價的 Server 啊),幾天前又花了五千多,買了一台 22" LCD 和 2 條 1066 的筆電記憶體。看來,今年稱得上花大錢的年代!3C 這種產品,沒事還是不要去留意,不然就會像我一樣不停敗下去 :p


呼,最近也有一些態度要改變,像是 3C 產品就是用來享受用的,但我總會有那種壞念頭,好比如花錢買了主機,就勢必要給它賺回來,例如大學那張顯卡,正是從事網頁相關的工作內容等。雖然成果也算是多少有賺回來,但這樣的心態反而給自己更多無所謂的壓力感。我想,我該試著多享受一下購物,而不是逼著自己賺錢啦!


忘了一提,這次購物的理由是...因為 Mac mini 轉 D-Sub 時的輸出,在我之前那台 19" LCD 1440x900 上,最多只能選擇 1280x768 的解析度,對於我這種愛看 code 的人,這種高度有點吃力。而 2GB DDR3 1066 的筆電記憶體,其實也是要拿來擴充 Mac mini 啦,思考了很久,最後還是看在最近的低價敗了。


2010年12月24日 星期五

驗證 GTalk 使用明文/加密文傳遞聊天內容

我一直以為 GTalk 是加密傳送聊天訊息的,直到今早一位在火燙公司的學弟,請我跟他用 GTalk 聊天時,意外發現 GTalk 原來不是完全的加密聊天的。


情境敘述:


目前共有三台電腦



  1. 第一台是 Ubuntu 10.04 安裝 Pidgin 2.6.6 版本,上頭標示支援 GTalk SSL/TLS 加密傳輸,使用 A 帳號

  2. 第二台是 Windows XP,安裝 GTalk 官方繁體中文版 1.0.0.105,此版本不提供加密服務,使用 B 帳號

  3. 第三台是 Windows 7,安裝 GTalk 官方英文版 1.0.0.104,此版本提供加密服務,使用 C 帳號


測試結果:



  • 當 A 跟 B 進行聊天時,發現兩者傳遞的內容都是明文的 XML 資料,其中 <body> 內記錄的就是聊天內容,除此之外,當然連帳號資訊都是未加密的

  • 當 A 跟 C 進行聊天時,此時兩端會使用加密傳遞

  • 當 B 跟 C 進行聊天時,此時兩端皆會使用不加密傳遞,結果跟 A 與 B 一樣


因此驗證了一件事,不要以為自己的 IM client 標榜加密,就等於跟人溝通都會加密的!還是會看對方情況來處理,例如 B 跟 C 都已是官方的軟體,只是一個是繁體版,另一個是英文版,但繁體版不加密,導致英文版最後也配合繁體版進行不加密的通訊。


查了一些文章,這篇說的跟我測的差不多:正面全裸的Gtalk 1.0.0.105,慎用!


所謂的加密部分,只能確保 gtalk client 端連到 google server 端的過程,但從 google server 連到對方那端時,不見得會加密(例如對方透過 http 在 GMail 上用)。除此之外,這次測到的一個大問題是:


就算你自己的 gtalk client 可以提供加密連線,但對方不提供時,結果自己連到 google server 這端也就不走加密


呼,還是萬試謹言慎行啊!還有,快去用英文版就好!


最後附上一些圖示:


gtalk 加密
GTalk 使用加密時的封包訊息


gtalk未加密
GTalk不使用加密時的封包,其中 <body> 儲存的就是聊天訊息,此例為 "Hello" 訊息,除此之外連同通訊雙方的帳密資訊也是曝光的,並使用可讀性佳的 XML 描述


2010年12月18日 星期六

Windows 7 常用免費軟體

回老家,順便重灌一下電腦,神清氣爽啦,順便記一下最近常用的免費軟體:



工作類:



Firefox Plugin:



Google Chrome Plugin:



筆記:


XMarks 可以把書籤資料紀錄在 Server 上,該 Server 可以是自己架的,也可以直接用 XMarks 上面的。而多個瀏覽器的好處,我可以把工作上的同步在 Google Chrome 上頭,然後把家裡用的同步在 Firefox 上頭,如此一來也可以分得很乾淨,另一種用法也可以用目錄分辨工作或家用。另外,我在 Firefox 上僅使用書籤工具列,好處是看到不錯的網頁就直接拖拉分頁進去記錄,而書籤工作列上只要目錄分的好,就不會有顯示上的問題,只會有操作上的便利。


2010年12月16日 星期四

Android 開發筆記 - 簡易的 Widget 實作

MyWidgetShow


除了要從選單點選來執行的應用程式外,還有另一種呈現的方式,那就 Widget 模式,也就是在廣告單上常常看到簡介某大廠 Android 手機時,其手機畫面正顯示的天氣資訊,有點像似待機時顯示在背景的資訊。這種程式實作上並沒有特別困難,但是我看了一些文章教學,反而很著重在介面設計,畢竟 Widget 的特色就是要美美的,結果對我這種介面沒經驗的人來說,往往第一步就卡住了,像是要用內建的 draw9patch 去弄圖片等,故在此就先完全不理會,連什麼背景框框都不要,純粹用來筆記實作 Widget 的流程。


實做 Widget 跟一般 project 的設定沒有差太多,但還是要多留意:



  1. 建立專案時,不見得要使用 Activity

  2. 編寫 AndroidManifest.xml 檔案

  3. 新增描述 Widget 的檔案

  4. 設定 Widget 排版

  5. 實作更新 Widget 的物件


建立一個 project


[Eclipse]->[File]->[New]->[Android Project]

Project name: MyWidget
Build Target: Android 2.2
Application name: MyWidget
Package name: com.test.widget
Min SDK Version: 8


留意的是在此不啟用 Create Activity,這並不是必須的選項。啟用 Activity 的好處,可以規劃使用者在新增 Widget 時,可以在過程中可以多加設定動作。在此僅學習建立 Widget 的流程,就不多提了。


MyWidgetStru


設定 AndroidManifest.xml 檔案


新增 <receiver> 等敘述標籤,別於以前的 <activity>。

<receiver android:name=".MyWidget" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data android:name="android.appwidget.provider"
        android:resource="@xml/widget" />
</receiver>


在此 reciever 的 name 代表之後會建立的 Class 名稱,也就用更新 Widget 資訊的。而 meta-data 中所描述的 resource 則是下一步要新增 Widget 的描述檔


完整描述:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.test.widget"
    android:versionCode="1"
    android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <receiver android:name=".MyWidget" >
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
            <meta-data android:name="android.appwidget.provider"
                android:resource="@xml/widget" />
        </receiver>

    </application>
    <uses-sdk android:minSdkVersion="8" />
</manifest>


建立 xml/widget.xml 檔案,用以描述 Widget 的資訊


在 res 目錄裡建立新的目錄,名為 xml,接著在 res/xml 目錄裡建立一個檔案,可從 xml 按右鍵->[News]->[Other]->[Android]->[Android XML File],並填寫 File 為 widget.xml 並勾選 AppWidget Provider 的型態,最後就可以按 Finish 結束,接著開啟 xml/widget.xml 檔案進行編輯


<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="294dp"
    android:minHeight="72dp"
    android:updatePeriodMillis="1800000"
    android:initialLayout="@layout/main"

>
</appwidget-provider>


在此 minWidth 和 minHeight 是設定 Widget 顯示的範圍,而 dp 的數值計算可參考官網的教學,在此不多談,而 updatePeriodMillis 代表此 Widget 更新的頻率,1000 代表 1 秒,所以 1800000 代表 30 分鐘更新一次,更新頻率不宜過高,容易使得手機沒電。然而,經測試發現,目前內定的情境,無論把更新頻率弄到多快,最少要等 30 分鐘才會更新一次,解決的方式也不是沒有,在此先不多談。


設定排版部份


在此使用預設的 main.xml 檔案,開啟 main.xml 檔案後,替已存在的 TextView 加上 android:id="@+id/now"、android:gravity="center"、android:textColor="@android:color/black" 和 android:textSize="18sp" ,另外,再把 layout_height 更新為 fill_parent 即可。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
    android:id="@+id/now"
    android:gravity="center"
    android:textColor="@android:color/black"
    android:textSize="18sp"
    android:layout_width="fill_parent"

    android:layout_height="fill_parent"
    android:text="@string/hello"
    />
</LinearLayout>


實做 Widget 的資訊更新


在 src/com.test.widget 裡,新增一個 Class 名為 MyWidget.java


package com.test.widget;

import java.text.SimpleDateFormat;
import java.util.Date;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;

public class MyWidget extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context,AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        RemoteViews updateViews = new RemoteViews( context.getPackageName(), R.layout.main);
        updateViews.setTextViewText(R.id.now,  new SimpleDateFormat( "yyyy/MM/dd HH:mm:ss" ).format( new Date() ) );
        appWidgetManager.updateAppWidget(appWidgetIds, updateViews);
    }
}


最後,把程式 Run 起來,起初模擬器上不會顯示任何效果,因為 Widget 是要讓使用者自行設置的,需要透過模擬器鍵盤上的 [Menu]->[Add]->[Widgets] 就可以找到 MyWidget,點選下去才是真的安裝此 Widget 啦,成果就是螢幕上面多了一個時間囉!這也是 MyWidget.java 裡頭寫得東作,僅把 TextView 上的內容更新為時間。另外,若想要把已安裝的 Widget 刪除,僅需用滑鼠按住它,接著 Home 就會變成垃圾桶,拖進去就可以囉。


2010年12月15日 星期三

Android 開發教學筆記 - 使用 Regular Expression、Network Connection 和 Thread

getNews


打算寫一個稍微複雜的小程式,練習的項目:



  • 使用 Regular Expression

  • 使用 Network Connection

  • 使用 Thread


以三個項目為出發點,把以前的老題目拿出來:定期抓台灣 Yahoo 首頁的兩則焦點新聞,顯示在 Android 模擬上。因此,先想一下排版問題,大概就三個 TextView,分別為 更新時間 和 兩則新聞。


建立 project


[Eclipse]->[File]->[New]->[Android Project]

Project name: MyWeb
Build Target: Android 2.2
Application name: MyWeb
Package name: com.test.Web
Create Activity: MyWeb
Min SDK Version: 8


設定使用網路權限


點選 AndroidManifest.xml 檔案,在 <manifest> 裡增加 <uses-permission android:name="android.permission.INTERNET" />


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.test.web"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".MyWeb"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
    <uses-sdk android:minSdkVersion="8" />
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>


設定排版


點選 main.xml 檔案,增加 3 個 TextView,分別為"更新時間"、"新聞1"和"新聞2"。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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"
    />
<TextView android:id="@+id/UpdateDate" android:layout_width="fill_parent" android:layout_height="wrap_parent"></TextView>    
<TextView android:id="@+id/News1" android:layout_width="fill_parent" android:layout_height="wrap_content"></TextView>
<TextView android:id="@+id/News2" android:layout_width="fill_parent" android:layout_height="wrap_content"></TextView>

</LinearLayout>


MyWeb.java


package com.test.web;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

import com.test.web.GetYahooNews;

public class MyWeb extends Activity {
    Handler jobs;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        jobs = new Handler();
        new Thread( new GetYahooNews( this )).start();
    }

    public void updateNews( final ArrayList<String> news )
    {
        jobs.post( new Runnable(){
            public void run()
            {
                TextView showTextView;

                if( news != null && news.size() >= 2 )
                {
                    if( ( showTextView = (TextView) findViewById(R.id.News1) ) != null )
                        showTextView.setText("[News] "+(String)news.get(0));
                    if( news.size() == 4 && ( showTextView = (TextView) findViewById(R.id.News2) ) != null )
                        showTextView.setText("[News] "+(String)news.get(2));
                }
                if( ( showTextView = (TextView) findViewById(R.id.UpdateDate) ) != null )
                {
                    DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
                    showTextView.setText( "[Update @ " + dateFormat.format(new Date() ) + "]" );
                }
            }
        });
    }
}


GetYahooNews.java


package com.test.web;

import java.io.*;
import java.util.ArrayList;
import java.util.regex.*;
import java.net.HttpURLConnection;
import java.net.URL;

import android.util.Log;

import com.test.web.MyWeb;

public class GetYahooNews implements Runnable {
    MyWeb activity;
    
    GetYahooNews( MyWeb n)
    {
        this.activity = n;
    }
    public static void report( String message )
    {
        //System.out.println( "[Report] " + message );
        Log.d( "Report" , message );
    }
    public static ArrayList<String> getHotNews()
    {
        ArrayList<String> news = new ArrayList<String>();
        HttpURLConnection con = null;
        try
        {
//*
            URL url = new URL("http://tw.yahoo.com");
            con = (HttpURLConnection) url.openConnection();            
            con.setReadTimeout(10000);
            con.setConnectTimeout(15000);
            con.setRequestMethod("GET" );
            con.addRequestProperty("User-Agent","Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9");
            con.setDoInput(true);
            con.connect();

            BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream(), "UTF-8" ));
// */
//            BufferedReader reader = new BufferedReader(new InputStreamReader( new FileInputStream("fetch.html"), "UTF-8" ));

            String n, result="";
//            while( ( n = reader.readLine() ) != null )
//                result += n;

            StringBuilder htmlContent = new StringBuilder();
            while ((n = reader.readLine()) != null)
               htmlContent.append(n);

            result = htmlContent.toString();

//            BufferedWriter out = new BufferedWriter( new FileWriter("fetch.html") );out.write(result);out.close();
            
            String pattern;
            int at;

            pattern = "<label class=\"img-border clearfix\">";
            if( ( at = result.indexOf(pattern) ) < 0 )
            {
                news.add( "format error 1" );
                report( "format error 1" );
                return news;
            }
            result = result.substring( at );

            pattern = "<ol class=\"newsad clearfix\">";
            if( ( at = result.indexOf(pattern) ) < 0 )
            {
                news.add( "format error 2" );
                report( "format error 2" );
                return news;
            }
            result = result.substring( 0 , at  );

            pattern = "<h3[^>]*>[^<]*<a href=\"(.*?)\"[^>]*>(.*?)</a></h3>";
            Pattern p = Pattern.compile( pattern , Pattern.CASE_INSENSITIVE | Pattern.DOTALL );
            Matcher m = p.matcher( result );

            while( m.find() )
            {
//                report( "\n==== Get === \n" + m.group() + "\n" );
//                report( "URL: " + m.group(1) );
//                report( "Title: " + m.group(2) );
                String newsTitle = ""+ m.group(2);
                String newsUrl = "" + m.group(1);
                if( ( at = newsUrl.indexOf( "http:" ) ) > 0 )
                    newsUrl = newsUrl.substring( at );
                news.add( newsTitle );
                news.add( newsUrl );
            }
        }catch(Exception e)
        {
            news.add( "Exception:"+e );
        }
        finally
        {
            if ( con != null )
                con.disconnect();
        }
        return news;
    }
    public void run()
    {
        try
        {
            while( true )
            {
                try
                {
//                    ArrayList<String> demo = new ArrayList<String>();demo.add("Yo1");demo.add("Yo2");demo.add("Yo3");demo.add("Yo4");activity.updateNews( demo );
                    activity.updateNews( getHotNews() );
                    Thread.sleep( 1000 * 60 * 60 * 2  );    // 2 hrs
                }
                catch(Exception e)
                {
                    report( "run while Exception:"+e );
                    break;
                }
            }
        }
        catch( Exception e )
        {
            report( "run Exception:"+e );
        }
    }
}


在 MyWeb 裡有一樣東西比較特別,叫做 Handler,可以把他當作管理 Thread 的用法,在官網的描述裡,比較重要的敘述:


There are two main uses for a Handler: (1) to schedule messages and runnables to be executed as some point in the future; and (2) to enqueue an action to be performed on a different thread than your own.


在這邊使用 Handler,是讓更新 UI 的部份能夠透過 Runnable 來處理事件,如果不使用的話,會有無法更新 UI 的現象,暫時還沒了解底層的問題。


接著是 MyWeb 裡的 updateNews 函數,其參數是使用 final 的描述,加上這個描述後,可以讓接下來的 new Runnalbe 裡,可以直接用傳進來的參數,如果不用 final 也有解法啦,就是自己在定義一個實做 Runnable 的物件,然後把參數都傳遞好,稍微麻煩一點:


public void updateNewsPrev( ArrayList<String> news )
{
    class UIUpdate implements Runnable
    {
        ArrayList<String> news;
        UIUpdate( ArrayList<String> in )
        {
            news = in;
        }
        public void run()
        {
            TextView showTextView;
            if( news != null && news.size() >= 2 )
            {
                if( ( showTextView = (TextView) findViewById(R.id.News1) ) != null )
                    showTextView.setText("[News] "+(String)news.get(0));
                if( news.size() == 4 && ( showTextView = (TextView) findViewById(R.id.News2) ) != null )
                    showTextView.setText("[News] "+(String)news.get(2));
            }
            if( ( showTextView = (TextView) findViewById(R.id.UpdateDate) ) != null )
            {
                DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
                showTextView.setText( "[Update @ " + dateFormat.format(new Date() ) + "]" );
            }
        }
    }
    jobs.post( new UIUpdate( news ) );
}


接下來是 GetYahooNews 的部份,其中 getHotNews 函數是擷取台灣 Yahoo 首頁的兩則新聞出來,以此當作資料,接著就只是單純的當個 Runnalbe 在跑,並且設定 2 小時跑一次,跑完一次會呼叫 MyWeb 的 updateNews 去更新 UI 部份。至於怎樣從 GetYahooNews 呼叫 MyWeb 呢?那就是一開始在使用 GetYahooNews 時,就把 MyWeb 傳進去給他記住,在此使用 activity 變數紀錄。


Android 開發教學筆記 - 使用 String 和 StringBuilder 的差別

在練習使用 HttpURLConnection 來取得指定 URL 網址內容時,發現透過 Android 模擬器跑得非常非常緩慢,但我把相同的 Java Code 用桌機的環境執行,卻十分快速,讓我不禁感到奇怪,難道這是模擬器的問題嗎?還是初始化網路連線所耗的資源問題呢?


後來看到一則文章:Re: [android-developers] Re: First HTTP/S requests are slow. [Android 1.5] - msg#03831,看到了解法!


原來我寫得程式碼:


String n, result="";
while( ( n = reader.readLine() ) != null )
    result += n;


文章提到的寫法:


String n, result="";
StringBuilder htmlContent = new StringBuilder();
while ((n = reader.readLine()) != null)
    htmlContent.append(n);
result = htmlContent.toString();


這兩者的速度至少差 20 倍啊!讓我寫得程式從原先要等約 130 秒,瞬間變成只要等五秒,真是太神奇了。我的 CPU 是 AMD X4 945 + 4GB 記憶體,也不見得會跑很慢才是,因此找了不少文章,才發現是寫法與模擬器的影響吧。程式還包括網路連線,變數很多,很難找出此點。


完整的程式碼:


HttpURLConnection con = null;
try
{
    URL url = new URL("http://tw.yahoo.com");
    con = (HttpURLConnection) url.openConnection();            
    con.setReadTimeout(10000);
    con.setConnectTimeout(15000);
    con.setRequestMethod("GET" );
    con.addRequestProperty("User-Agent","Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9");
    con.setDoInput(true);
    con.connect();

    BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream(), "UTF-8" ));

    String n, result="";

//    while( ( n = reader.readLine() ) != null )
//        result += n;

    StringBuilder htmlContent = new StringBuilder();
    while ((n = reader.readLine()) != null)
       htmlContent.append(n);
    result = htmlContent.toString();
}
}catch(Exception e)
{
}
finally
{
    if ( con != null )
        con.disconnect();
}


Android 開發教學筆記 - 排版更新或顯示問題

花了幾個小時,才發現這個蠢問題,特別紀錄一下。


在實作 Android 程式時,新增了 3 個 TextView,想要在特定情況下一起更新三個欄位,但很奇妙的只有第一個 TextView 會顯示出來,後面來兩個 TextView 卻看不到。花時間不斷地確認,最後才發現錯誤的地方是在 layout 的部份:


<TextView android:id="@+id/UpdateDate" android:layout_width="fill_parent" android:layout_height="fill_parent"></TextView>
<TextView android:id="@+id/News1" android:layout_width="fill_parent" android:layout_height="fill_parent"></TextView>
<TextView android:id="@+id/News2" android:layout_width="fill_parent" android:layout_height="fill_parent"></TextView>


有發現到了嗎?因為 layout_height 都是 fill_parent 的數值,將導致只又 UpdateDate 這個 TextView 會佔滿剩下的螢幕空間,導致 News1 和 News2 不會顯示出來,再加上一開始沒有給初始值,剛好沒看到這個現象。


修正方式,改使用 wrap_content 即可:


<TextView android:id="@+id/UpdateDate" android:layout_width="fill_parent" android:layout_height="wrap_content"></TextView>
<TextView android:id="@+id/News1" android:layout_width="fill_parent" android:layout_height="wrap_content"></TextView>
<TextView android:id="@+id/News2" android:layout_width="fill_parent" android:layout_height="wrap_content"></TextView>


紀錄一下,提醒自己。


2010年12月14日 星期二

[Java] Regular Expression 與 HttpURLConnection 練習

年初,用 PHP 寫了這個,[PHP] 使用官方 Plurk API 實作簡單的機器人 - 靠機器人救 Karma!以 Yahoo News 為例,年終時,給他拿來當作 Android 程式的練習題目,結果弄了半天,發現 Java 語法熟練度很差,因此乾脆跑回去練習 Java 了!需搞懂的就是如何使用 Regular Expression 和網路的連線處理。


簡易範例:


import java.io.*;
import java.util.regex.*;
import java.net.HttpURLConnection;
import java.net.URL;

class Test
{
    public static void report( String message )
    {
        System.out.println( "[Report] " + message );
    }
    public static void main(String argv[])
    {
        HttpURLConnection con = null;
        try
        {
//*
            URL url = new URL("http://tw.yahoo.com");
            con = (HttpURLConnection) url.openConnection();
            con.setReadTimeout(10000);
            con.setConnectTimeout(15000);
            con.setRequestMethod("GET" );
            con.addRequestProperty("User-Agent","Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9");
            con.setDoInput(true);
            con.connect();

            BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream(), "UTF-8" ));
// */
//            BufferedReader reader = new BufferedReader(new InputStreamReader( new FileInputStream("fetch.html"), "UTF-8" ));
            String n , result = "";
            while( ( n = reader.readLine() ) != null )
                result += n;

//            BufferedWriter out = new BufferedWriter( new FileWriter("fetch.html") );out.write(result);out.close();
//            System.out.println( "Result:" + result );

            String pattern;
            int at;

            pattern = "<label class=\"img-border clearfix\">";
            if( ( at = result.indexOf(pattern) ) < 0 )
            {
                report( "format error 1" );
                return;
            }
            result = result.substring( at );

            pattern = "<ol class=\"newsad clearfix\">";
            if( ( at = result.indexOf(pattern) ) < 0 )
            {
                report( "format error 2" );
                return;
            }
            result = result.substring( 0 , at  );

            pattern = "<h3[^>]*>[^<]*<a href=\"(.*?)\"[^>]*>(.*?)</a></h3>";
            Pattern p = Pattern.compile( pattern , Pattern.CASE_INSENSITIVE | Pattern.DOTALL );
            Matcher m = p.matcher( result );

            while( m.find() )
            {
                report( "\n==== Get === \n" + m.group() + "\n" );
                report( "URL: " + m.group(1) );
                report( "Title: " + m.group(2) );
            }
        }
        catch( Exception e )
        {
            report( "Error:" + e );
        }
        finally
        {
            if ( con != null )
                con.disconnect();
        }
    }
}


執行結果:


$ javac Test.java && java Test
[Report]
==== Get ===
<h3><a href="news/a/h1/t/*http://tw.news.yahoo.com/article/url/d/a/101214/5/2iy8o.html" title="最新!美元弱 新台幣升破30">最新!美元弱 新 台幣升破30</a></h3>

[Report] URL: news/a/h1/t/*http://tw.news.yahoo.com/article/url/d/a/101214/5/2iy8o.html
[Report] Title: 最新!美元弱 新台幣升破30
[Report]
==== Get ===
<h3><a href="news/a/h2/t/*http://tw.news.yahoo.com/article/url/d/a/101214/2/2iy6j.html" title="情人多愛你 手碰觸方式露餡">情人多愛你 手碰 觸方式露餡</a></h3>

[Report] URL: news/a/h2/t/*http://tw.news.yahoo.com/article/url/d/a/101214/2/2iy6j.html
[Report] Title: 情人多愛你 手碰觸方式露餡


說真的,細算的話,五年前 Java 是我最常用的語言,至少用了一年多,但如今什麼都忘光光了!


2010年12月13日 星期一

Android 開發教學筆記 - 簡單設定 Google Maps

參考資料:



想要在 Android 上頭使用 Google Maps 的功能,有幾道流程:



  1. 申請 Android Maps API Key

  2. 建立使用 Google APIs 的模擬器

  3. 建立使用 Google Maps API 的 projects (在此 Build Target 使用 Google APIs/Google Inc./2.2/8 )

  4. 設定 Android Application 環境,使用 INTERNET permissions 及相關使用的 lib

  5. 設定 MapView 的 layout

  6. 設定程式碼


首先關於取得 Google Maps API Key,此部份跟一般申請上的不一樣,需使用 Android 的申請 Sign Up for the Android Maps API - Android Maps API - Google Code,此時會需要一組認證指紋(MD5),而這組序號用在開發程式的階段(debug mode),而當你要將程式上架時,必須在用另一組(release mode),細節請參考 Getting the MD5 Fingerprint of the SDK Debug Certificate,在此就依 debug mode 申請一組:


$ keytool -list -keystore ~/.android/debug.keystore
...
Certificate fingerprint (MD5): 94:1E:43:49:87:73:BB:E6:A6:88:D7:20:F1:8E:B5:98


其中認證指紋(MD5)為 94:1E:43:49:87:73:BB:E6:A6:88:D7:20:F1:8E:B5:98 (此為範例,請輸入自己產生的),帶著這組就可以去申請 Android Maps API Key 囉,請輸入到 My certificate's MD5 fingerprint 欄位。送出後,頁面將顯示 "您的金鑰" 、 "金鑰適合所有使用以下指紋憑證所簽署的應用程式" 和 "此處提供您 xml 配置的範例",共三筆資。請保存好,這邊只會使用"您的金鑰"。


建立 Android API 模擬器,如果你已經有設定一台可運行 Google APIs 2.2 的話,那可以略過此步。


[Eclipse]->[Window]-> [Android SDK and AVD Manager]->[New]

Name: Map
Target: Google APIs(Google Inc.) - API Level 8
    
按下 Create AVD 及建立完成


建立一個專案


[Eclipse]->[File]->[New]->[Android Project]

Project name: MyMap
Build Target: Google APIs, Google Inc., 2.2, 8
Application name: MyMap
Package name: com.test.map
Create Activity: MyMap
Min SDK Version: 8

設定完就可以按 Finish 囉


Android Project Structure


緊接著設定 Application 的權限和相關 lib 部份,請在左邊點選 AndroidManifest.xml 檔案,此時右邊視窗則顯示該檔相關的設定,可以在底部找到 Manifest/Application/Permissions/Instrumentation/AndroidManifest.xml 等子頁面切換,在此切換 AndroidManifest.xml 分頁,直接編輯此 xml 檔案,在 <application> 裡頭增加 <uses-library android:name="com.google.android.maps" /> 資訊,在 <manifest> 裡增加 <uses-permission android:name="android.permission.INTERNET" />,分別代表要使用函式庫:(com.google.android.maps)和需要使用網路資源(android.permission.INTERNET)


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.test.map"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".MyMap"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <uses-library android:name="com.google.android.maps" />
    </application>
    <uses-sdk android:minSdkVersion="8" />
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>


設定完應用程式執行環境後,接著設定排版部份(layout),透過左邊視窗請點選 main.xml 檔案,直接用下面的資訊覆蓋掉:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mainlayout"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <com.google.android.maps.MapView
        android:id="@+id/mapview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:clickable="true"
        android:apiKey="Your Maps API Key"
    />

</RelativeLayout>


其中 Your Maps API Key 請改填"您的金鑰",如此一來即完成排版的部份


最後,則是修改程式碼的部份,請點選 MyMap.java 檔案



  1. 增加 import com.google.android.maps.*;

  2. 將 public class MyMap extends Activity 修改為 public class MyMap extends MapActivity

  3. 實做必要函數 boolean isRouteDisplayed()


完整程式碼:


package com.test.map;

import android.app.Activity;
import android.os.Bundle;
import com.google.android.maps.*;

public class MyMap extends MapActivity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
    @Override
    protected boolean isRouteDisplayed() {
        return false;
    }

}


如此一來,則可以執行,並看到模擬器顯示 Google Maps 囉!


AndroidMaps


上述算是一個簡單的設定流程,但僅供了解流程而已,因為產生的程式除了呈現一個 Google Maps 外,沒有太多的互動,因此,如果想要增加一些使用者互動部份,如 Zoom In/Zoom Out,請在 onCreate 加上兩行程式碼:


MapView mapView = (MapView) findViewById(R.id.mapview);
mapView.setBuiltInZoomControls(true);


請記得加到 setContentView(R.layout.main); 後面,如果加在前面會導致程式 crash 喔,可以看看 setBuiltInZoomControls method for MapView causes application to crash 的敘述,避免 crash 也可以改成:


MapView mapView;
if( ( mapView = (MapView) findViewById(R.id.mapview) ) != null )
    mapView.setBuiltInZoomControls(true);


但沒擺在 setContentView(R.layout.main); 後面,還是沒用的喔。


AndroidMapsZoom


其他資料:



2010年12月10日 星期五

冰箱 Get!

冰箱、食材


冰箱啊~這是多麼奢侈的設備啊。記得大學時期,好像是升大三還升大四時,室友在校園討論板上,終於!搶到了冰箱,一行人推著推車很高興地去跟學長買二手的,我記得好像賣 1200 的樣子,還附一個烤箱跟平底鍋,當時室友們都很黑皮!對我而言倒覺得還好,因為我只三餐偶爾宵夜而已,並且不怎喝飲料,冰箱對我而言真的有跟沒有是一樣的。後來,過了幾個月後,那台冰箱的使用度真的降到了冰點,除了一開始很黑皮冰的飲料外,好像就沒什麼特別了。


直到幾天前,多虧同事的幫忙,在公司的討論網上終於買了一台二手冰箱,且賣冰箱的正巧一年前坐在我座位附近,真巧啊!就這樣重複著大學時的步驟,借推車、運冰箱和定位。對了,搬冰箱最好搬完靜待一會兒喔,所以我是隔天下班後才正式啟用它。


據同事的說法,擔心冷藏不冷,所以不怎使用,因此,第一件事就是冰塊的測試 XD 空蕩蕩的冰箱只冰那一小盤的生水。慶幸地,一切 OK ,就算我把冰箱的強度降到最低,早上的生水,下班時還是會結冰,看來運氣不錯。


沒多久後,就跟同事去賣場,買了一包僅 49 元的韭菜水餃,成了第二住進冰箱的食材。今晚上班就去附近的果菜賣場買了相關的工具,如鍋、瓢子等設備,順便試了買了一包料理包以及一把青菜和一包豆皮,看來未來又有東西可學了,來煮點東西訓練一下。


2010年12月1日 星期三

[Javascript] 使用 jQuery 把表單資料紀錄在 cookie 裡

做了一個有很多選項要填寫的表單,但由於一些環境上的考量,有點懶的把已填寫過過得表單資料記錄在 server 端,因此就想到稍微惡搞一下,把表單資料都紀錄在 client 上!由於該表單也沒有啥機密資訊,所以也不用太擔心什麼。


紀錄方式:


當使用者按下送出表單的那一刻時,除了會加上一些判斷,確認資料是否有輸入完整,而後可以做的小動作就是把表單的資料都儲存在 cookie 裡,以後有需要可以從 cookie 撈出來幫忙填寫。


相關環境:



  • jQuery

    • <script type="text/javascript" src="http://code.jquery.com/jquery-1.4.3.min.js"></script>



  • jQuery Plugin - Cookie (非必要)

    • <script type="text/javascript" src="http://plugins.jquery.com/files/jquery.cookie.js.txt"></script>

    • 自己管也可以,但我有點懶的回顧 cookie 語法,就改用這個 plugin 啦,用 $.cookie( 'Name' , 'Value' , { path: '/', expires: 365 } ); 儲存,用 $.cookie( 'Name' ) 取回數值




用法:


<form method="POST" onsubmit="return checkData();">
    <dl>
        <dt><button onclick="reload(); return false">讀取前次資料</button></dt>

        <dd>&nbsp;</dd>


        <dt>姓名</dt>
        <dd><input class="cookie" name="builder_name" style="width:50%;"/></dd>
        <dd>&nbsp;</dd>

        <dt>喜愛運動</dt>
        <dd>
            <select class="cookie" name="status">
                <option value="0" selected>否</option>
                <option value="1">是</option>
            </select>
        </dd>
        <dd>&nbsp;</dd>


        <dt>狀態</dt>

        <dd><input type="checkbox" name="status"/> 已婚</dd>
        <dd>&nbsp;</dd>

        <dd>&nbsp;</dd>

       
<dt><button type="submit">執行</button></dt>

    </dl>
</form>


<script type="text/javascript">

function checkData()
{
    // ... do check ...

    // ... do save ...
    save();

    return true;
}

function save()
{
    $( '.cookie' ).each( function(){
        var target_cookie = '__' + $(this).attr('name');
        var data = $(this).val();
        if( $(this).attr('type') == 'checkbox' )
            data = $(this).is(':checked') ? 'on' : 'off';
    
        $.cookie( target_cookie, data , { path: '/', expires: 365 } );
        //console.log( target_cookie +' : ' + $(this).attr('type') + ' : ' + $.cookie( target_cookie ) + ' : ' + data );
    });
}

function reload()
{
    $( '.cookie' ).each( function(){
        var target_cookie = '__' + $(this).attr('name');
        if( $.cookie( target_cookie ) )
        {
            if( $(this).attr('type') == 'checkbox' )
            {
                if( $.cookie( target_cookie ) == 'on' )
                    $(this).attr('checked', true);
                else
                    $(this).attr('checked', false);
            }
            else if( $(this).attr('type') == 'select-one' && $(this).get(0) && $(this).get(0).options && $(this).get(0).options.length )
            {
                var data = $.cookie( target_cookie );
                var options_list = $(this).get(0).options;
                for( var i=0 , cnt = options_list.length ; i<cnt ; ++i )
                {
                    if( options_list[i].value == data )
                    {
                        $( options_list[i] ).attr( 'selected' , 'selected' );
                        break;
                    }
                }
                $(this).trigger('change');  // 有些 select 可以搭配 change 事件
            }
            else
                $(this).val( $.cookie( target_cookie ) );
            //console.log( target_cookie +' : ' + $(this).attr('type') + ' : ' + $.cookie( target_cookie ) );
        }
    });
}
</script>


如此一來,就可以儲存表單文字、checkbox以及 select 等,都可以處理。這邊比較投機的是對要儲存的表單,多設一個 class 數值,用來讓 jQuery 準確地撈出來。