Home > android > AndroidでSQLiteを利用した実用的な入力補完の実装

AndroidでSQLiteを利用した実用的な入力補完の実装

AndroidでSQLiteを利用する事で、実用的な入力補完を考えてみました。
実際の動作もご覧頂けます。

作るもの

タグをSQLiteに保存し、保存したタグを入力補完の候補として再構成します。

入力補完とは

AndroidではAutoCompleteTextViewを利用して、入力補完を簡単に実装できます。
文字を(デフォルトで)2文字入力すると、その補完候補を表示します。

例えば、「android」と入力すると、SQLiteに「android」という文字列を保存します。
次に、「an」と入力すると「android」という文字列が候補としてプルダウンします。

UIデザイン

UIのデザインは下のようにしました。

このUIを定義するXMLはこうなっています。
main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">
  <TextView android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="[Tags DB]" />
  <TextView android:id="@+id/saved_text"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
  <AutoCompleteTextView android:id="@+id/auto_complete"
                        android:layout_width="fill_parent"
                        android:layout_height="wrap_content" />
  <LinearLayout android:orientation="horizontal"
                android:gravity="center_horizontal"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content">
    <Button android:id="@+id/cancel_btn"
            android:layout_width="105px"
            android:layout_height="wrap_content"
            android:text="CANCEL" />
    <Button android:id="@+id/delete_btn"
            android:layout_width="105px"
            android:layout_height="wrap_content"
            android:text="DELETE ALL" />
    <Button android:id="@+id/save_btn"
            android:layout_width="105px"
            android:layout_height="wrap_content"
            android:text="SAVE" />
  </LinearLayout>
</LinearLayout>

リソースを取得しておきます。

this.textView = (AutoCompleteTextView) findViewById(R.id.auto_complete);
this.savedText = (TextView) findViewById(R.id.saved_text);
Button cancelButton = (Button) findViewById(R.id.cancel_btn);
Button deleteButton = (Button) findViewById(R.id.delete_btn);
Button saveButton = (Button) findViewById(R.id.save_btn);
入力補完部分

入力補完は以下のように実装できます。

// 入力補完候補を再構成する
private void updateTags(String[] tags) {
  ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
            android.R.layout.simple_list_item_1, tags);
  textView.setAdapter(adapter);
}

AutoCompleteTextViewにadapterとして登録するだけです。
tagsというString配列をSQLiteに保存した文字列から生成すれば、adapterを生成できそうです。

各ボタンの実装
CANCEL

CANCELボタンは入力内容を削除するだけです。実装はとっても簡単です。

cancelButton.setOnClickListener(new OnClickListener() {
  public void onClick(View arg0) {
    textView.setText("");// テキストを削除
  }
});
DELETE ALL

DELETE ALLボタンはSQLiteのテーブル内の要素を全て削除します。

private TagSQL sql;
deleteButton.setOnClickListener(new OnClickListener() {
  public void onClick(View arg0) {
    sql.deleteAllRow();// データ削除
    textView.setText("");// テキストを削除
    updateTags();// 表示を更新
  }
});

TagSQLというクラスは、独自に実装したSQLiteを実行するためのクラスです。

SAVE

SAVEボタンは入力内容をSQLiteに保存します。

saveButton.setOnClickListener(new OnClickListener() {
  public void onClick(View arg0) {
    // テキストを取得
    String text = textView.getText().toString();
    // -----中略-----
    sql.createRow(text);
    // -----中略-----
  }
});

createRowは引数の文字列をSQLiteに保存します。

savedText

savedTextはSQLiteに保存した内容を分かりやすく表示するエリアです。

SQLite

今回はTagSQLというSQLiteにアクセスするクラスを作っています。

テーブルの設計

データ設計は「(ROWID, TAG, COUNT)」とします。
ROWIDはテーブル内で一意になる数字(int)です。
TAGは保存する文字列(text)です。
COUNTは保存を試みた回数(int)です。

つまり、「android」という文字列を3回SAVEしたら、countは3になります。

