2013年7月31日水曜日

PCで作成したSQLite DBをAndroidアプリで使う

辞書アプリなどのSQLiteに保存されたデータを使うアプリでは、アプリの初回起動時に何らかの方法でデータをDBにインサートする必要があると思います。
件数が少なければ外部ファイルからインサートしてもいいのですが、件数が多くなってくるとこのインサート処理にすごく時間がかかります。
ぐぐるとPCで予め作成したSQLiteのDBファイルをAndroidのassetsディレクトリに含めておき、アプリ起動時にそのままアプリのDBの置き場所(/data/data/<アプリのパッケージ>/databases)にコピーしてしまうという方法を見つけたので試してみました。

Y.A.M の 雑記帳: Android あらかじめ作成した SQLite database をアプリに取り込む

いつものようにすぐに上記のyanzmさんのサイトがヒットしたので、手順に従ってみました。

Androidが読み込めるSQLiteのDBファイルはandroid_metadataという以下の様なロケールの情報を含んだテーブルが必要となるらしいです。

create table android_metadata (  
    locale text default 'en_US'  
); 

自分で作成したテーブル+このテーブルが入っていればAndroidでもそのまま読み込まれるようです。詳細は上記ブログを確認して下さい。

上記のサイトで紹介されているSQLiteOpenHelperを継承したDBHelperクラスは、よくあるgetReadableDatabaseを使用せず独自にDBをオープンしていますが、既存のgetReadableDatabaseをそのまま使いたかったのでさらに調査を進めた所、getReadableDatabaseをそのまま使いつつDBはコピーしたものを使うようなDBHelperクラスの実装を見つけました。

[Android Programming] sqliteのDBファイルをPCでつくってandroidで使う - ipreachableの日記

naichilab - Android / iOSアプリ開発メモ: PCで作ったSQLiteのDBファイルをAndroidで使う

(下のブログでは上のブログの実装にgetReadableDatabase部分も追記しています。)

上記サイトの実装で確かに動作したのですが、ログを見るとgetReadableDatabaseやgetWritableDatabaseを呼ぶたびにDBがコピーされていました。

原因究明のためSQLiteOpenHelperのgetReadableDatabaseやgetWritableDatabaseの中身を調べると、onCreateが呼ばれる条件はSQLiteの’PRAGMA user_version;’の実行結果が0であるということが分かりました。

‘PRAGMA user_version;’って何?ってことでさらにググると以下のサイトにこのように書かれていました。

SQLiteのユーザバージョンを利用する - Basic

ユーザーによって設定できるバージョン番号を管理します。PRAGMA schema_versionコマンドとは違い、SQLite内部では使用されません。

どうやらSQLiteが内部で管理しているDBのバージョンのようですね。

SQLiteOpenHelperはこのuser_versionが0であれば作成したばかりのDBであると判断し、onCreate()を呼んで初期化を行います。
その後SQLiteOpenHelperのコンストラクタで渡したversionの値をuser_versionに保存します。
次回getReadableDatabaseを呼んだ時はこのuser_versionが0でないので、onCreateは呼ばれないという仕組みです。
ちなみにonDowngradeとonUpgradeもこのuser_versionを利用して実装されています。
SQLiteに保存されたuser_versionの値よりSQLiteOpenHelperのコンストラクタに渡したバージョンの方が大きければonUpgradeが呼ばれます。

これで上記実装がなぜgetReadableDatabaseを呼ぶたびにDBがコピーされるかの謎が分かりました。
以下は上記実装の抜粋です。

@Override
public synchronized SQLiteDatabase getWritableDatabase() {
    SQLiteDatabase database = super.getWritableDatabase();
    if (createDatabase) {
        try {
            database = copyDatabase(database);
        } catch (IOException e) {
            Log.wtf(TAG, e);
            // TODO: エラー処理
        }
    }
    return database;
}

private SQLiteDatabase copyDatabase(SQLiteDatabase database) throws IOException {
    // dbがひらきっぱなしなので、書き換えできるように閉じる
    database.close();

    // コピー!
    InputStream input = context.getAssets().open(SRC_DATABASE_NAME);
    OutputStream output = new FileOutputStream(databasePath);
    copy(input, output);

    createDatabase = false;
    // dbを閉じたので、また開く
    return super.getWritableDatabase();
}

@Override
public void onCreate(SQLiteDatabase db) {
    super.onOpen(db);
    // getWritableDatabase()したときに呼ばれてくるので、
    // 初期化する必要があることを保存する
    this.createDatabase = true;
}

