2013/08/29

SQLiteDatabase.openDatabase で StackOverflowError

私が GooglePlay に出しているアプリ SQLiteViewer に以下のようなクラッシュレポートが届きました。

java.lang.StackOverflowError
at java.util.HashMap.<init>(HashMap.java:138)
at java.util.HashMap.<init>(HashMap.java:174)
at java.util.LinkedHashMap.<init>(LinkedHashMap.java:119)
at android.util.LruCache.<init>(LruCache.java:81)
at android.database.sqlite.SQLiteDatabase$1.<init>(SQLiteDatabase.java:2321)
at android.database.sqlite.SQLiteDatabase.setMaxSqlCacheSize(SQLiteDatabase.java:2321)
at android.database.sqlite.SQLiteDatabase.<init>(SQLiteDatabase.java:2072)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1129)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1086)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1146)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1086)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1146)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1086)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1146)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1086)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1146)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1086)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1146)
at ...

どうも、SQLiteDatabase#openDatabase を再帰的に呼んでいる様子。
明らかに、SQLiteDatabase 内部で再帰呼び出しに入っているので、Android SDK 内のバグなのは間違いありません。
とはいえ、原因が分からない事には対策しようが無いので、とりあえず AOSP のコードを見てみることにしました。

が…クラッシュレポートには、発生時の端末名も Android のバージョン番号も記載がありません。
仕方がないので、Android 2.1 から順に見ていくことにしました。

参考
バージョン間の違いを見ていくのは結構面倒くさいのですが、以下のサイトを使うと効率的にコードをチェックすることができます。
GrepCode: android - Java Project - Source Code

すると、Android 4.0 の SQLiteDatabase.java に以下のようなコードが

SQLiteDatabase.java (Android 4.0)
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
/**
 * Open the database according to the flags {@link #OPEN_READWRITE}
 * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}.
 *
 * <p>Sets the locale of the database to the  the system's current locale.
 * Call {@link #setLocale} if you would like something else.</p>
 *
 * <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be
 * used to handle corruption when sqlite reports database corruption.</p>
 *
 * @param path to database file to open and/or create
 * @param factory an optional factory class that is called to instantiate a
 *            cursor when query is called, or null for default
 * @param flags to control database access mode
 * @param errorHandler the {@link DatabaseErrorHandler} obj to be used to handle corruption
 * when sqlite reports database corruption
 * @return the newly opened database
 * @throws SQLiteException if the database cannot be opened
 */
public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
        DatabaseErrorHandler errorHandler) {
    SQLiteDatabase sqliteDatabase = openDatabase(path, factory, flags, errorHandler,
            (short) 0 /* the main connection handle */);
 
    // set sqlite pagesize to mBlockSize
    if (sBlockSize == 0) {
        // TODO: "/data" should be a static final String constant somewhere. it is hardcoded
        // in several places right now.
        sBlockSize = new StatFs("/data").getBlockSize();
    }
    sqliteDatabase.setPageSize(sBlockSize);
    sqliteDatabase.setJournalMode(path, "TRUNCATE");
 
    // add this database to the list of databases opened in this process
    synchronized(mActiveDatabases) {
        mActiveDatabases.add(new WeakReference<sqlitedatabase>(sqliteDatabase));
    }
    return sqliteDatabase;
}
 
private static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
        DatabaseErrorHandler errorHandler, short connectionNum) {
    SQLiteDatabase db = new SQLiteDatabase(path, factory, flags, errorHandler, connectionNum);
    try {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.i(TAG, "opening the db : " + path);
        }
        // Open the database.
        db.dbopen(path, flags);
        db.setLocale(Locale.getDefault());
        if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
            db.enableSqlTracing(path, connectionNum);
        }
        if (SQLiteDebug.DEBUG_SQL_TIME) {
            db.enableSqlProfiling(path, connectionNum);
        }
        return db;
    } catch (SQLiteDatabaseCorruptException e) {
        db.mErrorHandler.onCorruption(db);
        return SQLiteDatabase.openDatabase(path, factory, flags, errorHandler);
    } catch (SQLiteException e) {
        Log.e(TAG, "Failed to open the database. closing it.", e);
        db.close();
        throw e;
    }
}


986行目で openDatabase(String, CursorFactory, int, DatabaseErrorHandler, short) を呼んでいるのですが、そのオープン時に SQLiteDatabaseCorruptException が発生した場合、1024行目で openDatabase(String, CursorFactory, int, DatabaseErrorHandler) を呼んでしまいます。
これはそのまま、986行目に行くので、同じメソッドが呼ばれ、そのメソッド内では同じデータベースを開こうとするので、当然 SQLiteDatabaseCorruptException が呼ばれ…
という現象が起きてしまうわけです。

