2012年9月28日 星期五

Android 開發筆記 - 解決 HttpUriRequest/HttpGet/HttpPost 之 Host name may not be null

最近碰到一個 bug 卡關,那就是當我 new HttpGet("http://aaa_bbb.ccc.dddd") 出來,交由 HttpClient 執行時,卻會看到以下訊息:


java.lang.IllegalArgumentException: Host name may not be null
org.apache.http.HttpHost.<init>(HttpHost.java:83)
org.apache.http.impl.client.AbstractHttpClient.determineTarget(AbstractHttpClient.java:497)
org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:487)


當下讓我連問隔壁的同事幾次...難道 hostname 不能有底線?改成 new HttpGet("aaa_bbb.ccc.dddd") 則是:


java.lang.IllegalStateException: Target host must not be null, or set in parameters.
org.apache.http.impl.client.DefaultRequestDirector.determineRoute(DefaultRequestDirector.java:561)
org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:292)
org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:555)
org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:487)


原始程式碼:


String target = "http://aaa_bbb.ccc.dddd";


HttpClient mClient = new DefaultHttpClient();
HttpContext mHttpContext = new BasicHttpContext();
HttpUriRequest mHttpUriRequest = new HttpGet(target);
response = mClient.execute(mHttpUriRequest, mHttpContext);


解法:


String hostname = "aaa_bbb.ccc.dddd";
String target = "http://"+hostname;


HttpClient mClient = new DefaultHttpClient();
HttpContext mHttpContext = new BasicHttpContext();
HttpUriRequest mHttpUriRequest = new HttpGet(target);
response = mClient.execute(new HttpHost(hostname), mHttpUriRequest, mHttpContext);


不曉得這是不是一個 framework 的 bug?還是單純我操作錯誤呢...暫時先這樣解掉吧。


Android 開發筆記 - 解決/取消 EditText 自動 focus 問題

想必還滿常碰到一個 Activity 中,擺幾個 EditText 讓人輸入帳密來送出的表單吧!然而,當送出表單成功後,偶時會很偷懶直接把 mEditText.setText("Info") 且 mEditText.setEnable(false) 來處理,想說這樣又可以重複利用 XD 結果就會碰到開啟 Activity 後,自動 focus 在 EditText 並彈跳出 keyboard 的窘境了。如果動態進行 mEditText.setFocusable(false) 的方式,的確可以避開 focus 的問題,但很奇妙地再動態 mEditText.setFocusable(true) 時,卻會出錯而無法點選該欄位 Orz


最後,找到一些很折衷的辦法...那就是在 EditText 前,先讓某個處的 layout 可以被 focusable 就好 XD 這樣的解法真的是 It just works! 的狀態。


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical" >

       <LinearLayout
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:focusable="true"
              android:focusableInTouchMode="true"
              android:gravity="center_vertical">
              <TextView
                     android:layout_margin="5dp"
                     android:layout_width="120dp"
                     android:layout_height="wrap_content"
                     android:text="@string/title_account"
                     android:textAppearance="?android:attr/textAppearanceLarge">
              </TextView>
              <EditText
                     android:id="@+id/edittext_account"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:hint="@string/account_hint"
                     android:ems="10" >
              </EditText>
       </LinearLayout>
       <LinearLayout
              android:id="@+id/linearlayout_password"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:focusable="true"
              android:focusableInTouchMode="true"
              android:gravity="center_vertical">
              <TextView
                     android:layout_margin="5dp"
                     android:layout_width="120dp"
                     android:layout_height="wrap_content"
                     android:text="@string/title_password"
                     android:textAppearance="?android:attr/textAppearanceLarge">
              </TextView>


              <EditText
                     android:id="@+id/edittext_password"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:ems="10"
                     android:hint="@string/password_hint"
                     android:inputType="textPassword" >
              </EditText>
       </LinearLayout>
</LinearLayout>


2012年9月26日 星期三

Android 開發筆記 - 處理 API 回應 XML 資料的通用解法

如果 API 不是定義的很好,回應得資料格式不依,但至少有符合簡易的 XML 雛形,如:


<name>changyy</name>
<url>http://changyy.pixnet.net</url>
<style>blog</style>


並且很多支 API 格式都不一樣時,就會讓人想看看有沒統一解法,不小心就想到 Javascript property 的用法,可惜我對 Java 不熟,暫時就先用個 map 來處理了:


import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;


import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;