流れとしては以下の通りです。

  1. getWritableDatabaseを呼ぶ
  2. オーバーライドしたgetWritableDatabaseの中でsuper.getWritableDatabase()を呼ぶ
  3. 初めはDBが存在しないはずなので、空のDBが作成された後空のDBのuser_versionは0なのでonCreateが呼ばれる
  4. onCreateでthis.createDatabase = trueとなる
  5. super.getWritableDatabaseの処理が終わってcreateDatabaseがtrueになっているので、copyDatabase(database)が呼ばれる
  6. DBコピー後に再度super.getWritableDatabaseが呼ばれる
  7. 今度はコピーされたDBが存在するが、ここでコピー後のDBのuser_versionが0だと再度onCreateが呼ばれる
  8. 再度onCreateでthis.createDatabase = trueとなるので、再度getWritableDatabaseを呼んだ時にまたcopyDatabase(database)が呼ばれてしまう

対策としてはcopyDatabaseの後にSQLiteDatabaseのsetVersionを呼んでコピー後のDBのuser_versionをSQLiteDBHelperに渡したバージョンと同じものに設定してもいいですが、私は以下のように回避してみました。以下はScalaで書かれています。Javaの人ごめんなさい。

object UnitsDBHelper {
  private val DB_VERSION = 1
  private val DB_NAME_ASSET = "units.db"
  private val DB_NAME = "units"
}

class UnitsDBHelper(context: Context) extends SQLiteOpenHelper(context, UnitsDBHelper.DB_NAME, null, UnitsDBHelper.DB_VERSION) {

  import UnitsDBHelper._

  private val DB_PATH = context.getDatabasePath(DB_NAME).getAbsolutePath
  private var doesDBExist = {
    val checkDb = try {
      SQLiteDatabase.openDatabase(DB_PATH, null, SQLiteDatabase.OPEN_READONLY)
    } catch {
      case e: SQLiteException => null
    }
    if (checkDb != null) checkDb.close()
    checkDb != null
  }

  override def getReadableDatabase = getDatabase(super.getReadableDatabase)

  override def getWritableDatabase = getDatabase(super.getWritableDatabase)

  private def getDatabase(getDBFunc: () => SQLiteDatabase) = synchronized {
    if (!doesDBExist) {
      try {
        copyDatabase()
        doesDBExist = true
      } catch {
        case e: IOException => {
          throw new Error("Error copying database")
        }
      }
    }
    getDBFunc()
  }

  private def copyDatabase() = {
    val input = context.getAssets.open(DB_NAME_ASSET)
    val output = new FileOutputStream(DB_PATH)
    val buffer = new Array[Byte](1024)
    var size = 0
    while (-1 != {size = input.read(buffer); size}) {
      output.write(buffer, 0, size)
    }

    output.flush()
    output.close()
    input.close()
  }

  def onCreate(db: SQLiteDatabase) {}

  def onUpgrade(p1: SQLiteDatabase, p2: Int, p3: Int) {}
}

doesDBExistはyanzmさんのブログのcheckDataBaseExistsメソッドと同じで、すでにDBがあるかどうかチェックしています。最初にDBがない状態であればこの値はfalseになるので、copyDatabaseが呼ばれDBがコピーされます。その後doesDBExistをtrueにして2回目以降のgetReadableDatabaseの呼び出しではDBの存在チェックはしないようにしています。

その後getDBFunc()でパラメータで渡されたsuper.getReadableDatabaseなどの関数が呼び出されます。SQLiteOpenHelperのgetReadableDatabaseは初回呼び出し時でも既にDBが存在していれば空のDBなどは作成されないみたいです。その後コピー後のuser_versionの値を見て0ならばonCreateが呼ばれ、その後user_versionの値が1となります(コンストラクタで1を渡しているため)。

以降のgetReadableDatabaseの呼び出しはdoesDBExistが常にtrueとなるはずなので、DBのコピーは起きない仕組みです。

今のところこれで動いていますが、何か問題があれば教えて下さい。

2013年7月23日火曜日

[Android] Spinnerでプログラムから選択肢を変更した時にonItemSelectedを呼ばないようにする

少しはまったのでメモ。

以下のようにsetOnItemSelectedListenerでnullを設定すればいけるかと思いきや、これではnullに設定してもonItemSelectedイベントが呼ばれてしまいます。

  spinner.setOnItemSelectedListener(null)
  spinner.setSelection(0)

以下のようにsetSelection(0, false)を使うとonItemSelectedが呼ばれずに選択肢だけ変更されます。

  spinner.setOnItemSelectedListener(null)
  spinner.setSelection(0, false)

AbsSpinner | Android Developers

RobotiumでUIに関するアクションを実行

RobotiumでSoloではサポートされていない操作を行う必要がある時、textView.setText(“hoge”)のようなUIに対する変更のコードをRobotiumのテストで実行すると、以下のエラーとなります。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

エラーを回避するには、以下のようにrunOnUiThreadを呼び出してUIスレッド上で実行します。

solo.getCurrentActivity.runOnUiThread(new Runnable {
  def run() {
    textView.setText("hoge")
  }
})

