2011年6月16日

AndroidでURLを開く度に自作のアプリを起動する (5) 時間のかかる処理を別スレッドで実行する



HTTP通信を別スレッド化する


Androidアプリケーションでスレッドを使う方法はいろいろあるみたいだが、今回は以下の手順で実装した。
  1. ActivityクラスでRunnableインターフェースをインプリメントする。
  2. android.os.HandlerクラスのインスタンスをActivityクラスで保持する。
  3. Runnableのrunメソッドをオーバーライドする。
  4. 別スレッドでの処理を開始する。


1. ActivityクラスでRunnableインターフェースをインプリメントする。

Activityでインプリメントせずに、単にその都度Runnableのインスタンスを生成しても多分大丈夫。ただこちらの方が多少はメモリの節約になるらしい。(という記述をどこかで読んだ気がする。)
public class IntentReceiveActivity extends Activity implements Runnable {
  ...
}

2. android.os.HandlerクラスのインスタンスをActivityクラスで保持する。

別スレッドでの処理が完了した後にUIスレッド側で実行する処理を受け取る為に必要になる。
private Handler mHandler = new Handler();

3. RunnableのrunメソッドをOverrideする。

別スレッドでの処理が完了した後、mHandlerのpost()メソッドを呼んでUIスレッド側で実行したい処理を渡す。
変数urlとtitleはfinalで宣言しないとハンドラ内から参照する事が出来ないので注意。でも逆に言うとfinalで宣言したローカル変数がそのまま参照出来るので、Bundleに詰め込んでパラメータとして渡すなどという方法と比べるとこの書き方はかなり便利だと思う。
@Override
 public void run() {
  //HTTP通信を実行してページのタイトルを取得
  final String url = getIntent().getDataString();
  final String title = HttpUtil.getTitle(url, getString(R.string.msg_no_title));

  //処理完了後、ハンドラにUIスレッド側で実行する処理を渡す。
  mHandler.post(new Runnable(){
   @Override
   public void run(){
    try {
     //ListViewを更新(該当のurlが既にListViewから削除されていた場合はfalseを返す)
     if (updateList(url, title)){
      //データベースを更新。
      HistoryDb.save(url, title);
     }
    } catch (Exception e) {
        showErrorDialog(e);
    }
   }
  });
 }

4. 別スレッドでの処理を開始する。
new Thread(this).start();
実行時のイメージはこんな感じになった。処理中はくるくる回るアイコンを表示しておくと結構いい感じ。

HTTP通信処理中の表示(一番上の行)


複数のスレッドが同時に走っているのが分かる

処理中とそうでない時の表示の切り替えは ListItemAdapter クラスの中でtitleがセットされているかどうかで判断して行っている。その部分のソースは こちら。



ネットワーク接続が可能かどうか事前に調べる


ネットワークに接続されていない場合はタイトルの取得を行わずにURLだけを一覧に追加する様にした。接続されているかどうかの判断は下の関数で可能だ。

private boolean isConnected(){
  ConnectivityManager cm = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE);
  NetworkInfo info = cm.getActiveNetworkInfo();
  if (info == null){
   return false;
  } else {
   if (info.isConnected()){
    return true;
   } else {
    return false;
   }
  }
 }
AndroidSDK開発のレシピ―104個のレシピで学ぶAndroidアプリ開発の極意 塚田 翔也 (著)より



ブラウザを自動的に開くかどうかを設定で選べる様にする


設定画面にチェックボタンを追加して、インテントで受け取ったURLを自動的にブラウザで開くかどうか設定出来る様にした。ONの場合とOFFの場合で試してみた所、自動的にブラウザを開かずに単に一覧リストを表示するだけの方が使いやすい様な気がする。




リダイレクト時に複数のエントリが追加されてしまう問題


自動的にブラウザを開く場合にWebサーバーからリダイレクトのレスポンスが返って来ると、標準ブラウザはリダイレクト先のURLを開く為にまたインテントを発行する様になっている。そのインテントを再度このアプリが受け取ると、結局一覧リスト上はURLが微妙に違うだけで内容は同じページ、という行が複数追加されてしまう事になる。

例えば、
http://d.hatena.ne.jp/hogehoge/
というサイトを開いて、
http://d.hatena.ne.jp/hogehoge/touch
にリダイレクトされた場合にこの現象が起きる。

これを防ごうとすると、

「インテントを受け取った時にそれがリダイレクトされて来たものなのかどうか判定してそうであれば無視する。」

という動作が出来ればいいのだが、その判定方法が見つからないのでこの方法は今の所実現出来ていない。

次善の策としては、

「時間制限を設けて、前のインテント処理から一定の時間内(20秒とか)は同じタイトルの行を追加しない様にする。」

というやり方が考えられる。

ただ、ややこしくなりそうなのでそこまで実装するのは今回は見送り。(笑)



全ソースと次の目標


今回の全ソースはこちらからダウンロード可能になっている。
mikehibm/android-browser-intent03 at intent04 - GitHub

他にも「設定画面で標準以外のブラウザを選べる様にする」とか、「ListViewにチェックボックスを付けて処理対象を選択出来るようにする」とか、検討した機能はあった。

「メールを送る代わりにどこかのサーバーにデータを送信する」というのも面白そうだ。Google DocumentsのSpreadsheetに書き込めば意外と使い途があるかも知れない。

ただこのアプリを変更するのは今回で一旦ストップしておこうと思う。

実を言うと、「気になったリンクの一覧を簡単にPCに送る」というもともとの目的は Google Readerの「スターを付ける」機能を使えばほぼ達成出来る事に気付いてしまったからだ。
Googleリーダーに「後で読む」っぽい機能をつける方法 : ライフハッカー[日本版]
情報の収集、閲覧、集約は全てGoogleリーダーに任せることにした。 - iPhoneとiMacと自分と…

「スターを付ける」を使い出してからは、この自作アプリを使うのはTwitterなどGoogle Readerの管理外のURLをPCに送りたい時だけになってしまった。まあそれでも完全に用が無くなった訳ではないのでよしとしよう。


それに、他にもっと面白そうな事がたくさんある。あり過ぎて困る。

次からはSurfaceViewを使ったグラフィック処理や、SoundPoolを使って音を出す事などをやってみようかなと思っている。

あと、OpenGLにも興味を惹かれる。何か子供向けの教育アプリでも作れないものかなとこんな本を読んでみたりもしている。
OpenGLで作る Android SDKゲームプログラミング 中島 安彦 (著), 横江 宗太 (著), 株式会社パンカク (著)
簡単なものから始めて少しずつ機能を追加して行く構成になっていて、とても分りやすい。


AndroidでURLを開く度に自作のアプリを起動する