deleteAllRow

DELETE ALLボタンから実行する関数です。

// 全てのrowを削除する
public void deleteAllRow() {
  String query = "delete from " + TABLE + " where " + ROWID + ">-1;";
  db.execSQL(query);
}

単純にSQL文を構築して実行しているだけです。
ROWIDは0以上の整数なので、このSQL文で全ての要素を削除することができます。

createRow(String tag)

SAVEボタンから実行する関数です。
引数に渡される文字列をSQLiteに保存します。
ただし、すでにSQLite内に存在する文字列の場合はCOUNTを1増やします。

まず、tagがSQLite内に存在するかを確認する。

boolean has_row = hasRow(tag);

存在すれば、COUNTを1増やし、存在しなければ新たに行を追加した文字列を保存します。

if (has_row) {
  inclementCount(tag);
} else {
  createRow(tag, 1);
}

全体の実装方式はこういった感じになっています。
全ソースコードも公開しておきます。参考にして下さい。

ちゃんと入力して保存したテキストが入力補完候補となっています。

シェルでも確認してみる

Androidのシェル上でもSQLiteの状態を確認してみる。
SQLiteのデータベースの保存先は、以下の通りです。

/data/data/[アプリのパッケージ]/databases/[DB名]

実際に確認してみました。

ちゃんと保存出来ていますね。

Download

いつものようにEclipseのプロジェクトを公開します。興味のある方はご自由にどうぞ。

auto_complete_text.zip

ソースコードも一応公開しておきます。上のEclipseプロジェクト内のソースと同じですが…。

AutoCompleteTest.java

package com.adamrocker.android.autocomplete;
 
import java.util.ArrayList;
import com.adamrocker.android.autocomplete.TagSQL.TagRow;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.TextView;
 
public class AutoCompleteTest extends Activity {
  private AutoCompleteTextView textView;
  private TextView savedText;
  private TagSQL sql;
 
  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    setContentView(R.layout.main);
    textView = (AutoCompleteTextView) findViewById(R.id.auto_complete);
    savedText = (TextView) findViewById(R.id.saved_text);
 
    /*---------------------------------------------- Cancel Button */
    Button cancelButton = (Button) findViewById(R.id.cancel_btn);
    cancelButton.setOnClickListener(new OnClickListener() {
      public void onClick(View arg0) {
        textView.setText("");// テキストを削除
      }
    });
 
    /*---------------------------------------------- Delete Button */
    Button deleteButton = (Button) findViewById(R.id.delete_btn);
    deleteButton.setOnClickListener(new OnClickListener() {
      public void onClick(View arg0) {
        sql.deleteAllRow();// データ削除
        textView.setText("");// テキストを削除
        updateTags();// 表示を更新
      }
    });
 
    /*---------------------------------------------- Save Button */
    Button saveButton = (Button) findViewById(R.id.save_btn);
    saveButton.setOnClickListener(new OnClickListener() {
      public void onClick(View arg0) {
        // テキストを取得
        String text = textView.getText().toString();
        String[] texts = text.split(",");
 
        // データ保存
        for (int i = 0; i < texts.length; i++) {
          if (texts[i].length() > 0) {
            String tmp = texts[i].replaceAll("^[ ]+", "");
            tmp = tmp.replaceAll("[ ]+$", "");
            sql.createRow(tmp);
          }
        }
        updateTags();// 表示を更新
        textView.setText("");// テキストを削除
      }
    });
 
