前幾天看到朋友寫如何用Charles Proxy來debug REST API, 突然就想到了這個問題, Charles Proxy是一個不錯的工具, 我自己也蠻常用的 , 除了可以用來debug HTTP以外, 其實HTTPS也可以(參照SSL proxying) , 只要在手機上安裝好它的SSL certificate即可

不過這不是這篇要講的重點, 這種debugging的方式原理即是用Proxy作為一個中間人來紀錄HTTP/HTTPS的傳輸內容 , 這也帶來一個問題, 也就是你的資料是可以這樣簡單的暴露出去, 這種即是所謂的MITM (Man in the middle) 中間人攻擊

graph LR; Client-->Proxy[Proxy - Middle man]; Proxy[Proxy - Middle man]-->Client; Proxy[Proxy - Middle man]-->Server; Server-->Proxy[Proxy - Middle man];

一般來說, 用了SSL/HTTPS後, 也不是那麼容易安插一個中間人的 ,像Charles Proxy這樣的東西, 還是需要使用者自己去手機上的安全性設定裝信任憑證(trusted certificate), 一般常見的問題反而來自於開發者本身 , 很多人常以為走了HTTPS就安全了, 卻常常為了debug方便, 或是省錢用Self-signed certificate 而去覆寫了系統預設的 X509TrustManager, 以至於整個檢查機制被跳過, 真的漏洞都來自於人的惰性, 更多資訊可以參考:

  1. 窃听风暴: Android平台https嗅探劫持漏洞
  2. Android安全之Https中间人攻击漏洞
  3. Android HTTPS中间人劫持漏洞浅析
  4. Android客户端安全 -> HTTPS敏感数据劫持漏洞
  5. Android App 安全的HTTPS 通信
  6. 手機應用程式開發上被忽略的 SSL 處理
  7. How To: Use mitmproxy to read and modify HTTPS traffic
  8. INTERCEPTING ANDROID SSL / HTTPS TRAFFIC

該避免去寫的, 就該避掉, 以免產生不必要的漏洞, 但不過也有可能碰到使用者的手機被(有意/無意)安裝了攻擊者的憑證到系統的信任憑證中, 那麼碰到這種, 應用程式本身該如何保護自己?

這邊有兩個方式可以採用 -

  1. certificate pinning
  2. 在程式中寫死certificate

以下的範例以Android最紅, 常被使用的Retrofit為範例(雖說是Retrofit, 但其實是在okhttp動的手腳)

Certificate Pinning

或叫HTTP Public Key Pinning (HPKP), 簡單的說, 就是在程式內綁定Certificate的public key, 如果今天中間人存在, certificate不同了, 連接就不會成功

先來看範例

OkHttpClient client = new OkHttpClient.Builder()
        .certificatePinner(new CertificatePinner.Builder().add("cdn.rawgit.com", "sha256/p0962nIqD0mv1APQ1mgmRiuwrhZXBuj+t6dey/Adk0U=").build())
        .build();
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://cdn.rawgit.com/julianshen/cpblschedule/")
        .addConverterFactory(GsonConverterFactory.create())
        .client(client)
        .build();

這邊所動的手腳(跟Retrofit沒啥鳥關係)是在OkHttpClient上加上一個CertificatePinner, 利用了certificate pinner加上了一個host name跟public key的對應, 以這例子來說: “sha256/p0962nIqD0mv1APQ1mgmRiuwrhZXBuj+t6dey/Adk0U=” 這個public key對應的是 “cdn.rawgit.com” , 因此碰到"cdn.rawgit.com"來的url, 會檢查public key是否符合, 這邊public key的參數字串是要以"sha1/“或"sha256/“開始的, 依使用的演算法而不同

但又要怎取得這串"天書"呢?

很簡單, 首先確認你的電腦裡面有沒安裝openssl , 有的話就用以下的指令:

openssl s_client -connect cdn.rawgit.com:443 -servername cdn.rawgit.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl
dgst -sha256 -binary | openssl enc -base64

最後產生的內容如下

depth=2 /C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Certification Authority
verify error:num=20:unable to get local issuer certificate
verify return:0
writing RSA key
p0962nIqD0mv1APQ1mgmRiuwrhZXBuj+t6dey/Adk0U=

