2011年2月27日日曜日

AndroidのLooper覚書


デ部に参加したときにLooperの話を聞いた。
なんとなくは解ったけど実際のところどうなのか微妙な理解なので
ソースをみてみた。
frameworks/base/core/java/android/os/Handler.java
の109行目あたりにコンストラクタがある(この辺のソースは、そうそう変わらないだろうから行数かいてみた)が、
prepareしていないとエラーが起こるような仕組みになっている。
なので、
new Thread(new Runnable() {
   @Override
   public void run() {
    Handler handler = new Handler();
    
   }
   
  }).start();
こんなことすると落ちる。
UI threadの場合はActivityが作成される際に、Looper.java内のprepareMainLooper()がコールされて、準備OKになるけど、UIでないthreadは自分でprepareしてあげないといけない。
そのことはLooperのjavadocコメントに書いてあったりする。
class LooperThread extends Thread {
 public Handler mHandler;
 public void run() {
  Looper.prepare();
  
  mHandler = new Handler() {
   public void handleMessage(Message msg) {
    // process incoming messages here
   }
  };
  
  Looper.loop();
 }
}
こんな感じで書いてみればーって感じでコメントがある。

でも、実際どーやって使うの?
って感じなので動くサンプルかいてみた。
package com.matsuhiro.android.loopersample01;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class LooperSample01 extends Activity {
 private static final String TAG = "LooperSample01";
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  
  Button btn_ui = (Button) findViewById(R.id.button1);
  Button btn_not_ui1 = (Button) findViewById(R.id.button2);
  Button btn_not_ui2 = (Button) findViewById(R.id.button3);

  Thread th = Thread.currentThread();
  long id = th.getId();
  Log.v(TAG, "onCreate ui id="+id);
  
  final LooperThread lt = new LooperThread();
  lt.start();
  
  btn_ui.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    Thread th = Thread.currentThread();
    long id = th.getId();
    Log.v(TAG, "ui id="+id);
   }
  });
  btn_not_ui1.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    lt.mHandler.post(new Runnable() {
     @Override
     public void run() {
      Thread th = Thread.currentThread();
      long id = th.getId();
      Log.v(TAG, "not ui id="+id);
     }
     
    });
    
   }
  });
  btn_not_ui2.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    lt.mHandler.post(null);
    
   }
  });
 }
 
 class LooperThread extends Thread {
  public Handler mHandler;
  public void run() {
   Log.v(TAG, "Looper.prepare()");
   Looper.prepare();
   mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
     // process incoming messages here
     Thread th = Thread.currentThread();
     long id = th.getId();
     Log.v(TAG, "LooperThread id="+id);
    }
   };
   Looper.loop();
  }
 }
}
LooperThreadクラスの中でHandlerのhandleMessageをoverrideしているけど
こいつはMessageの中にcallback(Runnableのこと)が無かったら実行される。(インスタンス生成時にデフォルトのcallbackを設定しなければの話ではあるが。)
なので、
起動してbtn_ui、btn_not_ui1、btn_not_ui2の順番にクリックすると
onCreate ui id=1 ← onCreateが呼ばれた
Looper.prepare() ← 勝手Looperの準備しました
ui id=1 ← btn_uiクリックしました
not ui id=8 ← btn_not_ui1クリックしました
LooperThread id=8 ← btn_not_ui2クリックしました
のようなLog.vが出力される。

正直なところ、こんなの知っていたからって何時使うんだろって感じではある。
ただ、AsyncTaskとかを利用するような処理をいっぱいbackgroundで走らせると
基本設計時に考えたThread構成が良くわかんない感じになりそうではある。
そんなときには、自分でworker threadを作ってそのworkerに決まった処理をpostする。みないな仕組みを自分で作るにはいいかもね。

RSSのパースとか


AndroidにおけるRSSのパースに関しては、実はIBMのサイトで詳しく解説されている。
リンクで上げたページでは、パースの方法をいろいろ説明するためにFactoryを作っているけど
自分はandroid.sax.*を利用して実装した。
パースする前にHTTP GETするわけだけど、そのときに
httpget.setHeader("User-Agent", "Mozilla/5.0");
をセットし忘れないようにしないと、レスポンスがUTF-8で来てくれない。もっとも今はこれで平気だけど、何時ダメになるかわからないんですがね。。。

