2011年5月20日

AndroidでURLを開く度に自作のアプリを起動する (3) SQLiteデータベースを使う



先日のBloggerの障害の影響でこの前書いたエントリが消えてしまった。「ほぼ全てのデータが復旧された」との事だったが、僕が先週の金曜日に朝6時に起きて1時間半もかけて書いた文章はとうとう戻って来なかった。(涙)
Googleの「Blogger」が20時間半に及ぶサービス障害、週末にほぼ復旧 - ニュース:ITpro
やっぱりブログ記事もどこかにバックアップしておいた方が良いのだろうか。と言ってもローカルに保存すればハードディスクのクラッシュや記録メディアの紛失が心配だし、別のクラウドサービスに保存するにしても結局データが消えてしまうリスクは無くならない。考え出すと切りがない。。。

気を取り直して、部分的に残っていた下書きからもう一度書き直す事にした。


さて、


前回までで「ブラウザで表示したURLの履歴をメールで送信する」という目的は一応達成出来た。

ただしこの状態だと、何らかの理由でこのアプリのプロセスが終了させられた場合に履歴の一覧が消えてしまうという問題がある。単純に他のアプリに隠れて画面の裏に回っただけではすぐにプロセスが終了する事はないのだけれども、他のアプリがメモリを大量に使用して空きメモリが不足した場合や、単に長い時間プロセスがアクティブにならなかった場合などにシステムの判断で終了させられる事になる。

今回はプロセスが終了しても履歴の一覧を保持出来る様に、SQLiteのデータベースに保存する様に変更したい。

データベースを開く


AndroidでのSQLiteデータベースの扱い方は大体下のリンク先を見れば把握出来る。
Androidアプリのデータ保存方法の一つ「SQLite」の使い方 SQLiteOpenHelper編 | mucchinのAndroid戦記

Androidアプリでのデータベース基礎 ~速習! Androidアプリケーション開発(4)~(1/3):CodeZine

データを簡単に保存する方法(SQLite編) « Tech Booster
データベースを新規に作成するのではなく、予めアプリのリソースとして組み込んでおいたものをコピーする方法はこちら。
Y.A.M の 雑記帳: Android あらかじめ作成した SQLite database をアプリに取り込む

今回は1テーブルしか使わないので、HistoryDbというクラスを作ってその中でデータベースに関する操作をすべて行う事にしようと思う。もし複数のテーブルがある場合は、データベース全体を表すクラスと各テーブルに対応したクラスに分けた方が多分すっきりするだろうと思う。

とりあえずまずは init() メソッドを作って、最初に必ずこれを呼び出してデータベースが無ければ作成する様にした。
public static void init(String pkgName) throws Exception{
    DbPath = "/data/data/" + pkgName + "/" + DBNAME;
    SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DbPath, null);
    try {
        //テーブルが無ければ作成する。
        createTable(db);
    } catch (Exception e) {
        throw e;
    } finally{
        db.close();
    }
}
 
private static void createTable(SQLiteDatabase db) {
    String sql = "CREATE TABLE IF NOT EXISTS " + TBL_HISTORY
  + " (_id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT, title TEXT);";
    db.execSQL(sql);

    //urlにインデックスを追加。
    sql = "CREATE INDEX IF NOT EXISTS 'main'.'ix_history_url' ON 'history' ('url' ASC)";
    db.execSQL(sql);
}



データベースから一覧を取得する


HistoryDbクラスに selectAll() というメソッドを作ってその中で全件を取得する。取得した結果をループして、1レコード分の情報をHistoryDbクラスのインスタンスのプロパティ(正確には手抜きしたのでパブリックなメンバー変数)にセットし、ArrayListに追加して行く。
public static ArrayList selectAll() throws Exception{
    ArrayList array = new ArrayList();
  
    SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DbPath, null);
    try {
        //全件を取得する。
        String[] columns = {"_id", "url", "title"};
        String where = null;
        String having = null;
        String group_by = null;
        String order_by = "_id DESC";
  
        Cursor cursor = db.query(TBL_HISTORY, columns, where, null, group_by, having, order_by);
        while (cursor.moveToNext()){
            HistoryDb hist = new HistoryDb();
            hist.id = cursor.getInt(cursor.getColumnIndex("_id"));
            hist.url = cursor.getString(cursor.getColumnIndex("url"));
            hist.title = cursor.getString(cursor.getColumnIndex("title"));
            array.add(hist);
        }
    } catch (Exception e) {
        throw e;
    } finally{
        db.close();
    }
    return array;
}


結果のArrayListを受け取ったActivity側では、ArrayListの情報をまたループしてListViewにバインドされた adapter に入れ直している。ちょっと冗長なやり方になってしまった。
//DBから全件取得。
ArrayList array = HistoryDb.selectAll();