public class QueryReturnObject {
       private Map<String,String> properties = new HashMap<String,String>();
       public QueryReturnObject(String xml) {
              parsing(xml);
       }
       public String getProperty(String key) {
              if( key == null || !properties.containsKey(key) )
                     return null;
              return properties.get(key);
       }
       public String toString() {
              if( properties.size() == 0 )
                     return "No Properties";
              String out = "";
              for( Iterator<Entry<String, String>> it = properties.entrySet().iterator() ; it.hasNext() ; ) {
                     Entry<String, String> item = it.next();
                     out += "Key=["+item.getKey()+"], Value=["+item.getValue()+"]\n";
              }
              return out;
       }
       void parsing(String raw) {
              properties.clear();
              try {
                     XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
                     XmlPullParser parser = factory.newPullParser();
                     parser.setInput( new StringReader ( raw ) );
                     int eventType = parser.getEventType();
                     String fieldName = null;
                     while( eventType != XmlPullParser.END_DOCUMENT ) {
                            switch(eventType) {
                                   case XmlPullParser.START_DOCUMENT:
                                          break;
                                   case XmlPullParser.START_TAG:
                                          fieldName = parser.getName();
                                          break;
                                   case XmlPullParser.END_TAG:
                                          fieldName = null;
                                          break;
                                   case XmlPullParser.TEXT:
                                          if( fieldName != null )
                                                 properties.put(fieldName, parser.getText());
                                          break;
                            }
                            eventType = parser.next();
                     }
              } catch (XmlPullParserException e) {
                     e.printStackTrace();
              } catch (IOException e) {
                     e.printStackTrace();
              }
       }
}


如此一來,任何 API 回應的資料,就直接用 data = new QueryReturnObject(response) 來使用,接著就可以用 data.getProperty("Key") 的方式來存取囉。


2012年9月24日 星期一

Android 開發筆記 - HTTP Post (File Uploading) Progress Report

之前研究 HTTP Post 的方法時,順手實作了支援 Cookie 等功能,久了之後就會想到如何監控上傳進度的部分,原理都很簡單,但要熟整各個 framework 才能方便進行。所幸網路上好心人士非常多,找到這篇 Android Multipart POST with Progress Bar 真的超佛心的,就順手修改一下一點架構。


實作概念:


繼承 org.apache.http.entity.mime.MultipartEntity 物件後,當寫出資料時,記錄已累積的寫出量,在搭配總共要送出的資料量,簡易的除法就能得知目前所進行的進度了。


我自己的粗略使用:


@ HttpPostMultipartEntity.java:


// 九成九一樣,單純改一些變數名稱
// src: http://toolongdidntread.com/android/android-multipart-post-with-progress-bar/
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;


import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;


public class HttpPostMultipartEntity extends MultipartEntity {
       private final HttpPostProgressHandler handler;
       public HttpPostMultipartEntity(final HttpPostProgressHandler handler) {
              super();
              this.handler = handler;
       }
       public HttpPostMultipartEntity(final HttpMultipartMode mode, final HttpPostProgressHandler handler) {
              super(mode);
              this.handler = handler;
       }
       public HttpPostMultipartEntity(final HttpMultipartMode mode, final String boundary, final Charset charset, final HttpPostProgressHandler handler) {
              super(mode,boundary,charset);
              this.handler = handler;
       }

       @Override
       public void writeTo(final OutputStream outstream) throws IOException {
              super.writeTo(new HttpPostOutputStream(outstream, this.handler));
       }

       public static class HttpPostOutputStream extends FilterOutputStream {
              private final HttpPostProgressHandler handler;
              private long transferred;

              public HttpPostOutputStream(final OutputStream out, final HttpPostProgressHandler handler) {
                     super(out);
                     this.handler = handler;
                     this.transferred = 0;
              }

              public void write(byte[] b, int off, int len) throws IOException {
                     out.write(b, off, len);
                     this.transferred += len;
                     if( this.handler != null )
                             this.handler.postStatusReport(this.transferred);

              }

              public void write(int b) throws IOException {
                     out.write(b);
                     this.transferred ++;
                     if( this.handler != null )

                            this.handler.postStatusReport(this.transferred);
              }
       }
}


@ HttpPostProgressHandler.java:


public interface HttpPostProgressHandler {
       void setPostDataSize(long size);
       void postStatusReport(long transferred);
}


HTTP POST Usage:


void doPost( String api_url, String file_path, HttpPostProgressHandler reporter ) {
       List<NameValuePair> mParams = new ArrayList<NameValuePair>();
       mParams.add( new BasicNameValuePair( "path", remote_path ) );
       mParams.add( new BasicNameValuePair( "mode", "upload_file") );
       mParams.add( new BasicNameValuePair( "name", filename) );
       mParams.add( new BasicNameValuePair( "code", edit_code ) );


       // multipart with args
       //MultipartEntity entity=new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
       HttpPostMultipartEntity entity = new HttpPostMultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, reporter);
       for( NameValuePair mItem : mParams )
              try {
                     entity.addPart(item.getName(),new StringBody(mItem.getValue(), Charset.forName("UTF-8")));
              } catch (UnsupportedEncodingException e) {
                     e.printStackTrace();
              }

       File mFile = new File(file_path);
       if( ! mFile.exists() ) {
              System.out.println("File not found:"+local_path);
              return;
       }

       // add cookie
       CookieStore mCookieStore = BasicCookieStore()
       Cookie x = new BasicClientCookie("MyCookie", "MyCookieValue");
       ((BasicClientCookie)x).setPath("/");
       ((BasicClientCookie)x).setDomain("ServerDomain");
       cookieStore.addCookie( x );


       // add file
       entity.addPart( "file", new FileBody( mFile ) );


       // upload api
       HttpPost mHttpPost = new HttpPost(api_url);
       mHttpPost.setEntity(entity);


       // set post data total size
       if( reporter != null )
              reporter.setPostDataSize(entity.getContentLength());


       // use cookie
       HttpClient mHttpClient = new DefaultHttpClient();
       HttpContext mHttpContext = new BasicHttpContext();
       mHttpContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);

       try {
              HttpResponse response = mHttpClient.execute( mHttpPost, mHttpContext );
              HttpEntity result = response.getEntity();
              System.out.println( "Result:" + EntityUtils.toString(result) );
       } catch (Exception e) {
              e.printStackTrace();
       }
}


Main:


new Thread( new Runnable() {
       @Override
       public void run() {
              doPost(
                     "http://mytest/api/upload.php" ,
                     "/mnt/sdcard/test.png",
                     new HttpPostProgressHandler() {
                            long total_size = 0;


                            @Override
                            public void setPostDataSize(long size) {
                                   total_size = size;
                            }


                            @Override
                            public void postStatusReport(long transferred) {
                                   if(total_size == 0)
                                          return;
                                   System.out.println("Status:"+(float)transferred/total_size);
                            }
                     }
              );
       }
} ).start();


Others:


import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;


import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;


