這篇要示範的是, 如何寫一個ContentProvider讓其他的應用程式透過這個Provider去存取網路上的資料
這不是正統用法, 不過的確可以這樣用, 那為啥要這樣呢? 只是好玩, 還沒想到特別的應用
這邊用的範例是Twitter search (search keyword): MLB
首先要建立一個放Provider的Package, 不過這個Provider不是拿來存取DB的, 傳統的Android ContentProvider通常是用來存取資料庫, 但那並不代表那就是它的全部, “Content”Provider不就是拿來提供”Content”的嘛?管他Content哪來
建立一個Provider, 姑且叫它做TwitterSearchProvider, Authorities定為twitter.my.search好了, 裡面, 先只實作一個”query”
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco} span.s1 {color: #9a1867} span.Apple-tab-span {white-space:pre}
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
return new TwitterSearchCursor();
}
這Query啥事都不用作, 回傳個TwitterSearchCursor就好了
從這看出玄機了嘛? 我們就是要利用Cursor, 在Cursor裡面實作連接網路取得資料的動作
後面我懶得寫解釋了, MBP快沒電了, 以下就是TwitterSearchCursor的Source:
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco} p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco; min-height: 15.0px} p.p3 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco; color: #9a1867} p.p4 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco; color: #777777} p.p5 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco; color: #429073} p.p6 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco; color: #382ffa} p.p7 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco; color: #0023c7} span.s1 {color: #9a1867} span.s2 {color: #000000} span.s3 {color: #0023c7} span.s4 {color: #382ffa} span.s5 {color: #8dafc9} span.Apple-tab-span {white-space:pre}
public class TwitterSearchCursor extends AbstractCursor {
static class Tweet {
String created_at = ””;
String id_str = ””;
String text = ””;
}
ArrayList<Tweet> tweets = new ArrayList<Tweet>();
@Override
public String[] getColumnNames() {
// TODO Auto-generated method stub
return new String[] {”_id”, “created_at”,“id_str”,“text”};
}
@Override
public int getCount() {
if(tweets.size() == 0) {
loadData();
}
Log.d(“AAAA”, “cnt=”+tweets.size());
return tweets.size();
}
@Override
public double getDouble(int arg0) {
// TODO Auto-generated method stub
return 0;
}
@Override
public float getFloat(int arg0) {
// TODO Auto-generated method stub
return 0;
}
@Override
public int getInt(int index) {
return index;
}
@Override
public long getLong(int index) {
return index;
}
@Override
public short getShort(int index) {
return (short)index;
}
@Override
public String getString(int index) {
Tweet t = tweets.get(mPos);
switch(index) {
case 1:
return ””+t.created_at;
case 2:
return ””+t.id_str;
case 3:
return ””+t.text;
}
return null;
}
@Override
public boolean isNull(int arg0) {
// TODO Auto-generated method stub
return false;
}
private void loadData() {
HttpClient client = AndroidHttpClient.newInstance(“TwiAndroid”);
HttpGet get = new HttpGet(“http://search.twitter.com/search.json?q=mlb”);
try {
HttpResponse resp = client.execute(get);
BufferedReader reader = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
String line = null;
StringBuilder r = new StringBuilder();
while((line = reader.readLine())!=null) {
Log.d(“AAAA”, line);
r.append(line);
}
JSONObject obj = new JSONObject(r.toString());
JSONArray ary = obj.getJSONArray(“results”);
for(int i=0;i<ary.length();i++) {
Tweet t = new Tweet();
t.created_at = ary.getJSONObject(i).getString(“created_at”);
t.id_str = ary.getJSONObject(i).getString(“id_str”);
t.text = ary.getJSONObject(i).getString(“text”);
tweets.add(t);
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
其實就是利用getCount()時去存取Twitter search API
為什麼要在這時候呢? 主要是我在remote端用的是Cursor來bind這些資料, 第一個會被呼叫到的是getCount()
由於整個Cursor會被跑在Provider所在的process並非跟caller同一個process, 所以heap也是吃在provider
這種利用Custom cursor應該可以組合出其他不同的作法跟應用…
透過Facebook Android SDK去sign in Facebook其實很單純, 只需要這樣幾行code:
if (!facebook.isSessionValid()) {
facebook.authorize(this, new String[] { “publish_stream” }, new AuthDialogListener());
}
其中AuthDialogListener是繼承自Facebook.DialogListener, 因為Authentication dialog是由Facebook SDK處理的, 應用程式只要負責處理onComplete, onError等等callback
但單純這樣的code會在手機內有安裝Facebook for Android時發生問題
因為後來Facebook導入所謂Single sign on的機制, 讓Application跟Facebook for Android是可以用同一使用者的
Single sign on的原理很簡單, 只是透過Facebook for Android去做proxy login的動作而已, Application只消去Facebook註冊一個自己的package signature就可以了
但由於原本舊的Login視窗是包在SDK內一起被build進applciation package, 但在SSO機制內, Dialog是由Facebook for Android出的, 因此如果沒有加下面的code, AuthDialogListener的callback都不會被call到
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Monaco} span.s1 {color: #9a1867} span.s2 {color: #0023c7} span.Apple-tab-span {white-space:pre}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
facebook.authorizeCallback(requestCode, resultCode, data);
}
這是由於Facebook SDK是用startActivityForResult去call Facebook for Android, 因此application必須加上這行自己接它的callback再轉送給原本的listener
如果使用過Google MapView你會發現G社會叫你用你的package signature去申請map key(參考這邊)
那MapView本身又是如何取得你的package signature的呢?
其實很簡單, 可以透過目前的Context用Context.getPackageName()取得package name, 然後再利用package name去PackageManager.getPackageInfo()去查詢package的資訊(PackageInfo), 裡面有項資訊”signatures”就是我們所要的(一個package是可以被sign多次的)
Facebook Android SDK所提供的Single sign on (SSO)也是得先在server端註冊你的package signature, 這樣它可以避免別人偷用你的API key去做single sign on的動作(因為別人的AP可能不會跟你sign同一個signature)
但Facebook的SSO是透過Facebook for Android來做login的動作的, 你的application是透過startActivityForResult去叫Facebook for Android, 這牽涉到兩個不同的package, 如果依上面的例子的話, MapView其實是鑲嵌在你的UI上, 跟你的application是同一個package, 所以可以拿context來取得這資訊
Facebook for Android是如何取得call他的人的signature呢?
其實也是用PackageManager.getPackageInfo() 來取得, 但它如何知道呼叫他的人是哪個package的?
這只要組合兩個API就可以辦到了
第一個是Binder.getCallingUid() , 這可以讓你取得你的Calling process的UID
第二個是PackageManager.getPackagesForUid() , 這可以讓你取得屬於這UID的所有package
或許這邊你可能會有點疑問, 不是不同的package可以share UID嗎? 這樣取出來的package可能有很多個, 我怎知道用哪個? 我們目的只是要取得signature, 所以用哪個都一樣, 因為share UID的必要條件就是這些相同UID的package都是要sign同一把key, 所以只要找出所有package共通的signature就大功告成了
抓到Gingerbread之後本來要build一份給Nexus one的, 後來Build break後就懶得管它, 昨天稍微研究一下解法, 果然跟我想的差不多
Builde break的點在於libcameraservice, 主要是某H公司的camera driver並沒open source, 要build必須先從Nexus One取出binary來build, 但偏偏Gingerbread的libcameraservice跟camera driver的interface有變, 像是HAL_getNumberOfCameras, HAL_getCameraInfo在舊版的driver並不存在
解決的方法有兩種:
目前實驗出來的結果很…殘念..還是沒能讓它的camera可以正常使用… orz
第一個方法比較簡單, 只要改makefile: frameworks/base/services/camera/libcameraservice/Android.mk
把USE_CAMERA_STUB:=true變成always的true (就是把一些判斷都mark掉就好)
這樣build出來的就會是用camera stub
第二個方法可以參考這邊
這方法只是加個wrapper把需要的幾個function加上去, 並包裝舊的call
一樣要改frameworks/base/services/camera/libcameraservice/Android.mk, 只是多個目錄放wrapper
Cyanogenmod也是用同一招解決的, 不同的是, 它並沒多生出一個lib來放這warpper, 它是直接改在CameraService.cpp
其實是同一個方法, 由於它多生一個define : BOARD_USE_FROYO_LIBCAMERA來enable/disable這段code(畢竟Cyanogenmod不是只有for N1), 所以要在 device/htc/passion-common/BoardConfigCommon.mk 裡面加上 BOARD_USE_FROYO_LIBCAMERA := true 來把它打開
基本上, 兩種方法N1的Camera都等於廢了, 所以用哪一種都一樣
雖然Android可以透過ddms抓screen shot, 不過這樣畢竟沒iOS方便, iOS只要按個熱鍵就可以抓了, 不用接著電腦
要在Android上實現這功能其實也不難(不過,應該只能跑在rooted device吧?本文介紹的是直接跟AOSP一起build用platform certificate)
第一個直覺想到的是直接access framebuffer, 不過這樣實在太醜了, 我想要做的是直接利用Android既有的framework
想到framebuffer在往上一層就是surfaceflinger了, 稍微掃一下code果然發現有captureScreen這種東西, 所以下一步就是grep一下找看看範例囉!!沒想到, 運氣真好, 一下子就在 frameworks/base/services/surfaceflinger/tests/screencap 這邊找到範例, 還是一個standalone的程式
不過我想做的是直接收一個Intent就可以把screen capture到SD卡, 所以這個東西還要做進一步包裝, 於是動手把它包裝成JNI, 並寫了一個簡單的BroadcastReceiver去呼叫他…..花不到一個小時…搞定!!! XD
不過, 這不是我原本想做的東西….繼續改… :P
其實這是一個沒啥用的技巧(至少我還想不出應用), 就是加上一個像是浮水印的東西在畫面上, 不管在哪都會出現的, 像是這樣:

因為這個image要讓他一直不會消失, 所以他並不會是一個Activity, 因為Activity會有他的生命週期, 離開後就沒了, 所以要透過WindowManager去加上這個ImageView, 而且加上View的動作必須要給Background service去處理(這樣也就可以去做一些變化, 或動畫之類的)
首先, 要建立一個Service, 要達成這目的也很簡單, 在onCreate加上:
WindowManager wm = (WindowManager) this
.getSystemService(Service.WINDOW_SERVICE);
ImageView logo = new ImageView(this);
logo.setImageResource(R.drawable.onepieceoverlay);
WindowManager.LayoutParams lparam = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, 0, PixelFormat.TRANSLUCENT);
lparam.gravity = Gravity.RIGHT | Gravity.TOP;
wm.addView(logo, lparam);
這樣就好了, 其實很簡單, 就是透過WindowManager在system overlay上加個view
但這樣還不會work, 因為少了一個permission, 所以必須在AndroidManifest裡面加上
<uses-permission android:name=”android.permission.SYSTEM_ALERT_WINDOW”></uses-permission>
這樣就好了
不過system overlay沒辦法接touch event, 所以這邊放的view真的只是拿來放好看的
在Android Gingerbread引入了StorageManager這東西, 似乎是為了OBB(Opaque Binary Blobs)這功能而來的, 不過, 似乎是也還沒把tool含到SDK內, 也沒很詳細的說明文件, 所以看起來現在好像也沒啥人去用這個, 實際上試了一下, 最後還是有問題, 搞不好還有bug, 懶得繼續追下去了, 不過大致上理解怎來利用這東西
OBB是一個含有加過密的檔案系統的檔案, 唸起來頗繞口, 不過如果對Linux有點熟悉的話, 它是一個磁碟映像檔(image file), 最後會以loopback device的方式掛載入Android內(Android底層是Linux, 採用這種方式也不足為奇)
由於是加密過的, 所以, 應用程式可以把需要保密的內容放到這個檔內, 比如說私密的通訊資料庫啦, 或是見不得人的照片…(呃, 我講到哪去了)
要建立一個OBB的檔案需要幾個tool:
mkobb.sh
pdkdf2gen
obbtool
由於目前SDK好像還沒這幾個東西, 所以必須從aosp裡面去找, 前兩者是一組的, 要一起配合, 而且mkobb.sh只能在Linux底下跑, 如果你嘗試在Mac上跑(像我一樣), 是會失敗的 (不過想想, 目前沒支援Mac也很合理)
Linux下, 有幾個kernel module是一定必要被載入的
sudo modprobe cryptoloop
sudo modprobe twofish
sudo modprobe vfat
這樣你才可以建立一個被加密過的loopback file system image
執行的指令如下:
mkobb.sh -d obbdir -k password -o obbfile
-d 後面那個obbdir可以改成任何一個存在的目錄, 建立好的obb檔會幫你含入所有這目錄裡面的檔案, -k 後面輸入加密的密碼, -o 後面加入輸出的檔名
執行後內容會如下:
最後一行裡的”5f88a3619e6544ef”這個salt很重要, 要抄下來, 之後會用到
接下來就要用obbtool加簽章了, obbtool用法如下:
加簽章: obbtool a -n com.yourcompany.app -v 1 obbtest.obb -s 5f88a3619e6544ef
移除: obbtool r ~/Dropbox/obbtest.obb
-n 後面是package name, 必須要跟你的應用程式的package name相同, -v 後面則是自定的版本, -s 後面接的就是剛剛做mkobb.sh後產生的salt了
這樣這個obb檔就完成了, 可以把它放到手機sdcard或其他地方讓你的程式存取
在程式內掛載obb的話就要用到StorageManager了:
不過目前, 我碰到的狀況就是, 明明state已經變成MOUNTED, 但我就是取不到mounted path, 怪怪的
今天晚上的GTUG請來了Tony Chan講GB的新features, 不過, 這些內容其實大部分都已經在Android網站上看過很多次了, 提起不了啥興趣, 雖然是對StorageManager有點興趣, 但卻不在這次講的內容, 可見這東西真的並不是個完成品, 不足以拿出來推
唯一讓我有點感興趣的是Google Analytics for Android, 所以回來的時候在車上稍微研究了一下
目前這東西, 說起來有點陽春, 把user行為的tracking定義成兩個分類 - pageview和event
pageview這觀念感覺就是從Web上來的, 但mobile application其實也沒啥page, 硬要分的話, 可以拿Activity來當做一個page吧, 不過有很多user interaction是在同一個Activity發生, 所以這分類應該是拿來應用在追蹤使用者使用某大功能的頻率
既然pageview不能夠表示in-activity的互動行為, 其實還可以拿”event”來用, 可以把button click, gesture之類的當做event來記錄分析
不過, 對於pageview和event其實沒有很明確的分野, developer可以在任何時候任何地方使用這兩者記錄, 並沒有特別強制性(比如說只能在activity用pageview或是pageview只能每個activity記錄一次之類的), 所以要胡搞也可以啦 (不過那就失去意義了)
比較恐怖的功能是可以允許developer自定變數, 如果把使用者個人資訊一起送上去, 就不妙囉
這整個功能上還稍嫌陽春, 如果要tracking使用者的使用流程, 這樣的東西並沒辦法做到
這部份有被問了幾個問題, 回來後我稍微查了一下:
1. 使用權限問題: 要不要使用者先同意收集才可以使用? 很不幸的, 這一點似乎是developer自由心證了, 並沒有任何強制步驟是使用者同意後才可以使用, 雖然使用這lib必須在manifest加上兩個uses-permission:
<uses-permission android:name=”android.permission.INTERNET” />
<uses-permission android:name=”android.permission.ACCESS_NETWORK_STATE” />
但這兩個, 一個是存取Internet, 一個是存取網路狀態的, 完全跟Privacy無關
2. Tony提到, 在沒有網路的時候會先keep在local的database等有網路時在上傳資料, 我當時提的問題是, 是否會有background service來做這件事, 還是AP launch之後才會真正去檢查網路並重送, 是只會有一個background service來做這件事? 還是每個AP獨立, 我會想到這問題有兩點, 第一點是資料庫問題, 如果每個AP負責自己的, 那每個AP會有各自獨立的資料庫, 而不會統一管理, 第二個是background processes數量問題
在會中時沒得到啥答案, 所以我只好自己簡單的追了一下, 基本上, 這設計沒我想的複雜, 簡單很多, 沒任何background service, 只有一個AnalysticsReceiver但看起來是來處理com.android.vending.INSTALL_REFERRER而非Connectivity change
所以這問題的答案就是, 它其實只是一個AP內的thread來處理而已(應該是NetworkDispatcher的dispatcherThread)
而且tacking code是存於各個AP的data目錄下一個叫google_analytics.db資料庫內, 所以只要取得這資料庫就可以取得了
整個設計上應該可以再加強, 尤其是保護User privacy這部份, 不然有可能被告吧? 我認為比較好一點的設計應該是在Market那隻AP裡面放個service來負責上傳analytics的資料, 只要提供API給AP去呼叫這隻service就好, 這樣第一步可以透過Android permission的機制先卡第一關, 而且可以比較容易統一設計一個end user agreement的dialog, 當AP第一次使用的時候跳出來取得user同意, 此外, 亦可以自動在網路狀態從無到有時自動把未送出的資料送出, 而非一定要AP正在使用的時候
Visual UI design的工具在很多平台跟語言其實都有, 當然有好有壞啦, Android ADT也是有的, 所以並不稀奇, 不過Android有的是更好用的 - hierarchyviewer
一般WYSIWYG UI editor著重的是在開發時期拉UI layout的部份, 但更進階的通常就沒著墨了, ADT上的UI builder並不是很強, 真正要能夠調出複雜而且是自己想要的UI光靠那個也並不容易, 還是得對一堆layout有相當程度的了解才能比較得心應手, hierarchyviewer是屬於更進一步的工具, 他可以檢查做出來的成品的UI layout (當然也可以想辦法偷人家的看 :P), 由於Android上的layout設計是階層式的, 所以可以攤成樹狀, 由hierarchyviewer就可以很明顯看出各layout從屬關係, 並且可以從screen shot部分的對應看出某一段layout成果的長像是如何
此外還有一些更強的功能, hierarchyviewer可以幫你把整個畫面輸出成PSD 檔(Photoshop的檔案), 這不是只是簡單的輸出平面的screenshot, 而是利用PSD檔可以有層次(layer), 把畫面上不同的元件分作為不同的layer疊起來, 這樣從Photoshop就可以一目了然的看出是不是有哪些沒秀出來的原因是被另一個蓋到的
個人覺得最棒的是那個紅綠燈, 設計不良的UI的效能自然是很差, 這個紅綠燈就是幫助你把效能不好的地方抓出來, 分作三部分, layout, measure, draw, 可以由這三部分分析去進一步改善整體UI的效能
Android上有個很煩人的就是(尤其是某公司的手機), 如果畫面上有可以輸入的框, 預設會focus在那上面, 某些手機(或說某些公司的手機), 預設變成只要那邊取得focus, 就會跳出軟體鍵盤, 這對很多User來說, 會有點煩(要看場合啦)…..
要在code裡面把SIP預先藏起來的話, 要用這段code:
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);