所以我們要填的參數就是 “sha256/p0962nIqD0mv1APQ1mgmRiuwrhZXBuj+t6dey/Adk0U=”

關於HPKP的openssl相關的指令可以參考Public Key Pinning

如果你用Charles Proxy當中間人來測試這段程式的話, 你會得到下面這樣的exception

javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
                                            Peer certificate chain:
                                            sha256/ENlqFHtfARof3AK50Hbc1sj47M5hWhc5kQ5Z2vyfxq4=: CN=rawgit.com,OU=PositiveSSL Multi-Domain,OU=Domain Control Validated
                                            sha256/mXmLzo8k5ANwi11PlLSW/b4OC/Anjw1OeACDyZxD/WM=: C=NZ,ST=Auckland,L=Auckland,O=XK72 Ltd,OU=http://charlesproxy.com/ssl,CN=Charles Proxy Custom Root Certificate (built on Jlnmbp-retina.local\, 11 十二月 2015)
                                            Pinned certificates for cdn.rawgit.com:
                                            sha256/p0962nIqD0mv1APQ1mgmRiuwrhZXBuj+t6dey/Adk0U=

在程式中寫死certificate

這方法聽起來暴力了一點, 但原理其實跟前一個沒啥太大差別, 只是一個寫死public key一個寫死certificate

一樣先來看個範例:

String cert = ""
        +"-----BEGIN CERTIFICATE-----\n"
        +"MIIFdjCCBF6gAwIBAgIRAPbTBHdHHseM4hArA7n6uREwDQYJKoZIhvcNAQELBQAw\n"
        +"gZAxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO\n"
        +"BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTYwNAYD\n"
        +"VQQDEy1DT01PRE8gUlNBIERvbWFpbiBWYWxpZGF0aW9uIFNlY3VyZSBTZXJ2ZXIg\n"
        +"Q0EwHhcNMTYwMTAxMDAwMDAwWhcNMTcwMTEzMjM1OTU5WjBbMSEwHwYDVQQLExhE\n"
        +"b21haW4gQ29udHJvbCBWYWxpZGF0ZWQxITAfBgNVBAsTGFBvc2l0aXZlU1NMIE11\n"
        +"bHRpLURvbWFpbjETMBEGA1UEAxMKcmF3Z2l0LmNvbTCCASIwDQYJKoZIhvcNAQEB\n"
        +"BQADggEPADCCAQoCggEBALD2sUNSYp2R+mSEMF2qKvMS780qTkltvqP4rwEGKOLV\n"
        +"rR5QQWo8vzSlgZvxVsguRHi0pPBtVAH794L43tD+IuyQlDlNU2qc1aMqBkn3S2wN\n"
        +"Way8BS9w80pgeFnObZiFJtPI9pdwcB72Bgq8Nlc25oVrDVWr/Q8nLIKS/9FkNs+C\n"
        +"MPU00vGFZSFbR7s15ORj9+qPCskWZcHpQ+m9EKmZD3IVKj3QQyBD17cBoVkYoIpj\n"
        +"I8/r+NfVfKetlsB7Pcv8P3yLFVFC4+PGrnW9TLZK9aRtbDTvjElXwCQIPK5B2fKw\n"
        +"SJK26IJPDX0r1JkeuR53Afr509iEx3xAHcRz/kR5uo8CAwEAAaOCAf0wggH5MB8G\n"
        +"A1UdIwQYMBaAFJCvajqUWgvYkOoSVnPfQ7Q6KNrnMB0GA1UdDgQWBBSI83Kqafo7\n"
        +"kg9cmyT1vnceH4fAUjAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNV\n"
        +"HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwTwYDVR0gBEgwRjA6BgsrBgEEAbIx\n"
        +"AQICBzArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQ\n"
        +"UzAIBgZngQwBAgEwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5jb21vZG9j\n"
        +"YS5jb20vQ09NT0RPUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNy\n"
        +"bDCBhQYIKwYBBQUHAQEEeTB3ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LmNvbW9k\n"
        +"b2NhLmNvbS9DT01PRE9SU0FEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0Eu\n"
        +"Y3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wSwYDVR0R\n"
        +"BEQwQoIKcmF3Z2l0LmNvbYIVY2RuLW9yaWdpbi5yYXdnaXQuY29tgg5jZG4ucmF3\n"
        +"Z2l0LmNvbYINcmF3Z2l0aHViLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEASx3e6y9e\n"
        +"9F67N5NIDausDHDL+/fz6uj2DDNJdaQvALAYDV8hKpz7+QGotlQfI042U2i83J7m\n"
        +"DGZiKDpzaXa7IFfGiq6PFPUjntElHoU2E4vRb5LApg1sJ5YueYmRd3X7x0/jPYg6\n"
        +"TiTtHyXOnlMuqL2FUYJM0BP7cMfOppvUF0R2zHUVA0rVHuLrSStg8bMg8aVUIkKg\n"
        +"n3NS+eg4ofo95jaMRVykhLnylkYBk9dWBM6B19Yw7LgQd94MZSa+Xix4HxeFgvAM\n"
        +"BF+oeDMaY7mEN4Xm5hnrIS1FElRDq/ckpDV8JVpR4SQqB+tzSZPPIiep8EvgRDzd\n"
        +"2EkL187FF3MQ+w==\n"
        +"-----END CERTIFICATE-----\n";

