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)
    /**
     * 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 では解消しているようです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 などドロップダウンメニューの文字色が白になってしまい見えなくなってしまうという不具合が時々発生します。

たとえば、以下のコードを実行すると、
    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 に変えるとうまくいきます。
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
                this,
                android.R.layout.select_dialog_item,
                list);


2013/08/13

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

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

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

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

そんな場合は、以下のようにしてやれば、フォーカスが移った瞬間に候補を表示させることができます。
    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 で候補を表示させるわけです。
アプリの作りによっては、他のイベントをトリガにした方が良い場合もあるかもしれません。