以前のエントリのようにAsyncTaskを利用して、HTTP GETして、
レスポンスを
 protected List<message> doInBackground(String... params) {
  String query;
  query = "https://www.google.com/history/find?output=rss"+"&q="+params[0];
  HttpGet httpget = new HttpGet(query);
  httpget.setHeader("User-Agent", "Mozilla/5.0");
  HttpResponse response;
  List<message> messages = null;
  
  try {
   response = _HttpClient.execute(httpget);
   
   int status_code = response.getStatusLine().getStatusCode();
   if (status_code < 400){
    InputStream istream = response.getEntity().getContent();
    FeedParser parser = new AndroidSaxFeedParser(istream);
    messages = parser.parse();
    istream.close();
   }
  } catch (ClientProtocolException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } finally {
   // TODO finalize
  }
  return messages;
 }
のようにして取得する。(エラー処理とかははずしてしまったが。。。) messagesには各RSSのアイテムが入っているので、 それぞれ値を取得してリストに表示させてやればいい。

今回は欲しい情報を得るのに、認証が必要でその処理がCookieを利用していた。 あんまりいい方法じゃないかもしれないが、一度認証済みのHttpClientを他のActivityでも使いまわしたいな。。。と思ったので、"android データ Activity"でググッたら出てくる出てくる。 簡単に言うとApplicationのインスタンスにデータを保持させて全体からそれを覗く。ということだ。 準備は
public class SingleConnectApp extends Application {
 private DefaultHttpClient _HttpClient = null;
 
 public void setHttpClient(DefaultHttpClient client) {
  _HttpClient = client;
 }
 
 public DefaultHttpClient getHttpClient() {
  return _HttpClient;
 }
}
こんなクラスを作ってmanifestのapplicationタグを <application android:icon="@drawable/gfoot_icon" android:label="@string/app_name" android:name="SingleConnectApp"> のようにして、onCreate内とかで
SingleConnectApp app = (SingleConnectApp)this.getApplication();
  DefaultHttpClient client = app.getHttpClient();
のようにすればActivity間でインスタンスを共有できる。
よくよく考えるとDefaultHttpClientを拡張してsingletonのクラス作れば同じこと出来んじゃね?てかソッチのが良いな。とか思ったけど、Applicationクラスにデータを持たしてみたかったのでそのまま進めた。

2011年2月19日土曜日

leanbackとYouTube Remote