X509TrustManager trustManager = null;
SSLSocketFactory sslSocketFactory = null;

try {
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(new Buffer().writeUtf8(cert).inputStream());
    if (certificates.isEmpty()) {
        throw new IllegalArgumentException("expected non-empty set of trusted certificates");
    } else {
        char[] password = "password".toCharArray(); // Any password will work.
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, password);

        int index = 0;
        for (Certificate certificate : certificates) {
            String certificateAlias = Integer.toString(index++);
            keyStore.setCertificateEntry(certificateAlias, certificate);
        }

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);

        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
        if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
            throw new IllegalStateException("Unexpected default trust managers:"
                    + Arrays.toString(trustManagers));
        }
        trustManager = (X509TrustManager) trustManagers[0];

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[] { trustManager }, null);
        sslSocketFactory = sslContext.getSocketFactory();
    }
} catch (CertificateException e) {
    e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
} catch (KeyStoreException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} catch (KeyManagementException e) {
    e.printStackTrace();
}

OkHttpClient client = new OkHttpClient.Builder()
        .sslSocketFactory(sslSocketFactory, trustManager)
        .build();

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://cdn.rawgit.com/julianshen/cpblschedule/")
        .addConverterFactory(GsonConverterFactory.create())
        .client(client)
        .build();

看到這麼長的東西大概就不會想看了吧?(老實說我也寫的很懶, 這邊要特別提到有用到一個Class叫做Buffer,這是來自於okio, 蠻方便的東西), 跟前一個不同的地方在於, 前一個利用了CertificatePinner, 而這一個則是改了sslSocketFactory的TrustManager(喂!!!前面不是隱約有提到這樣不太好?!), 這個TrustManager全部也只信任這一個certifcate, 如果碰到其他的, 就會發生以下的exception:

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
        at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:328)

那, 上面那一大坨憑證資料是怎樣取得的? 方法其實差不多:

openssl s_client -connect cdn.rawgit.com:443 -showcerts

會得到這樣的結果:

CONNECTED(00000003)
depth=2 /C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Certification Authority
verify error:num=20:unable to get local issuer certificate
verify return:0
---
Certificate chain
 0 s:/OU=Domain Control Validated/OU=PositiveSSL Multi-Domain/CN=rawgit.com
   i:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA
 1 s:/OU=Domain Control Validated/OU=PositiveSSL Multi-Domain/CN=rawgit.com
   i:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA
 2 s:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA
   i:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Certification Authority
 3 s:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Certification Authority
   i:/C=SE/O=AddTrust AB/OU=AddTrust External TTP Network/CN=AddTrust External CA Root
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIFdjCCBF6gAwIBAgIRAPbTBHdHHseM4hArA7n6uREwDQYJKoZIhvcNAQELBQAw
gZAxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTYwNAYD
VQQDEy1DT01PRE8gUlNBIERvbWFpbiBWYWxpZGF0aW9uIFNlY3VyZSBTZXJ2ZXIg
Q0EwHhcNMTYwMTAxMDAwMDAwWhcNMTcwMTEzMjM1OTU5WjBbMSEwHwYDVQQLExhE
b21haW4gQ29udHJvbCBWYWxpZGF0ZWQxITAfBgNVBAsTGFBvc2l0aXZlU1NMIE11
bHRpLURvbWFpbjETMBEGA1UEAxMKcmF3Z2l0LmNvbTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBALD2sUNSYp2R+mSEMF2qKvMS780qTkltvqP4rwEGKOLV
rR5QQWo8vzSlgZvxVsguRHi0pPBtVAH794L43tD+IuyQlDlNU2qc1aMqBkn3S2wN
Way8BS9w80pgeFnObZiFJtPI9pdwcB72Bgq8Nlc25oVrDVWr/Q8nLIKS/9FkNs+C
MPU00vGFZSFbR7s15ORj9+qPCskWZcHpQ+m9EKmZD3IVKj3QQyBD17cBoVkYoIpj
I8/r+NfVfKetlsB7Pcv8P3yLFVFC4+PGrnW9TLZK9aRtbDTvjElXwCQIPK5B2fKw
SJK26IJPDX0r1JkeuR53Afr509iEx3xAHcRz/kR5uo8CAwEAAaOCAf0wggH5MB8G
A1UdIwQYMBaAFJCvajqUWgvYkOoSVnPfQ7Q6KNrnMB0GA1UdDgQWBBSI83Kqafo7
kg9cmyT1vnceH4fAUjAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNV
HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwTwYDVR0gBEgwRjA6BgsrBgEEAbIx
AQICBzArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQ
UzAIBgZngQwBAgEwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5jb21vZG9j
YS5jb20vQ09NT0RPUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNy
bDCBhQYIKwYBBQUHAQEEeTB3ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LmNvbW9k
b2NhLmNvbS9DT01PRE9SU0FEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0Eu
Y3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wSwYDVR0R
BEQwQoIKcmF3Z2l0LmNvbYIVY2RuLW9yaWdpbi5yYXdnaXQuY29tgg5jZG4ucmF3
Z2l0LmNvbYINcmF3Z2l0aHViLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEASx3e6y9e
9F67N5NIDausDHDL+/fz6uj2DDNJdaQvALAYDV8hKpz7+QGotlQfI042U2i83J7m
DGZiKDpzaXa7IFfGiq6PFPUjntElHoU2E4vRb5LApg1sJ5YueYmRd3X7x0/jPYg6
TiTtHyXOnlMuqL2FUYJM0BP7cMfOppvUF0R2zHUVA0rVHuLrSStg8bMg8aVUIkKg
n3NS+eg4ofo95jaMRVykhLnylkYBk9dWBM6B19Yw7LgQd94MZSa+Xix4HxeFgvAM
BF+oeDMaY7mEN4Xm5hnrIS1FElRDq/ckpDV8JVpR4SQqB+tzSZPPIiep8EvgRDzd
2EkL187FF3MQ+w==
-----END CERTIFICATE-----
subject=/OU=Domain Control Validated/OU=PositiveSSL Multi-Domain/CN=rawgit.com
issuer=/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA
---
No client certificate CA names sent
---
SSL handshake has read 6716 bytes and written 456 bytes
---
New, TLSv1/SSLv3, Cipher is DHE-RSA-AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
    Protocol  : TLSv1
    Cipher    : DHE-RSA-AES256-SHA
    Session-ID: ACFED5197F4B5F64DBAAC97A47380CD9283EFD1797E27EF43A215B06982698F4
    Session-ID-ctx: 
    Master-Key: 856DFB81C863F461FE75EE11E633EA5CAB3CD6683563F597F406EF19AB37CD3F45B4FE659AB8726F9291360CBE856F48
    Key-Arg   : None
    Start Time: 1475223830
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---

BEGIN到END那段剪貼下來即是

缺點