報告されたスタックトレースと行番号がずれているので、絶対とは言えませんが、現象からしてこの問題で間違いないと思われます。

この問題の修正方法ですが、 今回の場合、事前にデータベースが壊れているかどうかを知る術がありませんので、SQLiteDatabase#openDatabase()try & catchで囲んで、データベースが壊れている旨のエラーメッセージを出すしか回避策はなさそうです。

ちなみに、この問題は Android 4.1 では解消しているようです1

  1. Android 3.x 系はソースコードが公開されていないので、わかりません。 
?
2013/08/15

Ubuntu 12.04 LTS で 左Ctrl と Caps Lock キーを入れ替える方法2種

いつも忘れてしまうので、備忘録がわりに。

X Window System スタート時

/etc/default/keyboard を以下のように書き換える
XKBMODEL="jp106"
XKBLAYOUT="jp"
XKBVARIANT="106"
XKBOPTIONS="ctrl:swapcaps"

ログイン時


System Settings → Keyboard

Typing → Layout Settings

Layouts → Options...

Ctrl key position → Swap Ctrl and Caps Lock
2013/08/14

Android の AutoCompleteTextView で文字と背景が白くなってしまう

Android 2.3 以前では AutoCompleteTextView などドロップダウンメニューの文字色が白になってしまい見えなくなってしまうという不具合が時々発生します。

たとえば、以下のコードを実行すると、
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
AutoCompleteTextView mAutoCompleteTextView;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 
    mAutoCompleteTextView = (AutoCompleteTextView) findViewById(R.id.autoCompleteTextView1);
 
    String[] list = new String[] {
            "alpha",
            "bravo",
            "charlie",
            "delta",
            "echo"
    };
    ArrayAdapter<String> adapter = new ArrayAdapter<String>(
            this,
            android.R.layout.simple_dropdown_item_1line,
            list);
    mAutoCompleteTextView.setAdapter(adapter);
    mAutoCompleteTextView.setThreshold(1);
 
    mAutoCompleteTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
        @Override
        public void onFocusChange(View v, boolean hasFocus) {
            if (hasFocus) {
                mAutoCompleteTextView.showDropDown();
            }
        }
    });
}

以下のようになります。
ちゃんと候補は登録されていますので、タップすれば入力可能です。
文字色が白、かつバックグラウンドが白なため、候補が見えなくなっちゃってるんですね。

これ、バグというより、デザインが統一されていないために起きた不具合と言えそうです。

というわけで、以下のように ArrayAdapter のコンストラクタに与える引数を android.R.layout.select_dialog_item に変えるとうまくいきます。
1
2
3
4
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
        this,
        android.R.layout.select_dialog_item,
        list);


2013/08/13

AutoCompleteTextView をタップした瞬間に候補を表示させる方法

Android で入力候補を表示させる場合、一般的に AutoCompleteTextView を使用すると思います。

AutoCompleteTextView には setThreshold というメソッドがあり、何文字入力した時点から候補を表示するか指定することが出来ます。
しかし、このメソッド、0以下を指定できません。
つまり、最低でも1文字入力しないと候補が出て来ないのです。

多分、大量の候補があった場合に、全部表示してしまうとパフォーマンスが低下するからなのでしょう。
しかし、候補が少ない場合はフォーカスが移った瞬間に候補を表示してあげた方が親切な場合もあります。

そんな場合は、以下のようにしてやれば、フォーカスが移った瞬間に候補を表示させることができます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
AutoCompleteTextView mAutoCompleteTextView;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 
    mAutoCompleteTextView = (AutoCompleteTextView) findViewById(R.id.autoCompleteTextView1);
 
    String[] list = new String[] {
            "alpha",
            "bravo",
            "charlie",
            "delta",
            "echo"
    };
    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
            android.R.layout.select_dialog_item, list);
    mAutoCompleteTextView.setAdapter(adapter);
    mAutoCompleteTextView.setThreshold(1);
 
    mAutoCompleteTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
        @Override
        public void onFocusChange(View v, boolean hasFocus) {
            if (hasFocus) {
                mAutoCompleteTextView.showDropDown();
            }
        }
    });
}

onFocusChangeListener で、フォーカスが移ったことを検知し、その際に強制的に showDropDown で候補を表示させるわけです。
アプリの作りによっては、他のイベントをトリガにした方が良い場合もあるかもしれません。