[Android] Spinnerの中でアイテムのpositionを取得する

Spinnerの選択肢を手動で変更する場合、以下のようにsetSelectionで引数にSpinner中のアイテムのpositionを指定します。

// spinnerの先頭を選択
spinner.setSelection(0)

問題はこのpositionをどうやって調べるかです。Spinnerから特定の文字列に合致するアイテムのpositionを得たいと思った場合、SpinnerにはgetChildAtというメソッドがあるので以下のようなコードを書きたくなりますが、これはうまくいきません。

for(index <- 0 until spinner.getChildCount) {
  if (value == spinner.getChildAt(index).getTag.asInstanceOf[String]) {
    spinner.setSelection(index)
  }
}

実行してみると分かりますが、spinnerのgetChildAtとgetChildCountは現在選択されているアイテム1つしか取得できません。SpinnerにはAdapter経由でアイテムを複数設定していても、getChildCountを実行すると1が返ってきます。

Spinner中のアイテムのpositionを取得するにはAdapterを調べる必要があります。以下はCursorAdapterの例です。

def getPositionOf(categoryId: String, cursor: Cursor): Int = {
  while(cursor.moveToNext()) {
    val id = cursor.getString(cursor.getColumnIndex("category_id"))
    if (id == categoryId) return cursor.getPosition
  }
  -1
}

val pos = getPositionOf("category1", spinner.getAdapter.asInstanceOf[CursorAdapter].getCursor)
if (pos >= 0) {
  spinner.setSelection(pos)
}

CursorAdapterのCursorの値を順番に調べ、合致する値が見つかったらその時のcursorのpositionの値をspinnerのsetSelectionの引数にセットしています。

[Android] CursorLoaderのロード後にListViewの選択行を変更

ListViewでadapterにSimpleCursorAdapterとCursorLoaderを使用している状況では、onLoadFinishedメソッドの中でadapter.swapCursor(cursor)を呼び出してListViewの中身を更新すると思います。
この時swapCursorの後にListViewの選択行を変更するには、以下のようにします。