//ListViewに表示。
adapter.clear();
for (HistoryDb hist : array) {
    adapter.add(hist.url);
}




データベースに書き込む


HistoryDbクラスに save() メソッドを作って保存処理を書く。既にDBに存在しているかどうかは本来は主キーである「_id」の値で判断するべきだが、今回はURLでも一意になるのでURLを検索キーとして使った。まずUPDATEを実行してその結果が0件であればINSERTを行うというロジックになっている。
public static void save(String url, String title) throws Exception {
    SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DbPath, null);
    try {
        //同じURLが既にあれば更新する。無ければ挿入する。
        insertOrUpdateHistory(db, url, title);
    } catch (Exception e) {
        throw e;
    } finally{
        db.close();
    }
}
 
private static void insertOrUpdateHistory(SQLiteDatabase db, String url, String title) {
    ContentValues values = new ContentValues();
    values.put("url", url);
    values.put("title", title);

    String[] args = { url };

    //同じURLが既に保存されていれば更新する。
    int n = db.update(TBL_HISTORY, values, "url = ?", args );
    if (n == 0){
        //無ければ挿入する。
        db.insert(TBL_HISTORY, null, values);
    }
}




メニューにアイコンを付ける


メニューにアイコンを付けて見た。すると一気に見栄えが良くなった。アイコン一つで、不思議なものだ。アイコンの指定方法は、メニューを定義しているXMLファイル内のitemタグの属性として、
android:icon="@android:drawable/ic_menu_XXX"
を付けるだけだ。


Androidに標準で含まれているアイコンの一覧は、こちらで調べられる。
Taosoftware: Android メニューアイコン



今回の全ソース


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

さて、ここまででデータベースに履歴一覧を保存出来る様になり、少しはアプリケーションらしくなって来た。

せっかくここまで来たら、一覧にURLだけでなくページのタイトルも表示したくなって来た。タイトルはインテントでは飛んで来ないので、アプリ内でHTTP通信を行って取得する必要がありそうだ。それから、ページがリダイレクトされた場合にリダイレクト元とリダイレクト先の両方のURLが履歴として残ってしまうという問題も、出来ればなんとかしたい。

さて、上手く行くかどうか、それは次回のお楽しみという事で。


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







.

2011年5月18日

富士通の「Windows 7」携帯 LOOX F-07C に欲しかった3つの機能

こんな変わった携帯がドコモから発表されたらしい。
「超ちっこい7搭載PC(ケータイ機能もあるよ)」というところがいいですよ:LOOX Uを超えたケータイサイズの新LOOX──“PC”として写真と動画で見る「Windows 7ケータイ F-07C」 (1/2) - ITmedia +D PC USER
これは面白い。ほとんどネットブックに匹敵するスペックで、Windows Phone 7ではなく通常のWindows 7 Home Premiumが動くとの事。

正直言って僕の理想の「超小型PC携帯」に近付いている。もし機会があったら実際に手に取って触ってみたい。

この発表に触発されて、僕が思う「理想の超小型PC携帯」とは何だろうかと考えてみた。とりあえず思いつくのはこんな感じだろうか。

  • 外部モニター、マウス、キーボードが接続可能
    (これは F-07C で可能らしい。)

  • 重さは170グラム程度かそれ以下
    (F-07Cは約218グラム)

多少の重量オーバーには目をつぶるとしても、欲しかったのは次の3つだ。

  • 外部モニター接続時の解像度は1,680 x 1,050以上
    (F-07Cは1280×720ドット)
  • 10Mbps以上の高速で安定したネット接続
  • Wifiテザリングが可能



もしこんな端末があれば、会社と自宅の両方にクレードル+外部モニター+キーボード+マウスのセットを用意しておいて、会社に着いたら携帯をクレードルにセットして仕事、家ではベッドに寝転がってネット閲覧、大きな画面で見たくなったらクレードルにカチャッ、と言う使い方が出来るのに。


クレードル部分は妻や子供の携帯と共用で使える用になっていればなお嬉しい。その上でネットワークの速度が10Mbps以上あれば自宅のケーブルインターネットは要らなくなる。Wifiテザリングが出来ればiPodやiPadからのネット接続も問題無しだ。

でも考えてみると、僕の使い方だと職場のPCにリモート接続する事がメインの用途になりそうだ。そうだとすると、端末自体のOSは何であっても大した違いは無い様な気がして来た。結局の所、必要なのは携帯機能が付いたシンクライアントという事か。

さて、この流れで行くと数年後にはどんなスペックの端末が出て来るのだろうか、楽しみだ。PCはみんなポケットに入れて持ち歩くのが当たり前になるのだろうか。究極的には腕時計サイズになれば嬉しいのだけれど。(笑)