Androidで面白いのを見つけたので紹介。
youtubeのleanbackをリモートコントロールするソフトだ。
そもそもleanback自体があまり知られていない。
自分はIT企業に勤めているけど、そこでも知っている人のほうが少ないようだ。
youtubeのURLの後ろにleanbackと打つだけで、TVっぽいUIのページ(http://www.youtube.com/leanback)に飛ぶ(たぶんFlash)。
基本的に十字キーだけでコントロールできるように設計されている。

youtubeにログインして、leanbackのページをPCで表示させてから
これを起動すると、
Androidからleanbackの表示コンテンツがコントロールできる。
スゲー。
レスも速いし。
firefoxでleanbackのページを全画面表示にするとほんとにテレビっぽい。

youtubeに上げてみた。

2011年2月9日水曜日

AndroidでGoogle Web履歴をHTTP GET


Gfootというアプリをマーケットにリリースした。
アプリをリリースしたけど、何をやったのか?調べたのか?
書き残しておかないと忘れてしまいそうなので、
そろそろまとめてみる。
・ブラウザを利用しないでCookieを解決する
・RSSのパース
がメインかな?

GoogleのWeb履歴を見る際のCookieの扱いについては、ここを参照しました。
このサイトによると、履歴を見るためにログインするわけだが、そのためのCookieをログイン画面の
HTMLをGETで取得することで、解決していることがわかる。
また、ログインする際にformの情報をPOSTするわけだが、その情報も同時に取得している。
ただ、これだけだとAndroid上でどうやってログイン処理すればよいのかが
具体的にはわからないので、ここを参考にしてみた。
上記二つのサイトを参考に組み合わせてAndroidからGoogle Web履歴にアクセスしている。
以下は、説明に要らない部分など削っているので、変なところもあるかと思うが、
基本的には下記で得られた_HttpClient(DefaultHttpClientクラス)を利用すれば、履歴の取得が可能だ。
ちなみにHTMLをパースしてinput部分を取得するために
jericho-html-3.1.jarを利用しています。(LGPLなので使うだけならソースの公開義務はないはず。)
それと当然ですが、ネットワーク系の処理をするときにUIスレッドで処理すると
操作性が悪くなるので、AsyncTaskを利用してます。

@Override
  protected DefaultHttpClient doInBackground(String... params) {
   String email = params[0];
   String passwd = params[1];
   
   _HttpClient.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
   _HttpClient.getParams().setParameter("http.connection.timeout", 5000);
   _HttpClient.getParams().setParameter("http.socket.timeout", 3000);
   
   HttpGet httpget = new HttpGet(https://www.google.com/accounts/ServiceLogin?hl=ja&nui=1&service=hist);
   HttpResponse response;
   String result = null;
   try {
    response = _HttpClient.execute(httpget);
    int status_code = response.getStatusLine().getStatusCode();
    Log.v("Gfoot","status_code="+status_code);
    if (status_code < 400){
     InputStream istream = response.getEntity().getContent();
     InputStreamReader reader = new InputStreamReader(istream);
     BufferedReader objBuf = new BufferedReader(reader);
     StringBuilder builder = new StringBuilder();
     String sLine;
     while((sLine = objBuf.readLine()) != null){
      builder.append(sLine);
     }
     result = builder.toString();
     istream.close();
    } else {
     _HttpClient = null;
    }
   } catch (ClientProtocolException e) {
    _HttpClient = null;
    e.printStackTrace();
   } catch (IOException e) {
    _HttpClient = null;
    e.printStackTrace();
   }
   if (result == null) {
    return null;
   }
   
   
   String name;
   String value;
   Source source = new Source(result);
   List linkElements = source.getAllElements(HTMLElementName.INPUT);
   int count = linkElements.size();
   List nameValuePair = new ArrayList(count);
   for (Element linkElement : linkElements) { 
    name = linkElement.getAttributeValue("name");
    value = linkElement.getAttributeValue("value");
    if ("Email".equals(name)) {
     value = email;
    }
    if ("Passwd".equals(name)) {
     value = passwd;
    }
    nameValuePair.add(new BasicNameValuePair(name, value));
   }
   // この段階でPOSTするために必要な情報は、nameValuePairに含まれている。
   
   HttpPost httppost = new HttpPost(https://www.google.com/accounts/ServiceLoginAuth?service=hist);
   try {
    httppost.setEntity(new UrlEncodedFormEntity(nameValuePair));
    response = _HttpClient.execute(httppost);
   } catch (UnsupportedEncodingException e) {
    _HttpClient = null;
    e.printStackTrace();
   } catch (ClientProtocolException e) {
    _HttpClient = null;
    e.printStackTrace();
   } catch (IOException e) {
    _HttpClient = null;
    e.printStackTrace();
   }
   
   return _HttpClient;
  }

ここでログインの際に
https://www.google.com/accounts/ServiceLogin?service=hist
というのを利用して、ログインのためのCookieを取得しているが、
これはどうやって知るのか?履歴以外はどうなのか?
気になるところだと思う。
このサイトがかなりいろいろな情報をもたらしてくれる。
service=histは履歴をさしているわけだ。

次回は、履歴やBookmarkをRSS形式で取得する部分と
パースについて。
また、おまけでアプリケーション全体において永続的なデータを作る方法。

2011年2月6日日曜日

Androidマーケットに初登録


アンドロイドマーケットに初めて登録しました。
マーケットへのリンク
いろいろ調べたことは、また後日に書こうかと思います。