adapter.swapCursor(cursor)
listView.post(new Runnable {
  def run() {
    val pos = 0 // 選択する行
    // listView.setItemChecked(pos, true)  //singleChoiceの時はこれも呼ぶ
    listView.setSelection(pos)
    listView.performItemClick(listView.getChildAt(pos), pos, 0)
  }
}

ポイントはswapCursorの後に直接listView.setSelectoin(pos)を呼ばずに、postメソッドを呼んでRunnable経由でsetSelectionを呼び出す点です。

なぜならデバッガで追うと分かりますが、swapCursorの時点ではまだListViewの描画が終わっていないため、setSelectionなどを呼んでも効果がないからです。

上記のようにするとListViewの描画後にrun()メソッドの中身が実行され、ListViewの選択行が正しく変更されます。

2013年7月22日月曜日

[Android] Cursorに行をマニュアルで追加

MatrixCursor、MergeCursorを以下のように使うと、Cursorにマニュアルで行を追加することができます。

def onLoadFinished(loader: Loader[Cursor], cursor: Cursor) {
  val matrixCursor = new MatrixCursor(Array("_id", "category_id", "name"))
  matrixCursor.addRow(Array[AnyRef]("-1", "all", "All"))
  val mergeCursor = new MergeCursor(Array[Cursor](matrixCursor, cursor))
  adapter.swapCursor(mergeCursor)
}

MatrixCursorはマニュアルでCursorのデータを作成するCursorです。
コンストラクタでテーブルのカラムを定義し、addRowで実際のデータを挿入します。

MergeCursorは文字通りコンストラクタに渡した複数のCursorが持つデータを全て1つのCursorにマージしたCursorです。

上の例はDBから取得したcategory一覧に加えてallというデフォルトのカテゴリを追加する際に使用しています。

2013年7月18日木曜日

[Android] ListViewの選択されたアイテムの背景色を変更2

[Android] ListViewの選択されたアイテムの背景色を変更では、ListViewの行のレイアウトのbackgroundに以下のselectorを設定して選択された行の背景色を変更しました。

res/drawable/list_item_selector.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_selected="true"
        android:drawable="@color/selected_list_item_color"/>
  <item android:state_pressed="true"
    android:drawable="@color/selected_list_item_color"/>
  <item android:drawable="@color/default_color"/>
</selector>

res/values/colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="selected_list_item_color">#ffd33f</color>
  <color name="default_color">#000000</color>
  <color name="clear">#00000000</color>
</resources>

しかし、この方法では画面の中にListView以外にテキストボックスなどが存在して、ListViewからフォーカスが外れると行の選択状態が解除されてしまいます。

これを防ぐためには以下のようにListViewにandroid:choiceMode=”singleChoice”を設定し、行のレイアウトもCheckableを継承したカスタムレイアウトを設定する必要があります。

  <ListView
      android:id="@+id/unit_list"
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:layout_weight="1"
      android:choiceMode="singleChoice"
      />

方法はこちらのブログで詳しく解説されています。私の記事のサンプルコードはScalaなので、Javaのサンプルコードはこちらのブログを参考にして下さい。

Y.A.M の 雑記帳: Android DrawableState でリストアイテムの背景を変える ViewGroup を作る

singleChoiceではCheckableを継承したレイアウトが必要になるので、RelativeLayoutとCheckableを継承したカスタムレイアウトを作成します。

object CheckableRelativeLayout {
  private val CHECKED_STATE_SET = Array[Int](android.R.attr.state_checked)
}

class CheckableRelativeLayout(context: Context, attrs: AttributeSet, defStyle: Int) extends RelativeLayout(context, attrs, defStyle) with Checkable {
  def this(context: Context, attrs: AttributeSet) = this(context, attrs, 0)
  def this(context: Context) = this(context, null)

  private var checked = false

  def setChecked(checked: Boolean) {
    if (this.checked != checked) {
      this.checked = checked
      refreshDrawableState()
    }
  }

  def isChecked: Boolean = checked

  def toggle() = setChecked(!checked)

  override def onCreateDrawableState(extraSpace: Int) = {
    val drawableState = super.onCreateDrawableState(extraSpace + 1)
    if (isChecked) {
      View.mergeDrawableStates(drawableState, CheckableRelativeLayout.CHECKED_STATE_SET)
    }
    drawableState
  }
}

list_row.xml

ListViewの行のレイアウトで使用していたRelativeLayoutを上記のカスタムレイアウトに置き換えます。

<?xml version="1.0" encoding="utf-8"?>
<com.shinichy.convertBox.CheckableRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:background="@drawable/list_item_selector"
    >
  <TextView
      android:id="@+id/symbol_text"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="symbol"
      android:textColor="@color/white"
      />
  <TextView
      android:id="@+id/value_text"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentRight="true"
      android:text="0"
      android:textColor="@color/white"
      />
</com.shinichy.convertBox.CheckableRelativeLayout>

res/drawable/list_item_selector.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_checked="true"
    android:drawable="@color/selected_list_item_color"/>
  <item android:drawable="@color/clear"/>    
</selector>

これで行をタップすると行の背景が変わり、フォーカスを失っても背景が変わったままになります。

ListViewをタップした時に色を変えないようにする

上記の変更を行うと、ListViewをタップして指を離してから選択行の色が変更されます。

ListViewはデフォルトではタップした時にも色が変わるようになっているので、タップして指を離すまでは背景色が2つの行で変更されてしまいます。
タップした時に色を変えない方法は別の記事にまとめましたので、この挙動が嫌な場合は以下の記事を参考にタップした行の色を変えないようにして下さい。

[Android] ListViewをタップした時に色を変えないようにする

[Android] ListViewをタップした時に色を変えないようにする

ListViewはデフォルトではタップすると色が反転するようになっています。
ListViewのlistSelectorに透明色(#00xxxxxx)を設定すると、タップした時に色が変わらないようになります。

  <ListView
      android:id="@+id/unit_list"
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:layout_weight="1"
      android:listSelector="@drawable/list_selector"
      />

res/drawable/list_selector.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:drawable="@color/clear" />
</selector>

res/values/colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="clear">#00000000</color>
</resources>

2013年7月17日水曜日

テスト用DBを使ってActivityInstrumentationTestCase2のテストを行う

RenamingDelegatingContextをSQLiteOpenHelperのコンストラクタに渡すと、テスト用のDBを使用することができます。
RenamingDelegatingContextはこちらの記事が詳しいです。

u1aryzの備忘録とか: RenamingDelegatingContextを使ってみた

前回の記事[Android] CursorLoaderのテストではMockコンテキストとMockContentResolverを使用して、CursorLoaderにRenamingDelegatingContextを使って初期化したContentProviderを渡してテスト用のDBにアクセスさせました。
また、Mockコンテキストを使用するためにActivityUnitTestCaseを用いました。

ActivityUnitTestCaseはMockコンテキストを利用できるので、対象のアプリケーションを変更する必要がない点が利点ですが、Robotium等を使ったテストなどはActivityInstrumentationTestCase2を使う必要があります。ActivityInstrumentationTestCase2ではMockコンテキストを利用できないため別の方法でRenamingDelegatingContextを対象のアプリケーションに渡さなければいけません。

テスト対象のコードをいじる以外にあまりいい方法を思いつかなかったのですが、ContentProvider内部でlazy valを使ってSQLiteOpenHelperの初期化を遅らせ、初めてDBにアクセスする必要がある段階でisTestフラグを見てテスト中ならRenamingDelegatingContext、本番ならgetContextのコンテキストをそれぞれ使用してSQLiteOpenHelperを初期化するというアプローチをとりました。

object UnitsProvider {
  val AUTHORITY = "com.shinichy.convertBox.provider"
  private val TEST_PREFIX = "test_"
  var isTest = false
}

class UnitsProvider extends ContentProvider {
  ...

  private lazy val unitsDBHelper: UnitsDBHelper = {
    Log.d(getClass.getSimpleName, "unitsDBHelper initialized")
    val context = if (UnitsProvider.isTest) {
      Log.d(getClass.getSimpleName, "UnitsProvider is in test mode")
      new RenamingDelegatingContext(getContext, UnitsProvider.TEST_PREFIX)
    } else {
      getContext
    }
    new UnitsDBHelper(context)
  }

  // アプリケーション起動時にこのonCreateが呼ばれるため、ここではSQLiteOpenHelperの初期化は行わない
  def onCreate(): Boolean = {
    true
  }

  ...

  class UnitsDBHelper(context: Context) extends SQLiteOpenHelper(context, UnitsDBHelper.DB_NAME, null, UnitsDBHelper.DB_VERSION) {
   ...

lazy valはJavaにはないため、もしJavaで同じ事を実現するなら以下の様なメソッドを用いて同じ事が実現できると思います。もちろんhelperは直接使わず、SQLiteOpenHelperが必要な箇所は全てgetHelper()を使用するようにします。

private SQLiteOpenHelper helper = null;

private SQLiteOpenHelper getHelper() {
    if (helper == null) {
        Context context = null;
        if (isTest) {
            context = new RenamingDelegatingContext(getContext(), TEST_PREFIX);
        } else {
            context = getContext();
        }
        helper = new UnitsDBHelper(context);
    }
    return helper;
}

以下がテストコードの抜粋です。

class MainActivityTest extends ActivityInstrumentationTestCase2(classOf[MainActivity]) {

  var solo: Solo = null
  var activity: MainActivity = null

  override def setUp() {
    UnitsProvider.isTest = true
    solo = new Solo(getInstrumentation, getActivity)
    activity = getActivity
  }
  ...

setUpの最初にisTestフラグを立てます。これ以降最初にunitsDBHelperを使用する箇所でlazy valの箇所が評価され、RenamingDelegatingContextを用いてUnitsDBHelperが初期化され、テスト用のDBが使用されるようになります。

RenamingDelegatingContextはandroid.testパッケージにあるため、AndroidManifest.xmlのapplication要素の中に以下のuses-libraryと書いておかないとエラーログがはき出されてしまいます。

<uses-library android:name="android.test.runner"/>

[Android] CursorLoaderのテスト

現在開発中のアプリではCursorLoaderとSimpleCursorAdapterを使ってDBのデータをListViewに表示しています。
CursorLoader、SimpleCursorAdapterの使い方は以下の記事が詳しいです。

コジオニルク - Android - パワフルなCursorLoader

このCursorLoaderをテスト実行時にテスト用のDBからデータをロードする方法を紹介します。
下記のサンプルコードはScalaです。

まずはテスト用のContentResolverとContextを作成します。

TestContentResolver、TestContext

class TestContentResolver(context: Context) extends MockContentResolver {
  private val TEST_PREFIX = "test_"
  val provider = new UnitsProvider
  val info = new ProviderInfo()
  info.authority = UnitsProvider.AUTHORITY
  info.enabled = true
  info.packageName = UnitsProvider.getClass.getPackage.getName

  val delegatingContext = new RenamingDelegatingContext(context, TEST_PREFIX)
  provider.attachInfo(delegatingContext, info)
  addProvider(UnitsProvider.AUTHORITY, provider)
}

class TestContext(context: Context) extends ContextWrapper(context) {
  private lazy val resolver: ContentResolver = new TestContentResolver(context)

  override def getContentResolver = resolver

  // CursorLoader calls context.getApplicationContext inside it, so override this too.
  override def getApplicationContext = this
}

MockContentResolverはモック作成用のクラスで、このクラスを継承して独自のContentResolverを作成します。
Contextの方も同様にMockContextというクラスもありますが、MockContextを継承して作成したモックをActivityUnitTestCaseのsetActivityContextで使用するとエラーとなってしまうため、ActivityUnitTestCaseではContextWrapperを継承したモックContextを使用します。
CursorLoaderは内部でgetApplicationContextを呼んで使用するため、こちらもオーバーライドしてモックを返すようにします。

UnitsProviderはCursorLoaderで使用するための私が作成した独自のContentProviderです。
通常ContentProviderではonCreateメソッドの中でContentProvider内部で使用するためのSQLiteOpenHelperを継承したクラスを初期化します。

UnitsProvider(独自のContentProvider) 抜粋

class UnitsProvider extends ContentProvider {
  ...

  def onCreate(): Boolean = {
    unitsDBHelper = new UnitsDBHelper(getContext)
    true
  }

UnitsDBHelper(抜粋)

  class UnitsDBHelper(context: Context) extends SQLiteOpenHelper(context, UnitsDBHelper.DB_NAME, null, UnitsDBHelper.DB_VERSION) {
    ...

RenamingDelegatingContextはテスト用のDBを作るためのContextで、これをSQLiteOpenHelperのコンストラクタに渡すと実行時に第2引数で指定したPrefix付きのSQLite DBファイルが作成されます。
詳細は以下の記事が詳しいです。

u1aryzの備忘録とか: RenamingDelegatingContextを使ってみた

TestContentResolverはこのRenamingDelegatingContextを作成し、provider.attachInfo(delegatingContext, info)の箇所でUnitsProviderにこのコンテキストをセットします。UnitsProviderの中でgetContextをするとセットされたRenamingDelegatingContextが返されます。
addProvider(UnitsProvider.AUTHORITY, provider)でここで作成したテスト用のUnitsProviderを登録します。

MainActivityTest

以下がテストコードです。ActivityInstrumentationTestCase2がよく使われると思いますが、モックContextを設定できないためモックContextを使いたい場合はActivityUnitTestCaseを継承させます。
以下のページで違いが詳しく解説されています。

Activity Testing | Android Developers

SoloはRobotiumのコードです。

class MainActivityTest extends ActivityUnitTestCase(classOf[MainActivity]) {

  import com.shinichy.convertBox.TR

  var solo: Solo = null
  var activity: MainActivity = null

  override def setUp() {
    val mockContext = new TestContext(getInstrumentation.getTargetContext)
    setActivityContext(mockContext)
    startActivity(new Intent(), null, null)
    solo = new Solo(getInstrumentation, getActivity)
    activity = getActivity
    getInstrumentation.callActivityOnStart(activity)
  }

  override def tearDown() {
    solo.finishOpenedActivities()
  }

  def testUnitList() {
    val unitList = activity.findView(TR.unit_list)
    solo.sleep(1000)
    assertEquals(5, unitList.getCount)
  }
}

まず上で定義したモックContextを作成し、setActivityContextを呼び出してセットします。
この後startActivityで作成したActivityの中でgetContextやgetContentResolverを呼び出すとモックのTestContextやTestContentResolverが使われるようになります。

startActivityで作成したActivityはonCreateが呼ばれた状態でonStartやonResumeは呼ばれていません(ActivityInstrumentationTestCase2ではonResumeまで呼ばれた状態)。CursorLoaderはonStartの状態でないとロードされない(onLoadFinishedが呼ばれない)ため、getInstrumentation.callActivityOnStart(activity)でonStartの状態にするとロードされます。ロードは非同期に実行されるため、solo.sleep(1000)で1秒待ってDBからの読み込みが完了してからListViewで作成された行数をチェックしています。

MainActivity(抜粋)

以下はテスト対象のActivityです。CursorLoader、SimpleCursorAdapterを使ってListViewにDBのデータを表示しています。

class MainActivity extends Activity with TypedActivity with LoaderManager.LoaderCallbacks[Cursor] {
  override def onCreate(bundle: Bundle) {
    super.onCreate(bundle)
    setContentView(R.layout.main)

    adapter = new SimpleCursorAdapter(
      this,
      R.layout.list_row,
      null,
      Array[String]("symbol"),
      Array[Int](R.id.symbol_text),
      0
    )

    val unitList = findView(TR.unit_list)
    unitList.setAdapter(adapter)
    getLoaderManager.initLoader(0, null, this)
  }

  def onCreateLoader(id: Int, args: Bundle): Loader[Cursor] = {
    new CursorLoader(getApplicationContext, Contract.UNITS.contentUri, null, null, null, null)
  }

  def onLoadFinished(loader: Loader[Cursor], c: Cursor) {
    adapter.swapCursor(c)
  }

  def onLoaderReset(p1: Loader[Cursor]) {
    adapter.swapCursor(null)
  }
}

2013年7月16日火曜日

IntelliJ+sbt+Androidな環境のデバッグ方法

sbt-androidプラグインを使ってScalaでAndroid開発を行う場合のIntelliJのデバッガの設定方法を紹介します。

実機側の設定

実機側の設定はJavaで開発する時と同じです。
以下はAndroid 4.2.2の設定方法です。

  1. 設定 > セキュリティ > 提供元不明のアプリ にチェック
  2. 開発者向けオプションが表示されていない場合、端末情報 > ビルド番号 を7回タップすると開発モードとなって開発者向けオプションの設定項目が表示される
  3. 開発者向けオプション > USBデバッグ にチェック

IntelliJの設定

  1. Androidプロジェクトを開いて Run > Edit Configurations… でAndroid Applicationを選択して新規作成
  2. デバッグ対象のモジュールを選択
  3. “Do not launch Activity”にチェック
  4. “Deploy application”のチェックを外す
  5. Target Deviceを選択
  6. Before launchのMakeを削除
  7. OKを押して閉じる
  8. Run > Debug…で上記の構成を選択してデバッグ実行
  9. “Connected to the target VM, address: ‘localhost:8600’, transport: ‘socket’”というメッセージが表示されれば成功です
    もしうまくいかない場合は’adb devices’コマンドで端末が表示されるか確認してみて下さい

sbtの設定

特に必要ありません。sbtからアプリケーションをインストールして実行すると、設定したブレークポイントがあればヒットするはずです。

アプリケーション起動時にデバッガをアタッチする

onCreateなどの起動時の処理のデバッグをしたい場合は、端末の 開発者向けオプション > デバッグアプリを選択 をタップして対象のアプリを選択し、”デバッガを待機”にチェックを入れると、アプリケーションを起動してもデバッガがアタッチされるまで実行されません。

2013年7月12日金曜日

sbtでJUnit実行後にレポートを出力

sbtでJUnitのテストを実行するにはbuild.sbtのlibraryDependenciesに以下を追加するだけです。

"com.novocode" % "junit-interface" % "0.8" % "test->default"

しかし、これだけだとコンソールに結果が表示されるだけで、xmlに結果は出力されません。
xml形式のレポートを出力するためにはsbt-simple-junit-xml-reporter-pluginを導入する必要があります。

Githubのページにもありますが導入は非常に簡単で、project/project/plugins.scalaに以下を記述するだけです。

project/project/plugins.scala

import sbt._

object Plugins extends Build {
    lazy val plugins = Project("plugins", file("."))
    .dependsOn(
        uri("git://github.com/bseibel/sbt-simple-junit-xml-reporter-plugin.git")
    )
}

このプラグイン導入後にJUnitのテストを実行すると、プロジェクトのルートディレクトリのtest-reportsディレクトリにxmlのレポートが出力されます。

ただ、ルートディレクトリに勝手に出力されるのが気に入らなかったので、target/test-reportsディレクトリに出力されるよう書き換えたものをGithubに上げました。
shinichy/sbt-simple-junit-xml-reporter-plugin

project/project/plugins.scalaで指定したuriを以下のようにgit://github.com/shinichy/sbt-simple-junit-xml-reporter-plugin.gitに変更すると私が修正したものが代わりに使用されます。

project/project/plugins.scala

import sbt._

object Plugins extends Build {
    lazy val plugins = Project("plugins", file("."))
    .dependsOn(
        uri("git://github.com/shinichy/sbt-simple-junit-xml-reporter-plugin.git")
    )
}

こちらのプラグインを使用すればJUnitの結果はtarget/test-reportsディレクトリに出力されるようになります。

sbt-androidでRobolectricのテストを実行

RobolectricはActivityなどのAndroidのライブラリに依存するテストを実機やエミュレータを用いずに、通常のJVM上で実行可能にするフレームワークです。
エミュレータを使わないので高速に動作し、JenkinsなどのCIサーバ上でも通常のテストと同様そのまま実行することができます。
動作原理は以下の記事が詳しいです。

Robolectric - Androidのクラスを端末やエミュレータなしで単体テ…:ITpro

今回はこのRobolectricのテストをsbt-androidから実行する方法を説明します。
方法は大きく2つあり、1つはrobospecsを使ってSpecs2で実行する方法、もう1つはRobolectricが標準で対応しているJUnitを使う方法です。それぞれ以下のメリット、デメリットがあります。

robospecsを使う方法

  • メリット
    • Specs2を使用してテストを書くことができる
  • デメリット
    • Robolectricのバージョンが古い(1.1)

JUnitを使う方法

  • メリット
    • 最新のRobolectricを使用できる
  • デメリット
    • JUnit 4を使用しなければならない

僕は最新のRobolectricを使用したかったので、RobolectricのテストはJUnitを使うことにしました。Androidのフレームワークに依存しないPOSOなScalaのクラスはSpecs2でテストを書いています。

導入方法

  1. build.sbtのlibraryDependenciesに以下を追加

      "org.robolectric" % "robolectric" % "2.1.1" % "test",
      "junit" % "junit" % "4.11" % "test",
      "com.novocode" % "junit-interface" % "0.8" % "test->default"
    

    junit-interfaceはsbtでJUnitのテストを実行するためのものです。

  2. Robolectricのテストを追加

    src/test/scala/com/example/test/MainActivityTest.scala

    package com.example.test
    
    import org.junit.runner._
    import org.robolectric.annotation.Config
    import org.robolectric.{Robolectric, RobolectricTestRunner}
    import org.junit.{Test, Before}
    import com.example.{TR, MainActivity}
    import org.junit.Assert._
    
    @RunWith(classOf[RobolectricTestRunner])
    @Config(manifest="src/main/AndroidManifest.xml")
    class MainActivityTest {
      var activity: MainActivity = null
    
      @Before
      def setUp() {
        activity = Robolectric.buildActivity(classOf[MainActivity]).create().get()
      }
    
      @Test
      def searchText() {
        val searchText = activity.findView(TR.search_text)
        assertNotNull(searchText)
      }
    }
    

    上のテストはあくまでサンプルなので、自分のプロジェクトに合わせて適宜書き換えて下さい。ポイントは以下の部分です。

    @Config(manifest="src/main/AndroidManifest.xml")
    

    sbt-androidのプロジェクトではAndroidManifest.xmlはsrc/mainにあると思いますが、JavaのAndroidプロジェクトではプロジェクトのルートディレクトリに通常存在します。Robolectricは実行時にカレントディレクトリ(通常はプロジェクトのルートディレクトリ)にあるAndroidManifest.xmlを読もうとするため、上記のようにしてAndroidManifest.xmlを指定しないとエラーとなってしまいます。またnew Activity()のように直接Activityをnewすると警告が出たため、Robolectric.buildActivityを使用してActivityを取得しています。

  3. 実行

    $ sbt test
    

    Specs2、JUnitのテストが同じプロジェクトに混在していてもきちんと全てのテストが実行され、結果も集計されて表示されます。ほんとよくできてますよね!

2013年7月11日木曜日

sbt-androidでRobotiumのテストを実行

SBT-Android 0.7でRobotiumなどのActivityやViewなどのAndroidのライブラリに依存したテストの実行方法が少し変わっているようなので、sbt-androidからRobotiumのテストの実行方法について説明します。

giter8でテンプレートからプロジェクトを作成しているとtestsディレクトリが初めからできていると思いますが、その中にRobotiumのテストコードを置きます。

tests/src/main/scala/MainActivityTest.scala

import android.test.ActivityInstrumentationTestCase2
import com.jayway.android.robotium.solo.Solo
import junit.framework.Assert._
import com.shinichy.convertBox.MainActivity

class MainActivityTest extends ActivityInstrumentationTestCase2(classOf[MainActivity]) {
  // need to import here to refer to com.shinichy.convertBox.TR
  import com.shinichy.convertBox.TR

  var solo: Solo = null
  var activity: MainActivity = null

  override def setUp() {
    solo = new Solo(getInstrumentation, getActivity)
    activity = getActivity
  }

  override def tearDown() {
    solo.finishOpenedActivities()
  }

  def testHello() {
    val searchText = activity.findView(TR.search_text)
    solo.enterText(searchText, "Hello")
    assertEquals("Hello", searchText.getText.toString)
  }
}

次にtests/src/mainにテスト用のAndroidManifest.xmlを作成します。
書き方は以下を参考にして下さい。
Testing from Other IDEs | Android Developers

以下はcom.shinichy.convertBoxパッケージをテストする例です。

tests/src/main/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.shinichy.convertBox.tests"
          android:versionCode="1"
          android:versionName="1.0">
  <application>
    <uses-library android:name="android.test.runner"/>
  </application>
  <instrumentation android:name="android.test.InstrumentationTestRunner"
                   android:targetPackage="com.shinichy.convertBox"
                   android:label="Tests for com.shinichy.convertBox"/>
</manifest>

sbtでtestsプロジェクトを対象にtestコマンドを実行します。あらかじめテスト対象のAPKをインストールしておいて下さい。

sbt
tests/test

参考

2013年7月7日日曜日

CloudBees Dev@cloudのJenkinsで別バージョンのsbtを使う

この記事を書いている時点ではDev@cloudのJenkinsにインストールされているsbtのバージョンは0.11.3と少し古いです。
ここではDev@cloudのWebDav Repositoryに任意のsbt-launch.jarを置いて、Jenkinsから使う方法を紹介します。

  1. プライベートリポジトリに使用したいバージョンのsbt-launch.jarを置きます。

    以下のprivateリンク先のページでプライベートリポジトリのWebDAVのURLとアクセス方法が書いてあります。
    MacならFinderで⌘+K (サーバへ接続)でURLを入力するとマウントできます。

    マウントに成功するとMacでは/Volumes/privateでアクセス可能となるので、ここにsbt-launch.jarを置きます。

  2. Build Environmentで以下にチェックを入れます。

    チェックを入れるとJenkinsタスクからは/private/<account-id>でプライベートリポジトリのファイルにアクセスできるようになります。

  3. “Manage Jenkins” > “Configure System”でsbtのパスを設定

    プライベートリポジトリに置いたsbt-launch.jarのパスを入力します。

    もちろんJenkins sbt pluginをインストールしていることが前提です。

この方法はsbtに限らず色んな場面で役立ちそうですね!