Scala で Android アプリ開発(Log 編)

ログを出力する際、ログ出力位置の情報も一緒に出力する。

package com.github.cooldaemon.HelloWorld

import _root_.android.util.{Log => ALog}

object Log {
  val TAG = "HelloWorld"
  
  def e(m: String) = printlog(ALog.e, m)
  def w(m: String) = printlog(ALog.w, m)
  def i(m: String) = printlog(ALog.i, m)
  def d(m: String) = printlog(ALog.d, m)
  def v(m: String) = printlog(ALog.v, m)
  
  private def printlog(f: (String, String) => Int, m: String): Int = {
    val st = ((new Throwable()).getStackTrace).apply(2)
    f(TAG, "%s.%s(%s:%s): %s".format(
      st.getClassName, st.getMethodName, st.getFileName, st.getLineNumber, m
    ))
  }
}

製品出荷時には、ログを出力しないよう注意。

Scala で Android アプリ開発(AsyncTask 編)

ScalaでAndroidアプリ作成時、AsyncTaskの可変長引数メソッドが使えないことへの対策とサンプル ― Gist を丸パクリ。

Scala は、可変長引数を持つメソッドを上書きできないので、可変長引数を上書き済みの /path/to/hello-world/src/main/java/com/github/cooldaemon/HelloWorld/AsyncTask.java を作成する。

package com.github.cooldaemon.HelloWorld;

public abstract class AsyncTask<Params, Progress, Result> extends android.os.AsyncTask<Params, Progress, Result> {
   @Override
   protected Result doInBackground(Params... params) {
      return doInBackground(params.length > 0 ? params[0] : null);
   }

   abstract protected Result doInBackground(Params param);

   @Override
   protected void onProgressUpdate(Progress... values) {
      onProgressUpdate(values.length > 0 ? values[0] : null);
   }

   protected void onProgressUpdate(Progress value) {}

   @SuppressWarnings({"unchecked"})
   protected final void publishProgress(Progress value) {
      super.publishProgress(value);
   }
}

用意した AsyncTask を継承して、別スレッドで処理したいタスクを記述する。

//可変長引数を使えない分は、case class で対応する
case class FooParam(foo: String, bar: String)

//  AsyncTask から Activity を操作する(doInBackground は除く)ために Activity のインスタンスを渡しておく
class FooTask(val activity: FooActivity) extends AsyncTask[FooParam, Int, Either[Throwable, Unit]] {
  override protected def onPreExecute() {
    // Activity に変化を加える。例えばダイアログの表示など
  }

  // doInBackground だけ別スレッドで動作する
  // doInBackground 内で publishProgress を使うと UI のスレッドに Int の値を送れる
  // 送った Int の値は onProgressUpdate で受け取れる
  override protected def doInBackground(param: FooParam): Either[Throwable, Unit] = {
    for {
      _     <- ham(param).right
      chick <- egg(param).right
      _     <- spam(param, chick).right
    } yield ()
  }

  override protected def onProgressUpdate(progress: Int) {
    // プログレスの更新
  }

  // AsyncTask の cancel メソッドを使用されると onCancelled が呼ばれる
  override protected def onCancelled() {
    // doInBackground の停止を促す。例えば doInBackground で参照しているフラグを onCancelled で更新するなど
  }

  override protected def onPostExecute(result: Either[HttpClientErrorResult, Unit]) {
    // Activity に変化を加える。例えばダイアログを消すなど
  }
}

このコードは、画面の回転に対応していない。
画面が回転すると Activity のインスタンスは置き換えられるが AsyncTask は残り続けるので AsyncTask が保持する Activity のインスタンスを置き換える必要がある。
また、画面の回転と ProgressDialog を組み合わせる場合、画面回転後に ProgressDialog を再表示する必要があり、onCreateDialog に頼る事になる。
ProgressDialog を再表示する場合、以前の ProgressDialog 進行状況を引き継いだり、キャンセル処理を正常に動作させる必要があるので、なるべく画面が回転しないように固定しておいた方が良い。
もし、どうしても AsyncTask + ProgressDialog + 画面回転に対応したいのであれば、Activity から操作できる AsyncTask と ProgressDialog を管理する object を用意する。
この辺りは、ダウンローダ編で詳しく解説する。

Scala で Android アプリ開発(AlertDialog 編)

毎回、AlertDialog を作るのは面倒なので、次のようなオブジェクトを作っておく。

package com.github.cooldaemon.HelloWorld

import _root_.android.content.Context
import _root_.android.app.{Dialog, AlertDialog => AAlertDialog}
import _root_.android.content.DialogInterface

object AlertDialog {
  def create(message: String)(f: () => Unit)(implicit c: Context): Dialog = {
    (new AAlertDialog.Builder(c))
      .setMessage(message)
      .setCancelable(true)
      .setPositiveButton("はい", new DialogInterface.OnClickListener() {
        override def onClick(dialog: DialogInterface, id: Int) {
          f.apply()
          dialog.dismiss()
        }
      })
      .setNegativeButton("いいえ", new DialogInterface.OnClickListener() {
        override def onClick(dialog: DialogInterface, id: Int) {
          dialog.cancel()
        }
      })
      .create()
  }
}