.

2011年5月10日

AndroidでURLを開く度に自作のアプリを起動する (2) メール送信機能と設定画面の追加



前回はURLを開くインテントを受け取ってListViewに追加した後、標準ブラウザで開くという大まかな流れを作成した。

今回は、ListViewに保持しているURL履歴の一覧をメールで送信するという部分と、その為に必要な設定画面を実装したい。

全ソースがダウンロード出来るリンクを最後に付けておいたので、興味のある方はぜひダウンロードしてあれこれ試してもらえればと思う。




メールを送る


「メールを送る」とは言っても、アプリから直接メールサーバーに接続してデータを送るのは大変だ。やろうと思えばその為のライブラリもある事はあるらしい。
Android: マルチスレッドでJava Mail
Downloads - javamail-android - JavaMail port for the android plateform - Google Project Hosting

が、今回は単純にデフォルトのメーラーを起動する為のインテントを発行するだけにしようと思う。

メーラーを起動する為のインテントは、次の方法で発行出来る。
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:" + to_addr));
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, message );
startActivity(intent);

メニューボタンから「メール送信」を選ぶとListViewの内容をメールで一括送信する様にした。


本当は startActivity の代わりに startActivityForResult を使って実際に送信ボタンが押されたのか、キャンセルされたのかも取得したかったのだが、どうも上手く結果が返って来なかったのでこれは断念した。



設定画面を作る


とりあえず3つの項目を設定出来る様にしておいた。


設定画面の作り方はこちらが参考になった。(特に設定された内容をサマリーに表示する部分)
Y.A.M の 雑記帳: Android 設定画面を作成する
Y.A.M の 雑記帳: Android Preference の summary を動的に変更



ListViewで項目がタップされた時の処理


ListViewで項目がタップ(クリック)された時の処理は、AdapterView.OnItemClickListenerクラスのインスタンスをsetOnItemClickListenerメソッドでListViewにセットすれば記述出来る。

例えばこんな感じになる。
//リストの項目がタップされた時の処理
list.setOnItemClickListener(
    new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView parent, View view, int position, long id) {
            ListView listview = (ListView)parent;
            selected_url = (String)listview.getItemAtPosition(position);
            dialog.show();
        }
    }
);

今回は項目がタップされたら処理を選択するダイアログを表示する様にしてみた。


この処理は1回だけ実行すれば良いのでActivityのonCreateで行う様にする。
//リストの項目がタップされた時に開くダイアログを準備。
String[] str_items = { getString(R.string.mnu_browser) , 
                       getString(R.string.mnu_send), 
                       getString(R.string.mnu_delete)};
final AlertDialog.Builder dialog = new AlertDialog.Builder(this)
        .setIcon(R.drawable.icon)
        .setTitle(getString(R.string.mnu_select))
        .setItems(str_items, 
            new DialogInterface.OnClickListener(){
                //ダイアログの項目が選択された時の処理。
                public void onClick(DialogInterface dialog, int which) {
                    switch (which){
                        case 0:
                            openBrowser(selected_url);
                            break;
                        case 1:
                            sendEmail(selected_url);
                            break;
                        case 2:
                            deleteUrl(selected_url);
                            break;
                        default:
                            break;
                     }
                  }
              }
        );

参考にしたサイト: 色々なダイアログの例があるので便利。
八角研究所 : Android で再開する Java プログラミング(14) - ダイアログを制するものがAndroidを制する!



今の所の問題点


ここまでで一応URLの履歴をメールで送信出来る様にはなった。

ただ、ちょっと気になる点がある。それはブラウザでリダイレクトが発生する度に新たに「ブラウザで開く」インテントが発生するという事だ。結果として履歴の一覧にはリダイレクト前と後のURLがそれぞれ残る事になる。

例えば、bit.lyなどの短縮URLサービスを使った場合や、スマートフォンからのアクセスを自動的に専用のURLに誘導する様になっているサイトなどでこの現象が起きる。

実質的に同じページを指しているのに履歴一覧に複数行表示されるのは、ちょっと都合が悪い。リダイレクト後のインテントを受け取った時にそれが「リダイレクトされたものである」という事が認識出来ればリストに追加しない様に出来るのだが、今の所その方法を見つけられていない。



今回の全ソース


mikehibm/android-browser-intent02 - GitHub

次回はいよいよSQLiteを使ってローカルデータベースにURLの履歴を保存する様に変更してみたい。

ちなみに、現在大活躍中の参考書はこちら。まだAndroidの世界で右も左も分からない自分には必携の書になっている。
AndroidSDK開発のレシピ―104個のレシピで学ぶAndroidアプリ開発の極意


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






.