public class HttpQueryUsage {
       public static HttpPost createHttpPost(String url, List<NameValuePair> params, List<NameValuePair> files, HttpPostProgressHandler handler) {
              if( url == null )
                     return null;


              MultipartEntity entity=new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
              if( params != null && params.size() > 0)
                     for( NameValuePair item : params )
                            try {
                                   entity.addPart(item.getName(),new StringBody(item.getValue(), Charset.forName("UTF-8")));
                            } catch (UnsupportedEncodingException e) {
                                   e.printStackTrace();
                            }


              if( files != null && files.size() > 0 )
                     for( NameValuePair file : files ) {
                            try {
                                   File src = new File(file.getValue());
                                   if(!src.exists())
                                          continue;
                                   entity.addPart( file.getName(), new FileBody( src ) );
                            } catch (Exception e) {
                                   e.printStackTrace();
                            }
                     }

              HttpPost post = new HttpPost(url);
              post.setEntity(entity);


              if(handler!=null)
                     handler.setPostDataSize(entity.getContentLength());

              return post;
       }
       public static HttpGet createHttpGet(String url, List<NameValuePair> params) {
              if( url != null )
                     return new HttpGet( params == null || params.size() == 0 ? url : url + "?" + URLEncodedUtils.format(params, "utf-8") );
              return null;
       }
       public static HttpResponse executeQuery(HttpUriRequest request, CookieStore cookie_store) throws ClientProtocolException, IOException {
              HttpClient mClient = new DefaultHttpClient();
              if( cookie_store == null )
                     return mClient.execute(request);
              HttpContext mHttpContext = new BasicHttpContext();
              mHttpContext.setAttribute(ClientContext.COOKIE_STORE, cookie_store);
              return mClient.execute(request, mHttpContext);
       }
       public static void exampleGetUsage() {
              String url = "http://localhost/login.php";
              List<NameValuePair> params = new ArrayList<NameValuePair>();
              params.add( new BasicNameValuePair("user","username") );
              params.add( new BasicNameValuePair("passwd","password") );
              try {
                     CookieStore cookie_store = new BasicCookieStore();
                     HttpEntity result = HttpQueryUsage.executeQuery(HttpQueryUsage.createHttpGet(url, params), cookie_store).getEntity();
                     for( Cookie item : cookie_store.getCookies() )
                            System.out.println("CookieName: "+item.getName()+",CookieValue: "+item.getValue() );
                     if( result != null )
                            System.out.println("PageResult:"+EntityUtils.toString(result));
              } catch (Exception e) {
                     e.printStackTrace();
              }
       }
       public static void examplePostUsage() {
              String url = "http://localhost/login.php";
              List<NameValuePair> params = new ArrayList<NameValuePair>();
              params.add( new BasicNameValuePair("user","username") );
              params.add( new BasicNameValuePair("passwd","password") );
              try {
                     CookieStore cookie_store = new BasicCookieStore();
                     HttpEntity result = HttpQueryUsage.executeQuery(HttpQueryUsage.createHttpPost(url, params, null, null), cookie_store).getEntity();
                     for( Cookie item : cookie_store.getCookies() )
                            System.out.println("CookieName: "+item.getName()+",CookieValue: "+item.getValue() );
                     if( result != null )
                            System.out.println("PageResult:"+EntityUtils.toString(result));
              } catch (Exception e) {
                     e.printStackTrace();
              }
       }
       public static void examplePostFileUploadingUsage() {
              String url = "http://localhost/login.php";
              List<NameValuePair> files = new ArrayList<NameValuePair>();
              files.add( new BasicNameValuePair("file1","/mnt/sdcard/test.png") );
              try {
                     CookieStore cookie_store = new BasicCookieStore();
                     HttpEntity result = HttpQueryUsage.executeQuery(HttpQueryUsage.createHttpPost(url, null, files, null), cookie_store).getEntity();
                     for( Cookie item : cookie_store.getCookies() )
                            System.out.println("CookieName: "+item.getName()+",CookieValue: "+item.getValue() );
                     if( result != null )
                            System.out.println("PageResult:"+EntityUtils.toString(result));
              } catch (Exception e) {
                     e.printStackTrace();
              }
       }
}


2012年9月20日 星期四

Android 開發筆記 - 簡易 Java AES 加解密與 SHA1 筆記

寫 Mobile app 有時需要存一些敏感的資訊,如果只是當做一個認證用途,大概就用 MD5 或 SHA1 來使用,但如果需要保留的,大概就需要能夠加密後又解密的,這時候就可以考慮拿一把 key 進行 AES Encryption/Decryption 動作。


簡易 SHA1:


import java.security.MessageDigest;


public static String sha1(String input) {
       try {
              MessageDigest digest = MessageDigest.getInstance("SHA-1");
              digest.reset();
              byte[] out = digest.digest(input.getBytes("UTF-8"));
              return android.util.Base64.encodeToString(out, android.util.Base64.NO_WRAP);
       } catch (Exception e) {
              e.printStackTrace();
       }
       return null;
}


簡易 AES Encryption/Decryption (使用自製的 key):


import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import android.util.Base64;


public static String selfKey(String key) {   // key.length() must be 16, 24 or 32
       int length = key.length();
       if( length < 16 ) {
              for( int i=length ;i<16; ++i )
                     key += i%10;
              return key;
       } else if ( length < 24 ) {
              for( int i=length ;i<24; ++i )
                     key += i%10;
              return key;
       } else if ( length < 32 ) {
              for( int i=length ;i<32; ++i )
                     key += i%10;
              return key;
       }
       return key.substring(0, 32);
}


public static String selfEncode(String key, String value) {
       SecretKeySpec spec = new SecretKeySpec(selfKey(key).getBytes(), "AES");
       Cipher cipher;
       try {
              cipher = Cipher.getInstance("AES");
              cipher.init(Cipher.ENCRYPT_MODE, spec);
              return Base64.encodeToString(cipher.doFinal(value.getBytes()), android.util.Base64.NO_WRAP);
       } catch (Exception e) {
              e.printStackTrace();
       }
       return null;
}


public static String selfDecode(String key, String value) {
       SecretKeySpec spec = new SecretKeySpec(selfKey(key).getBytes(), "AES");
       Cipher cipher;
       try {
              cipher = Cipher.getInstance("AES");
              cipher.init(Cipher.DECRYPT_MODE, spec);
              return new String( cipher.doFinal(Base64.decode(value, android.util.Base64.NO_WRAP)) );
       } catch (Exception e) {
              e.printStackTrace();
       }
       return null;
}