次のように onCreateDialog 内で使用する。

class FooActivity extends TypedActivity {
  // ..snip..

  object DialogID extends Enumeration {
    val WIFI,BLUETOOTH = Value
  }

  override protected def onCreateDialog(id: Int, args: Bundle): Dialog = id match {
    case id if id == DialogID.WIFI.id =>
      AlertDialog.create("Wi-Fi設定を行う") { () =>
        startActivity(new Intent(android.provider.Settings.ACTION_WIFI_SETTINGS))
      }
    case id if id == DialogID.BLUETOOTH.id =>
      AlertDialog.create("Bluetooth設定を行う") { () =>
        startActivity(new Intent(android.provider.Settings.ACTION_BLUETOOTH_SETTINGS))
      }
  }

  // ..snip..
}

Scala で Android アプリ開発(NDK 編)

下準備

始めに /path/to/hello-world/project/build.scala を次のように修正する。

// ..snip..

object AndroidBuild extends Build {
  lazy val main = Project (
    "Hello World",
    file("."),
    settings = General.fullAndroidSettings ++
               AndroidNdk.settings // これを追加
  )

  // ..snip..
}

次に /path/to/hello-world/src/main/jni/ を作成する。

$ make -p /path/to/hello-world/src/main/jni/

ラッパークラスを作る

ものは試しにファイルの情報を取得するラッパ /path/to/hello-world/src/main/scala/FileStat.scala を作成する。

package com.github.cooldaemon.HelloWorld

import _root_.java.io.File
import _root_.java.util.{Map => JMap}
import _root_.scala.collection.JavaConversions._

object extendFileStat {
  implicit def fileToFileStat(file: File): FileStat = new FileStat(file)
}

class FileStat(file: File) {
  System.loadLibrary("file_stat")

  @native private[this] def getStat(name: String): JMap[String, Long]
  def stat: Map[String, Long] = mapAsScalaMap(getStat(file.toString)).toMap
}

C 関連のファイルを用意する

.class ファイルを作るため一度コンパイルし、その後、javah でラッパクラスから C ヘッダファイルを作成する。

$ cd /path/to/hello-world
$ sbt
> compile
> exit
$ javah -o ./src/main/jni/file_stat.h -classpath ./target/scala-2.9.1/classes com.github.cooldaemon.HelloWorld.FileStat

sbt 経由で javah を実行する方法は試していない。

Makefile として /path/to/hello-world/src/main/jni/Android.mk を作成する。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := file_stat
LOCAL_SRC_FILES := file_stat.c

#LOCAL_C_INCLUDES += $(SBT_MANAGED_JNI_INCLUDE)

include $(BUILD_SHARED_LIBRARY)

最後にファイル本体 /path/to/hello-world/src/main/jni/file_stat.c を作成する。

#include <sys/stat.h>
#include "file_stat.h"

jobject NewLong(JNIEnv* env, jlong value)
{
    jclass longClass = (*env)->FindClass(env, "java/lang/Long");
    jmethodID init = (*env)->GetMethodID(env, longClass, "<init>", "(J)V");
    jobject longObj = (*env)->NewObject(env, longClass, init, value);
    (*env)->DeleteLocalRef(env, longClass);
    return longObj;
}

JNIEXPORT jobject JNICALL Java_com_github_cooldaemon_HelloWorld_FileStat_getStat
  (JNIEnv *env, jobject obj, jstring name)
{
    jclass mapClass = (*env)->FindClass(env, "java/util/HashMap");
    jmethodID init = (*env)->GetMethodID(env, mapClass, "<init>", "(I)V"); 
    jobject mapObj = (*env)->NewObject(env, mapClass, init, 1);

    jmethodID put = (*env)->GetMethodID(env, mapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

    jboolean iscopy;
    const char *mfile = (*env)->GetStringUTFChars(env, name, &iscopy);    

    struct stat finfo;
    lstat(mfile, &finfo);

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "dev"),
        NewLong(env, finfo.st_dev)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "ino"),
        NewLong(env, finfo.st_ino)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "mode"),
        NewLong(env, finfo.st_mode)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "nlink"),
        NewLong(env, finfo.st_nlink)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "uid"),
        NewLong(env, finfo.st_uid)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "gid"),
        NewLong(env, finfo.st_gid)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "rdev"),
        NewLong(env, finfo.st_rdev)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "size"),
        NewLong(env, finfo.st_size)
    );

コンパイルしてみる。

$ sbt
> android:ndk-build

使用する

val f = new File(context.getFilesDir, "foo")
import extendFileStat._
f.stat foreach {case (k, v) => Log.d(k + ":" + v.toString)}