2011年1月5日 星期三

Android 開發筆記 - 透過 Google Maps API 畫出 GPS 路徑

mygpspath2
紅色線即為GPS logs 的路徑


想要練習把一連串的 GPS logs 畫在 Google Map 上,首先要解決的問題有兩種:



  • 哪裡找 GPS logs

  • 要怎樣在 Google Maps 上畫線


關於第一個問題,可以在 OpenStreetMap 上找到測資,有個頁面是 GPS 軌跡,在那邊可以取得別人公開的 GPS logs,此例為 2011_01_03_Schwechat_Brno.gpx,然而下載的資料格式是 xml 檔案,而測試僅需使用經緯度,因此要稍微處理一下,以 Ubuntu 系統為例:


$ wget http://www.openstreetmap.org/trace/896382/data
$ grep -P '<trkpt lat="([0-9.]+)" lon="([0-9.]+)"' 896382.gpx | awk -F '"' '{ print $2, $4; }' > /tmp/test.data


如此一來,/tmp/test.data 就是只有經緯度的資料,但是行車紀錄的過程,可能停留在某處,而導致資料重複的問題,在此可以透過 hash 的概念,再更新 awk 的使用:


$ grep -P '<trkpt lat="([0-9.]+)" lon="([0-9.]+)"' 896382.gpx | awk -F '"' '{ if(!hash[$2$4] ){ hash[$2$4] = 1; print $2,$4;  } }'


然後我又比較偷懶,想乾脆轉成 double array 來使用,故流程多了點:


$ grep -P '<trkpt lat="([0-9.]+)" lon="([0-9.]+)"' 896382.gpx | awk -F '"' 'BEGIN{out=""}{ if(!hash[$2$4] ){ hash[$2$4] = 1; if( out != "" ) out = out "," ;  out = out""$2","$4; } }END{print "double logs[]={"out"};";}' > java.log


這時候 java.log 檔案內就是一個 double array 的宣告,把他複製起來擺到程式裡吧 :P 畢竟這只是個測試,先不用開檔讀資料的建法。


接著就是建立 Android Project 囉


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

Project name: MyGPSPath
Build Target: Google APIs/2.2/8
Application name: MyWidget
Package name: com.test.map.path
Create activity: MyGPSPath
Min SDK Version: 8


修改 AndroidManifest.xml,增加使用網路的權限以及使用 Google Maps lib


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.test.map.path"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".MyGPSPath"
                  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,使用 MapView 並且填寫 Google Map API Key,另外新增一個按鈕,以供 debug 使用


<?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="GOOGLE_MAP_API_KEY"
    />
    
    <Button
        android:text="Report"
        android:id="@+id/GetReport"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true">
    </Button>
</RelativeLayout>


程式碼:


package com.test.map.path;

import java.util.List;