    /*---------------------------------------------- SQL DB */
    sql = new TagSQL(this);
    updateTags();
  }
 
  // データベースから全行を取得して、その内容を候補リストと、テキスト表示を更新
  private void updateTags() {
    ArrayList<TagRow> row_list = sql.getAllRows();
    String tags[] = getAllString(row_list);
    updateTags(tags);// auto complete候補リストの更新
    updateSavedText(row_list); // テキスト表示を更新
  }
 
  // 入力補完候補を再構成する
  private void updateTags(String[] tags) {
  ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
    android.R.layout.simple_list_item_1, tags);
    textView.setAdapter(adapter);
  }
 
  // savedTextに表示する
  private void updateSavedText(ArrayList<TagRow> list) {
    String str = "";
    for (int i = 0; i < list.size(); i++) {
      TagRow tr = list.get(i);
      str += tr.tag + "(" + tr.count + ")" + ", ";
    }
    savedText.setText(str);
  }
 
  // TagRowのArrayListから文字列だけのString配列を構成する
  private String[] getAllString(ArrayList<TagRow> list) {
    int size = list.size();
    String[] str = new String[size];
    for (int i = 0; i < size; i++) {
      str[i] = list.get(i).tag;
    }
    return str;
  }
 
  @Override
  protected void onPause() {
    super.onPause();
    if(sql != null)
    sql.close();
    finish();
  }
}

TagSQL.java

package com.adamrocker.android.autocomplete;
 
import java.io.FileNotFoundException;
import java.util.ArrayList;
 
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
 
public class TagSQL {
 
  // [TABLE LAYOUT]
  /* ----------------------- */
  /* | rowid | tag | count | */
  /* ----------------------- */
  class TagRow {
    public int rowid;
    public String tag;
    public int count;
  }
 
  private static final String ROWID = "rowid";
  private static final String TAG = "tag";
  private static final String COUNT = "count";
  private static final String DB_NAME = "AutoCompleteTest";
  private static final String TABLE = "Tags";
  private static final String[] ROW = new String[] { ROWID, TAG, COUNT };
  private static final int DB_VERSION = 1;
  private static final String CREATE_TABLE_STMT = "create table " + TABLE
          + " (" + ROWID + " integer primary key autoincrement, " + TAG
          + " text not null, " + COUNT + " integer);";
 
  private SQLiteDatabase db;
 
  // コンストラクタ
  // DBが無ければ作り、あればオープンする
  public TagSQL(Context context) {
    try {
      db = context.openDatabase(DB_NAME, null);
    } catch (FileNotFoundException e) {
      try {
        db = context.createDatabase(DB_NAME, DB_VERSION, 0, null);
        db.execSQL(CREATE_TABLE_STMT);
      } catch (FileNotFoundException e1) {
        db = null;
      }
    }
  }
 
  // Tagの数を取得
  private int getCount(String tag) {
    int returnCount = 1;
    Cursor c = db.query(true, TABLE, ROW, TAG + "='" + tag + "'", null, null, null, null);
    if (c.count() > 0) {
      c.first();
      returnCount = c.getInt(2);
    }
    return returnCount;
  }
 
  // tagがdbに存在するかをチェック
  private boolean hasRow(String tag) {
    boolean flag = false;
    Cursor c = db.query(true, TABLE, ROW, TAG + "='" + tag + "'", null, null, null, null);
    if (c == null)
      return flag;
    if (c.count() > 0) {
      flag = true;
    }
    return flag;
  }
 
  // 番号rowIdのラベルをDBから削除する
  public void deleteRow(int rowId) {
    db.delete(TABLE, "rowid=" + rowId, null);
  }
 
  // 全てのrowを削除する
  public void deleteAllRow() {
    String query = "delete from " + TABLE + " where " + ROWID + ">-1;";
    db.execSQL(query);
  }
 
  // tagがTABLEに存在すればCOUNTをインクリメント
  // tagがTABLEに存在しなければtagをINSERT
  public void createRow(String tag) {
    boolean has_row = hasRow(tag);
    if (has_row) {
      inclementCount(tag);
    } else {
      createRow(tag, 1);
    }
  }
 
  // 新しい行を追加する
  // TABLE内に同じtagが無い事を前提とする
  public void createRow(String tag, int count) {
    ContentValues cval = new ContentValues();
    cval.put(TAG, tag);
    cval.put(COUNT, count);
    db.insert(TABLE, null, cval);
  }
 
