ping in Android(續)

後來發現裏面有包ping這指令,而且有設置setgid權限,那麼應該是可以執行。只是試了之後,卻不行,然後我就以為不行。之後不死心,把stderr接出來看,才知道我下錯指令,於是修正以後,就可以了。

[java]
final class Helper {
private final String TAG=”Helper”;
private void pingInExec(String host) {
Runtime runtime = Runtime.getRuntime();
String command = String.format(“/system/bin/ping -c 2 %s”, host);
Process proc;
try {
proc = runtime.exec( command );
BufferedReader in = new BufferedReader(new InputStreamReader(proc.getInputStream()));
BufferedReader err = new BufferedReader(new InputStreamReader(proc.getErrorStream()));

String inputLine;
while ((inputLine = in.readLine()) != null) {
Log.d(TAG, inputLine );
}
in.close();
while ((inputLine = err.readLine()) != null) {
Log.e(TAG, inputLine );
}
err.close();
proc.waitFor();
int exit = proc.exitValue();
Log.d(TAG, String.format(“exitcode=%d”, exit) );
if (exit == 0) { // normal exit
Log.d(TAG, “RESPONSE_OK”);
} else { // abnormal exit, so decide that the server is not reachable
Log.d(TAG, “RESPONSE_TIMEOUT” );
}
} catch (IOException e) {
Log.e( TAG, e.getMessage() );
} catch (InterruptedException e) {
Log.e( TAG, e.getMessage() );
}
}
}
[/java]

ping in Android

上網找了一下,一般對於ping的建議是直接利用java內建的InetAddress.isReachable()來做,實際上在ping內部網路的伺服器時,是沒問題的,但是如果要ping位於外部網路的伺服器時,就會失敗而回傳False。

Android內部的實作是在libcore/luni/src/main/java/java/net/InetAddress.java裡,這裡就很簡單的建立socket,然後試著連到指定位址的port 7,如果可以連,或者是伺服器明確地拒絕,就視為伺服器存在,可以連線。這就解釋了為什麼無法ping位於外部網路的伺服器,因為ISP為了安全或是其他考量,而不允許。我分別以python與java寫了與Android實作相似的程式去實驗,的確都不行。

[python]
import sys
import socket

if len(sys.argv)<2:
print( “Need at least 1 parameters.” )
print( “Usage: {0} host”.format( sys.argv[0] ) )
sys.exit(-1)

r = False
try:
s = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
s.settimeout(5)
s.connect( (sys.argv[1], 7) )
r = True
except socket.error, ex:
if ex.errno==111:
r = True
else:
print( ex )

if r:
print( “{0} is reachable.”.format( sys.argv[1] ) )
else:
print( “{0} is NOT reachable.”.format( sys.argv[1] ) )
[/python]

[java]
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.io.IOException;