import com.google.android.maps.*;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MyGPSPath extends MapActivity {

    StringBuilder debugOut;
    MapView mapView;

    // 從 openstreetmap 取出的 gps logs, 在此僅擺上頭尾兩點
    double logs[]={48.138050017878413,16.481179967522621,/* ... , */ 49.179430995136499,16.558635039255023}; 

    @Override
    protected boolean isRouteDisplayed() {
        return false;
    }
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        debugOut = new StringBuilder();
        
        if( ( mapView = (MapView) findViewById(R.id.mapview) ) != null )
        {
            mapView.setBuiltInZoomControls(true);
            mapView.setTraffic(true);
        }
        
        Button b;
        if( ( b = (Button) findViewById(R.id.GetReport) ) != null )
        {
            b.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    debugOut.append("logs length:"+logs.length+"\nbegin at:"+logs[0]+","+logs[1]+"\n");
                    AlertDialog alertDialog = new AlertDialog.Builder(MyGPSPath.this).create();
                    alertDialog.setTitle("debug");
                    alertDialog.setMessage(debugOut);
                    alertDialog.setButton("OK", new DialogInterface.OnClickListener(){
                        public void onClick(DialogInterface dialog, int which)
                        {
                            dialog.cancel();
                        }
                    } );
                    alertDialog.show();
                }
            });
        }
        
        List<com.google.android.maps.Overlay> ol = mapView.getOverlays();
        ol.clear();
        ol.add(new MyMapOverlay());
        mapView.invalidate();
        
        MapController mapController = mapView.getController();
        if( mapController != null )
        {
            mapController.animateTo(new GeoPoint( (int) (logs[0]* 1000000) , (int)(logs[1]* 1000000) ) );
            mapController.setZoom(8);
        }
    }
    
    class MyMapOverlay extends com.google.android.maps.Overlay
    {
        private Point getPixelXYFromGeoValue( Projection p , double lat, double lng )
        {
            GeoPoint in = new GeoPoint((int) (lat * 1000000) , (int) (lng * 1000000) );
            Point out = new Point();
            p.toPixels(in, out);
            return out;
        }
        @Override
        public boolean draw(Canvas canvas, MapView mapView,boolean shadow, long when)
        {
            super.draw(canvas, mapView, shadow);
            
            //if( !shadow )
            {
                Projection p = mapView.getProjection();
                Point out;
                Path myPath = new Path();
                
                for( int i=0; i<logs.length ; i+=2 )
                {
                    out = getPixelXYFromGeoValue( p, logs[i], logs[i+1] );
                    if( i == 0 )
                        myPath.moveTo(out.x, out.y);
                    else if( i+2 != logs.length )
                        myPath.lineTo(out.x, out.y);
                    else
                        myPath.setLastPoint(out.x, out.y);
                }
                
                Paint myPaint = new Paint();
                myPaint.setColor(Color.RED);
                myPaint.setStyle(Paint.Style.STROKE);
                myPaint.setStrokeWidth(10);
                myPaint.setAlpha(70);
               
                canvas.drawPath(myPath, myPaint);
            }
            return true;
        }
    }
}


成果:


mygpspath
紅色線則是這條 GPS logs 所繪製出來的路徑


mygpspathdebug
按下右上角的按鈕,可以顯示一些訊息,如點的個數和起點位置,另外也可以擺一些 debug 訊息也不錯


其他部份,還有很多待改善的地方,例如 GPS logs 過長時,移動地圖所造成的重繪之效率問題


[Unix] 以 line 為單位,去除重複的資料

有一堆 GPS 資料想要使用,但在紀錄資料時,有可能某處停留較久,因此會有重複資料的問題。解決方式可以寫簡單的程式,但我一直想不出有哪個洽當的 Unix 指令可以用,就先挑 awk 來使用。


資料格式:


25.064903,121.526657,
25.064903,121.526657,
25.064903,121.526657,
25.064898,121.526573,
25.064898,121.526573,
25.064898,121.526573,
25.064890,121.526489,
25.064890,121.526489,
25.064886,121.526436,
25.064886,121.526436,


輸出目標:


25.064903,121.526657,
25.064898,121.526573,
25.064890,121.526489,
25.064886,121.526436,


指令:


$ awk '{ if( !a[$0] ){ print $0 ; a[$0] = 1; } }' in.txt


使用簡單的 hash 概念。但比較重要的是指令少用,紀錄一下,以節省以後的時間花費。


感謝許青蛙補充!若不在意順序還可以用 sort -u 就好


感謝 fin 補充,對於連續的資料可以採用 uniq 來處理就行啦,所以在這個情境下就是連續性的刪除,可以用:


$ cat in.txt | uniq


2011年1月3日 星期一

中華電信 免費、短期日租型 3G 上網

之前想說放特休時申請短期的 3G 上網,但因為不能報帳的關係,再加上好像也不是必須的,因此最後沒用到。這次恰好碰到國小同學在問寒假返家想申請短期的行動上網,順便筆記一下,以免每次要找資訊都要重新找一遍。