  // COUNTをアップデートする
  public void updateRow(String tag, int count) {
    ContentValues args = new ContentValues();
    args.put(COUNT, count);
    db.update(TABLE, args, TAG + "='" + tag + "'", null);
  }
 
  // COUNTをインクリメントする
  // tagが存在することを前提とする
  public void inclementCount(String tag) {
    int count = getCount(tag) + 1;
    updateRow(tag, count);
  }
 
  // 全てのTAGを取得する
  public ArrayList<TagRow> getAllRows() {
    ArrayList<TagRow> list = new ArrayList<TagRow>();
    Cursor c = db.query(TABLE, ROW, null, null, null, null, null);
    int numRows = c.count();
    c.first();
    for (int i = 0; i < numRows; i++) {
      TagRow tr = new TagRow();
      tr.rowid = c.getInt(0);
      tr.tag = c.getString(1);
      tr.count = c.getInt(2);
      list.add(tr);
      c.next();
    }
    return list;
  }
 
  // DBを閉じる
  public void close() {
    if (db != null)
      db.close();
  }
}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">
  <TextView android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="[Tags DB]" />
  <TextView android:id="@+id/saved_text"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
  <AutoCompleteTextView android:id="@+id/auto_complete"
                        android:layout_width="fill_parent"
                        android:layout_height="wrap_content" />
  <LinearLayout android:orientation="horizontal"
                android:gravity="center_horizontal"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content">
    <Button android:id="@+id/cancel_btn"
            android:layout_width="105px"
            android:layout_height="wrap_content"
            android:text="CANCEL" />
    <Button android:id="@+id/delete_btn"
            android:layout_width="105px"
            android:layout_height="wrap_content"
            android:text="DELETE ALL" />
    <Button android:id="@+id/save_btn"
            android:layout_width="105px"
            android:layout_height="wrap_content"
            android:text="SAVE" />
  </LinearLayout>
</LinearLayout>
関連のありそうなエントリ

Comments:21

安藤恐竜 08-03-14 (金) 8:49

他のアプレットのText入力を乗っ取るにはどーしたらいーんでしょ?あんまりちゃんと調べてないけど。

AndroidでIMEするならPOBoxかなぁなどと。

Blood Sisters
http://nirvash.sakura.ne.jp/blog/

adamrocker 08-03-14 (金) 18:40

Content Providerの仕組みを使うとアプリケーション間のデータ共有はできそうですが…。そういうことでは無いですか?(汗)

POBoxってソースの公開が中断されてませんか?探しきれてないだけかなぁ??やっぱり日本語欲しいですよねぇ…。

おーしゃん 08-03-14 (金) 19:17

はじめまして。
いつも貴重なソースコード公開していただいて大変参考になってます。
なんかすごくタイムリーな記事とコメントがあがっていたのでちょっとした小物を公開しちゃいます。
http://bizpal.jp/ocean.android/00003
AutoCompleteTextViewにPOBox、みんな考えることは同じですねぇ。

安藤恐竜 08-03-15 (土) 12:26

こんな時にはとっても頼りになるOpenEmbeddedのビルド環境。
$ bitbake poboxserver
ってやってみたら、ここから落としてくれました。
http://jaist.dl.sourceforge.net/sourceforge/gakusei/pobox-1.2.5.tar.bz2

って…Ruby 1.8のビルド始めやがった。(笑)

IMEはGoogleさんが作ってくれるんだろーなーと期待してますけど、どーなんでしょーねー。

adamrocker 08-03-16 (日) 23:13

>おーしゃんさん
はじめまして。と言っても、Hotpepperのアプリケーションで個人的に既に見知っていました。
コメントありがとうございます。
さっそくPOBoxでIMEモドキを実装されたんですね。スゴいです。文字入力を監視しながらPOBoxを検索して変換ってかんじでしょうか?

>安藤恐竜さん
bitbakeってのがあるんですね!!全然知りませんでした、いつも貴重な情報ありがとうございます。
IMEはGoogle JPの仕事だと思ってますw←丸投げ(汗)