class Ping {
public static void main(String[] args) {
InetAddress in;

try {
in = InetAddress.getByName(args[0]);
boolean result = in.isReachable(5000);
if (result) {
System.out.println(“Response OK”);
}
else {
System.out.println(“Response fail”);
}
} catch (UnknownHostException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
[/java]

Java裡只能建立 stream(TCP) 或 dgram(UDP) 的socket,那麼只能用JNI,用C寫ping了,但經過實驗結果,發現會因為權限的關係而無法建立socket,原來要建立raw與IPPROTO_ICMP的socket,需要root權限。一般linux裡,非root使用者可以使用ping,是因為ping加上了setuid權限,才能使用。在Android裡,要不就是建立service,要不就是設法為ping加上setuid,否則是都無法使用的。

Get IP Address in Android

這篇Get the ip address of your device on Android Development其實就講的很清楚了,就是用NetworkInterface.getNetworkInterfaces()去取得所有網路介面,然後再用網路介面的getInetAddresses()去看該網路卡上的所有IP位址,然後把Loopback位址排除掉,就可以取得了。該開的權限有android.permission.INTERNET與android.permission.ACCESS_WIFI_STATE。

我工作上的需求,是要拿到3G那邊的IP,經過實驗,發現在WiFi開啟的情況下,只會拿到WiFi網路介面的IP位址,而非3G網路介面的。如果把WiFi網路介面關閉,就可以拿到3G網路介面的IP。我以為是程式邏輯的關係,可是在確認過NetworkInterface.getNetworkInterfaces()傳回的網路介面數量以後,發現都是取得兩個網路介面,而非我預期的三個。好,那我不使用NetworkInterface.getNetworkInterfaces()改用NetworkInterface.getForName()去取的話,在WiFi開啟的情況下,取得的結果會是null,也就是說該網路介面被關掉了。

直接去看底層,在 mydroid/libcore 下NetworkInterface的JNI函式,那邊是使用netlink去跟kernel問所有的網路介面,然後解析之後傳回。那麼底層應該是沒有問題,看來是中間層為了能取得較快的網路速度,而自動把3G網路介面給關閉了。看來也只能在程式執行前,先把WiFi給關閉,才能取得3G網路介面的IP位址了。

參考資料:

Android 2.3.x 的 JavaScript Interface

星期天利用Google reader看文的時候,看到Fred大的這篇:Android 問題百出之 2.3.x 的 JavaScript Interface,我就心想,完了,我踩到雷。因為最近正好在實驗WebView Javascript與Java部份,回家一試,果真如此,真的頭很大。

再上網找了一下,發現很早以前就有人回報這個問題,喂,我說Google啊,那為什麼不修?!(怒),這個討論串很長,連PhoneGap開發小組也都有回。結果沒辦法,只能參考Fred大的文去改了。可是改了以後,卻怎麼樣都不能動,連原來的會導向到LogCat的部份都失效了,試了好久,又去StackOverflow找,找了幾篇,像:Uncaught TypeError when using a JavascriptInterface…等等,但還是不行,真的讓人摸不著頭緒。

後來是發現 onConsoleMessage 應該是要加 @Override 的,現在卻不用了,上網去Android reference找,才發現我搞錯類別名稱了。一個是WebChromeClient,而Fred大文章裡用的是WebViewClient,搞清楚,修改程式以後,這才讓我有了log利器,能用這個來去找到Javascript修補程式的錯誤,並修正。

為了要可以自動化為類別產生javascript修補程式的步驟,我運用了Annotation去標記需要產生Javascript代碼的函式,這樣就可以運用reflection的方式去找到這些函式,並產生程式,以注入到WebView裡面去。為了避免老是改程式,我也利用了Google瀏覽器,直接把產出的Javascript程式,放到Google瀏覽器的網址列去執行,這樣很快就知道哪裡有錯誤,並且修正。

總之,大概就是這樣子,解決了。

emulator無法啟動

這蠻奇怪的,但只要每次我在 AVD Manager 要啟動時,勾選 Wipe user data 以後,就都可以啟動。可是因為要手動指定 proxy 的關係,我必須用指令列的方式,但加上 -wipe-data 卻沒有用,上網搜了一下發現 Android Emulator can’t start, ’cause of wrong folder裡面有提到要設定 ANDROID_SDK_HOME,於是在命令提示字元裡先設置 set “ANDROID_SDK_HOME=c:\Documents and Settings\your_name” 之後,就可以順利啟動 emulator 了。
以後要省這麻煩的話,可以在”我的電腦”/”內容”/”進階”/”環境變數”裡去新增,就可以一勞永逸了。

Android MapView – Failed to find provider info

這篇Failed to find provider info for com.google.settings in MapView Example講得很仔細,該說的都說了。
我個人的經驗:

  • 有這個錯誤訊息,未必會看不到地圖。
  • 當apiKey是對的時候,沒有網路,仍然會看不到地圖,會讓人懷疑apiKey有誤。
  • 請務必讓網路暢通,如果公司裡面有proxy,可以試試在命令提示字元去啟動emulator:emulator -avd your_avd -http-proxy your_proxy_server:your_proxy_server_port -debug-proxy,這樣應該就可以了。(這指令是在Android 2.0, 2.0.1 proxy problems撈到的)。

關於網路的部份,還可以參考:

WebView裡$.parseJSON() fail

查了好久。我是先用setWebChromeClient()去處理WebView的onConsoleMessage() callback,將console.log()的輸出導向到Android的LogCat以便於觀察。先懷疑jQuery是不是有問題,就先拿另外一個變數放確定可以的JSON字串丟進去parse,沒問題。那我就懷疑從addJavascriptInterface()那兒來的物件的傳回值了,直接複製傳回結果,放到JavaScript裡當作一個字串,parse的結果,是沒問題的。那?會是型態的問題嗎?用typeof()檢查了一下,發現是object,而非字串,真相大白。我先試著把一個空字串加到傳回值裡,然後就可以得到字串,也就可以正常parse了。再查了一下,JavaScript是有轉換函式的,叫String(),試了一下,也可以把型態是object的傳回值轉為字串。至此,就解決了parseJSON()的問題。

WebView

畫面如果用Android裡的xml,光想,就讓我頭痛。還是用HTML來的方便,而且現在有HTML5,Android也有WebView,再加上之前敗了這本建構Android應用程式,我想這樣應該會方便很多。可是用HTML,就得用JavaScript,也需要從Android程式那邊溝通,就研究了一下。

Using WebViews裡提供的範例,就有講其中的關鍵了,就是使用WebView提供的addJavascriptInterface(),這可以把Java的物件加到JavaScript runtime裡去,這樣JavaScript就可以使用這個物件。可以呼叫之後,那麼資料該怎麼傳遞呢??這牽涉到轉換的問題,而且Android只允許傳遞int、String等一些基礎類別,而不允許使用自訂類別,這就糟了。還好,這篇Working with Android addJavascriptInterface就有討論到這個問題,他的結論是使用JSON字串,這看來是最簡便的方法。JavaScript可以使用jQuery提供的parseJSON()與toJSON()來從字串轉成物件,或從物件轉為字串,而Java則可以用jackson來轉換。Build a Contacts Application With jQuery Mobile & the Android SDK這篇是更完整的使用範例。

Using WebViews裡另外有提到WebView的setWebViewClient,這個其實是WebView的callback,讓你可以在WebView遇到一些特定狀況的時候,作一些處理。詳細的說明要參照WebViewClient這邊。

如果嫌麻煩的話,是參考書裡的PhoneGap就好,書上介紹的步驟就只要寫HTML+CSS+JavaScript就好,不用特別再寫Java的部份,也不用裝eclipse,最後就用ant,就可以build出apk。

“How to install Ubuntu on Android”觀後感

看完還是順手紀錄一下,要不然就白花這25分鐘看了。
簡單的說,要先 root 並且裝上 busybox,接著下載 bootubuntu,這是預先編譯好的 ARM Ubuntu image,放到 SD Card 上的 ubuntu目錄下。然後用 adb shell 連上,執行 su,到 SD Card 的 ubuntu 目錄下,執行 bootubuntu,就進 ubuntu 環境了。有了 ubuntu 環境,就能執行 apt-get,立馬就可以裝上 openssh-server、tightvncserver,openssh-server 是為了有更好的終端機環境可用,因為 Windows 的命令提示字元不夠理想。tightvncserver 則是要給 market 上的 vnc client 用的,執行 vnc client 連到本機上的 vnc server,就有 Ubuntu 畫面跑出來了。