有時候會希望可以 3G 上網,例如老家沒有網路,再加上自己用得是傳統手機,也沒啥上網功能,甚至有的 sim 卡還在走 2G 網路,殊不知使用手機 2G 上網(稱作 GPRS),不小心下載 10MB 資料量可就要費2457元!為此中華電信也都有做一種保護機制,像 GPRS 最高收費 4000 元,而 3G 上網好像最高收費是 1500 還 2000 ,我這邊沒找到資訊,可以打電話去問客服。為此,也有人為了避免自己或家人不小心用手機上網,一種是把 sim 卡的上網功能關閉,等到要想要使用時才打去客服請她們打開。另一個議題則是 sim 卡到底要不要換成 3G 的,新辦手機得大部分應該都會是 3G 的(或 3.5G的?這些我都沒研究),但很久以前辦的手機應該就會是 2G 的。有人說從 2G 調成 3G,有些通話費會變貴,細節要在驗證,而另一種問題則是手機會因為環境的關係,手機的使用模式會不停地在 2G 或 3G 網路切換,照成手機電力浪費,在此也不多談。另外一提的,想從 2G 轉成 3G 要去櫃檯辦理,要換 sim 卡,通常要等 1 ~ 2 天才會啟用。


回到主題,目前中華電信有提供 3G 上網的試用方案:3G行動上網免費試用



  • 免費 72 小時使用3G上網

  • 押金 2000 元

  • 可借行動網卡


試用的好處一來是免費,二來是可以測試使用環境,如住家或租屋處,是否適用於 3G 上網,因為並不是任何地方都適用的,這跟基地台、住家附近屏蔽等有關。缺點是只有限定特定幾家的店可以申請。


另一種則是直接用 3G 日租型方案:3G行動上網日租型



  • 3天 250 元

  • 7天 450 元


這是不限流量的費用,以七天來說,一天平均約65元,細節可從上述連結過去,點選"費率說明"。另外,看來是要自備行動網卡。缺點是別家的有比較便宜,像威寶有以日計算,一天39元喔,但還是要留意,使用 3G 上網還是跟使用環境有關。


雖然現在智慧手機當道,很多人一開始辦手機時就被強迫要辦 3G 網路的方案,例如吃到飽一個月 750 元,但對於我這類型的,身處於 wifi 環境再加上常常宅在家,對於 3G 網路的使用可有可無,偶爾去租短期的使用,其實還滿適合的,除非每個週末都要出去旅遊或是到處爬爬走的人,吃到飽方案可能也適合,另外,也有一些人租用一個月 200、400 或 650 元的月租方案,然後用超過的量,最多會只收一個定額,依序最高1500、1100 和 950 等,也是另一種使用考量喔,細節請參考:3G行動上網


其他參考資訊:



2011年1月2日 星期日

2011年~新年快樂!








這是電腦模擬的煙火,跟現場還是有不少落差 XD


不一會兒,一年又過了。在 2010 年的最後一天,跟隨著天龍人的腳步,在清大的竹客站,等車等了兩小時,第一次看到排隊的人潮,已經不是 S 型可以形容的,然後車子發動到台北又是兩個半小時,整整快要五個小時,才踏入目的地。在台北街頭看著人潮的,發現台北人真的十足地很會打扮,而底心瞧見的是錢潮!可惜得我還沒任何概念可以付諸於行動。


隨後,又跟著腳步到101附近等著倒數,也體會了什麼是水洩不通,在人群中擠壓打轉,但還不錯,瞧著 101 會有那種念頭,總要去 101 一次,無論是在那邊工作還是瞧瞧風景。


散場之後,散步回家,也算是第一次經過國父紀念堂,看到許多還在玩煙火的年輕人,以及不少外國人和相機的蹤影,另外,螢光幕上的阿妹正賣力的吆喝著。


沒想到 2011 年就這樣的開跑了,隔天一早收到了一位實驗室學長的紅色炸彈!希望今年可以過得比去年更加充實,將自己想做的事好好地實現。


祝大家新年快樂!


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


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