MichaeL 08-03-17 (月) 11:04

Googleを待たずしてIME作りたいなあ
Androidオフも実現したい!
Androidまとめwikiで募集かけてみようかなあ

adamrocker 08-03-18 (火) 0:23

>MichaeLさん
>Googleを待たずしてIME作りたいなあ
IMEは簡易的な物なら個人でも作れるんでしょうか?
日本語でAndroidを使うなら必須機能ですよね。是非欲しいです。

>Androidオフも実現したい!
日本のAndroid開発者(上下位レイヤ問わず)ってどれぐらいいるんでしょうね?ベンチャー企業内部では結構プロジェクトが立ち上がってたりするんでしょうか?私はあまりキャッチアップできてないんですが、沢山いると嬉しいなぁ。

安藤恐竜 08-03-18 (火) 7:06

Google I/O、逝きませんかー?(笑

ひそかに豆ナイトさんに集結しないかと勝手に期待してます>オフ。
http://www.mamezou.net/modules/mamenight1/
春の陣 電気羊は桜吹雪の夢を見るか Google Android 4月下旬

P.S. おーしゃんさん。その節は無茶なリクエストを出してしまい、申し訳ございませんでした。↑のPOBoxのソースを貼ったときには、おーしゃんさんのコメ、見えなかったような気が。スルーしちゃってすみません。

adamrocker 08-03-19 (水) 15:18

WordPressを弄ってらバグってしまって、コメントが変になってしまってましたスミマセン。

>安藤恐竜さん
Google I/O行けるんですね。イイなぁ。私の本職は全然関係ないので行かせてもらえそうにありません…orz
レポート楽しみにしています。

豆ナイトってのがあるんですね。知りませんでした。
夜がめっぽう弱い私ですが、Androidネタなら興味深いので是非参加したいです。

コメントはスパムが多すぎるので、管理者(私)が毎回承認を出しています。
CAPTCHAとか導入しようかと思っているのですが、コメントを頂く量がそれほど多くないので、導入コストと運用コストのバランスを見ています。
単純に面倒臭がりなだけですが(スミマセン

私の気が向けば、なんらかの対策を打ってみますw

おーしゃん 08-03-19 (水) 18:33

>adamrockerさん
adamrockerさんが想像されているような事ができる程のスキルはないので
AutoCompleteTextViewに若干手を加えただけの代物です。

>MichaeLさん
Googleには日本語入力関連の予定とか方針だけでも示してほしいところですね。

>安藤恐竜さん
豆ナイトの開催場所って会社から歩いて行ける所だったりするので、
都合が付くようなら参加したいですねぇ。

MichaeL 08-03-19 (水) 18:45

コメントが見えなくなっててなんか悪いことしちゃったかと思ってましたw

豆ナイトかあ
こんなのがあるんですねえ
wikiのほうにも載せておこうかな

MichaeL 08-03-19 (水) 19:10

Androidまとめwikiのページ作っちゃいました。
http://www29.atwiki.jp/android/pages/54.html

当方大阪なので平日だと豆ナイト行くのは難しいかもしれません。

adamrocker 08-03-19 (水) 20:09

宣言するとやる気が出るタイプのようです私w
CAPTCHAを導入してみました。とりあえず様子を見てみます。

taku 08-03-25 (火) 2:30

いつも参考にさせていただいております。
以前の記事でSMACKを使ったチャットアプリを作られていたと思うのですが、
SMACKのファイル送受信APIって
ANDROID上では動かないですかね???

試してみたけどどうもうまくいかなかったもので。。。

adamrocker 08-03-25 (火) 8:35

>takuさん
Smackでファイル転送を実装した事がないので分かりませんが、Google Talkではファイル転送機能は無いみたいですね。
どこで、どうなって動かないのかが分かると、もうちょっとお役に立てるかもしれません…
すいません、ヘッポコなコメントで(汗)

taku 08-03-26 (水) 1:10

>adamrockerさん

お返事ありがとうございます。

SMACKXの”FileTransferManager”クラスをnewしようとすると、
内部でNullPointerExceptionがでてるみたいです。。。

logcatの内容です

DEBUG/dalvikvm(674): Exception Ljava/lang/NullPointerException; from FileTransferNegotiator.java:126 not caught locally

XMPPmessagingのついでにファイル転送も行いたかったのですが、別の方法を考えたほうがよさそうですかねー。

突然の質問、もうしわけありません(><)

adamrocker 08-03-26 (水) 17:43

>takuさん
ちょっと調べてみました。
不審なところでNPEが出ているのでSMACKXのソースを読んでみました。
#ソースはココ↓からDL
#http://www.igniterealtime.org/downloads/source.jsp

FileTransferNegotiator.java:126周辺はこんな感じ。

120:    public static void setServiceEnabled(final XMPPConnection connection,
121:            final boolean isEnabled) {
122:        ServiceDiscoveryManager manager = ServiceDiscoveryManager
123:                .getInstanceFor(connection);
124:        for (String ns : NAMESPACE) {
125:            if (isEnabled) {
126:                manager.addFeature(ns);
127:            }
128:            else {
129:                manager.removeFeature(ns);
130:            }
131:        }
132:    }

ということで、managerがnullである可能性が高いです。
このmanagerは122,123行目のgetInstanceForで作られてます。
では、ServiceDiscoveryManager#getInstanceForはどうなっているか。

87:    public static ServiceDiscoveryManager getInstanceFor(XMPPConnection connection) {
88:        return instances.get(connection);
89    }

このinstancesはMapで、XMPPConnectionをキーにしてServiceDescoveryManagerを持っています。
そして、このマッピングはXMPPConnectionでコネクションが成立した時に生成する仕組みになっています。
具体的にはXMPPConnect#connect()メソッド実行時に生成します。
ということで、FileTransferManagerをnewする前にXMPPConnectのconnectメソッドを実行していますか?
もし、実行していたらゴメンナサイ。現状これぐらいしか分かりません(汗

どうにも分からない場合は、普通のJavaでsmackを使ってファイル転送が可能かを検証してみるとかすると良いかも…。
androidがダメなのかsmackのファイル転送がダメなのかの切り分けができそうです。

ゲスト 08-03-28 (金) 1:31

>adamrockerさん

ご丁寧なお返事本当にどうもありがとうございます!
念のため確認したのですが、
やはりconnectメソッドは実行済みでした。

FileTransferManagerをnewする前にテキストメッセージを送信しております。

普通のJavaでの確認は今週末にでも行ってみたいと思います。
取り急ぎご報告まで。

また何かわかったらご報告させていただきます!

adamrocker 08-03-28 (金) 7:22

>takuさん
そうでしたか。お役に立てず残念です…。
何か分かったら教えて下さい。smackでファイル転送できたら色々便利そうですね。できるとイイなぁ〜。

taku 08-04-04 (金) 2:19

追加情報

通常のAndroidではない普通のJAVA APPLICATIONでのFiletransferはやはりうまくいくみたいですねー

adamrocker 08-04-04 (金) 7:24

>takuさん
貴重な情報ありがとうございます。
ではAndroidのFiletransferがおかしくなっているということですね…。
Android用smackとオリジナルのsmackとのdiffを見る限りFiletransfer周辺に変更は無さそうなので、ネイティブがおかしいのかもしれませんね。
そうなるとユーザアプリではどうしようもありませんね…残念。

Comment Form
Remember personal info

*
To prove that you're not a bot, enter this code
Anti-Spam Image

Trackbacks:0

Trackback URL for this entry
http://www.adamrocker.com/blog/195/practical_way_of_autocompletetextview_with_sqlite.html/trackback/
Listed below are links to weblogs that reference
AndroidでSQLiteを利用した実用的な入力補完の実装 from throw Life

Home > android > AndroidでSQLiteを利用した実用的な入力補完の実装

Search
Feeds
Meta

Return to page top