這兩個方法有一個極大的缺點, 如果沒有解決方案還是最好別用(啊講那麼多, 最後不能用?!) , 這缺點就是SSL Certificate是有時效性的, 因此server端的憑證是會換會改變的, 只要server端一換, 就可能造成client端的失效, 所以如果要使用這兩個方法, 前提必須先解決的是如何更新Client端要檢查的public key或certificate, 這邊就不討論這部份的機制了, 這應該有很多方法可以來作才對

#好久沒寫關於Android的內容了

認真說的話, 這也算不上啥非官方API, 算是一個為了抓取Play store上資訊的一個小小工具: PlaystoreUtil

現在很多網路的服務, 大多有提供開放的REST API來供人寫原生的程式使用, 當然也有非常多並沒有, 像是Play store, 目前就沒開放的API可供存取, 剛好想要有個東西可以查詢某個app在play store上是屬於啥分類的, 所以就乾脆自己自製一個囉…

現在的網頁, 大多結構性很好, 所以就算沒有REST API, 其實也不難處理, 搭配上 jsoup , 可以說輕而易舉

jsoup是一個可以用css selector來解析html的Java函式庫, 有了這個, 解析html可以不用辛苦的爬dom tree, 只要幾行簡單的程式即可:

Document doc = Jsoup.connect("http://example.com/").get;

Elements links = doc.select("a[href]"); // a with href
Elements pngs = doc.select("img[src$=.png]");

再來看看play store

先看看每個app的資訊畫面, 以Facebook為例, 它的url是 –

https://play.google.com/store/apps/details?id=com.facebook.katana&hl=ja

很明顯的, id後面是package name, 另外如果加上"&hl=“可以指定語言, 然後再看到頁面上:

在Facebook (公司名稱)下方有個分類, 可以使用Chrome的開發人員工具(我比較習慣這個), 找到這個連結的css class名稱是”document-subtitle category“,而名稱則在它底下的一個span, 這span有個屬性itemprop, 值是genre

因此, 透過以下這段code就可以取到類別名稱囉

Document doc = Jsoup.connect("https://play.google.com/store/apps/details?id=" + packageName + "&hl=" + locale.getISO3Language()).get();
Elements elements = doc.select("span[itemprop=genre]");

當然, 這方法不只適用於play store, 其他網頁也可以嘗試用這個方法來取得資料

詳細的範例在: https://github.com/julianshen/PlaystoreUtil

Android library project是為了解決Android開發中在不同專案間分享原始碼以及資源檔(resource)而出現的, 傳統的jar並未考慮資源檔的問題, 因此便需要靠Android library project來解決

目前, Android library project已經被廣泛運用, 舉凡ActionbarSherlock, Facebook Android SDK, 很多都已採用這形式

不過現在一般用法還是比較廣泛應用在跟UI相關這類的應用上, 這也合理, 這類的應用常需要包含原始碼和資源檔, 不過它也適合在其他應用上, 舉個例子(好吧, 這例子有點不清不楚), 我們也有可能需要讓所有使用某個library project的應用程式自動加上一個Intent Receiver, 假設這receiver實作上是固定的, 並不需要使用的應用程式自行去繼承, 或是, 我們希望某個Activity的實作是被連結到各個應用程式中, 這類應用使用Android library project也是可以辦到的

這類的應用, 通常還需要在AndroidManifest裡宣告, 除了receiver, activity, 也有可能加上一些service, 甚至是permission, 正常來說, 在建置使用了Android library project的專案時, library project裡的AndroidManifest的內容並不會合併到最後的AndroidManifest裡, 以致於, 雖然在library project內這些都被宣告了, 但成品內可能無法被使用, 這解法也很單純, 只要在應用程式(不是library project)的project.properties裡加上:

manifestmerger.enabled = true

Manifest merger似乎存在有一段時間了, 但似乎也沒看到啥正式的官方文件, 不過目前這方法是可行的就是了

看到Square發表的這個Retrofit - http://bit.ly/167v72a 蠻有趣的, 它的目的似乎是試圖的想要去簡化開發REST client, 開發者不用寫太多的邏輯, 只要寫一個Interface跟利用annotation就可以完成一個簡單的REST client:

public interface GitHubService {
@GET("/users/{user}/repos")
List<Repo> listRepos(@Path("user") String user);
}

因為開發者只需要寫interface和annotation, 實質上並不用寫任何的code, 真正實作的部份他用了Proxy class的技巧包裝起來了, 這作法讓我想起來很久之前我在之前的工作幫公司寫的一個legacy系統的wrapper, 那時有很多機器產生的interface, 如果人工一個個實作很浪費時間, Proxy class可以解決掉這一部分的問題, 同樣的在retrofit似乎也是想用這技巧節省實作

但可惜的是, 現在的retrofit並還沒加入OAuth的支援, 因此送出去的API部分並沒被oauth簽章過,不過所幸要解決這一部分也不難, 寫一個Client class搭配Signpost還是可以做到, 這邊範例繼承了OkClient(使用OkHttp) :



因為OkHttp也是一種HttpURLConnection, 因此Signpost搭配DefaultOAuthProvider和DefaultOAuthConsumer即可, 另外初始化RestAdapter時加上這個新的Client即可:

RestAdapter restAdapter = new RestAdapter.Builder() .setClient(new SignedOkClient(mConsumer))


via Blogger http://bit.ly/11OBeFb

前一篇寫了一個自訂義的layout - SimpleCellLayout, 前一個版本的問題就是, 必須是寫程式把child view加進這個layout之中, 而且針對像是欄與行的數目也必須在程式裡設定, 並無法寫到layout xml中, 所以這次的目邊就是要讓這個layout可以像下面這樣用layout xml來擺佈:


在這範例之中, 用到幾個像是col, row, gapsize, cellX這些在原生Android並不存在的屬性, 為了這些屬性, 就需要定義一個attrs.xml在res/values目錄內, attrs.xml 裡面要定義的就是這些樣式描述屬性, 這邊定義了: 給SimpleCellLayout本身用的col(欄數), row(行數), gapsize(間距大小), 以及給他的Child views用的cellX(格子的橫軸位置), cellY(格子的縱軸位置), colspan(格子寬), rowspan(格子高), 除了gapsize我們需要的跟實際螢幕上的大小有關, 所以格式定義為dimension外(就是可以用3dp, 1px這類的值), 其他都是整數就可

這些屬性, 到時候就是要放在xml標籤內的屬性

要用到這些屬性, 需要先在tag裡面定義一個新的name space, 如同在前面範例寫的:

xmlns:celllayout=“http://bit.ly/GEGVYd”
文件內的範例大多是apk/後面接著package name, 不過Android Studio卻是建議使用"res-auto", 定義了這個name space後, 便可以在後面的tag內使用像是"celllayout:col"這樣的屬性了

那在layout內又要怎處理這些屬性? ViewGroup有兩個建構子所需要帶的參數含有AttributeSet, 基本上只要從這兩個建構子處理就好

這範例內的initFromAttributes就是用來從AttributeSet擷取這些屬性, 前面attrs.xml有定義stylable, 所以基本上有一堆相對應的Resource ID可以使用,這邊只處理了col, row, 和gapsize, 因為只有這三個屬性直接跟這layout相關, 至於這layout的child views該怎處理呢? 它就要靠LayoutParams了, 我在SimpleCellLayout內定義了一個CellLayoutParams, 基本上這類別也有一個建構子是用來處理AttributeSet的


這邊比較重要的就是要overwrite掉generateLayoutParams, 因為ViewGroup原有的generateLayoutParams實作的是回傳ViewGroup.LayoutParams, 他並不認得CellLayoutParams, 但SimpleCellLayout靠的是CellLayoutParams來儲存layout所需的資訊, 原本用程式來加入view的範例部份, 我是手動產生一個CellLayoutParams來給child view, 但如果是放在layout xml, 並無法指定要用哪個LayoutParams類別, 這時候就要靠generateLayoutParams了 有趣的是, 雖然是我自訂的layout, 但在我做完這些後, Android studio的UI編輯器, 卻知道怎去依據我的layout把元件畫到正確的位子上面:



via Blogger http://bit.ly/1af6KAB