Android + Scala ですごく遅いQRコードリーダーを作ってみた

# 準備
[ScalaでAndroidアプリ開発環境を構築する](http://qiita.com/JunSuzukiJapan@github/items/2fb8cddadd1fd94226f1)という記事の実践編（？）。

前回の記事を参考に、Scala用Androidプロジェクトを作成しておきます。

# ZXingライブラリを追加

https://zxing.googlecode.com/files/ZXing-2.1.zip

上記のURLから"ZXing-2.1.zip"をダウンロードします。
（へたに、ZXing-2.2.zipをダウンロードすると、必要なjarファイルが含まれていないので注意。）

解凍して、下記のファイルをプロジェクト/libsに追加してやります。

core/core.jar
javase/javase.jar

# AndroidManifest.xmlでカメラの使用を許可する

Androidでカメラを使うにはマニフェストファイルで宣言しておく必要があります。

```text:AndroidManifest.xml
<uses-sdk （略） />
```

の後ろに、以下の宣言を追加しましょう。

```text:AndroidManifest.xml
    <uses-permission android:name="android.permission.CAMERA" />
```

# SurfaceViewを用意

res/layout/activity_main.xmlを書き換えて、カメラのプレビューを表示するためのSurfaceViewを用意します。

```text:activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <SurfaceView
        android:id="@+id/preview_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        />

</RelativeLayout>
```

# LuminanceSourceクラスを継承したクラスを作成

JavaでZXingを使う場合、サンプルにあるPlanarYUVLuminanceSource.javaをコピーして使うのが定番（？）っぽいですが、ここでは、Scalaで書き直したものを使用します。

```scala:YUVLuminanceSource.scala
package com.example.scalaqr

import com.google.zxing.LuminanceSource
import android.graphics.Bitmap

final class YUVLuminanceSource(val yuvData: Array[Byte], val dataWidth:Int, val dataHeight: Int,
    val left: Int, val top: Int, val width: Int, val height: Int) extends LuminanceSource(width, height)
{
  if (left + width > dataWidth || top + height > dataHeight) {
    throw new IllegalArgumentException("Crop rectangle does not fit within image data.");
  }

  override def getRow(y: Int, row: Array[Byte]): Array[Byte] = {
    if (y < 0 || y >= getHeight()) {
      throw new IllegalArgumentException("Requested row is outside the image: " + y)
    }

    val width = getWidth()
    var vRow = row 
    if (vRow == null || vRow.length < width) {
      vRow = new Array[Byte](width)
    }
    val offset = (y + top) * dataWidth + left
    System.arraycopy(yuvData, offset, vRow, 0, width)

    vRow
  }

  override def getMatrix(): Array[Byte] = {
    val width = getWidth()
    val height = getHeight()

    // If the caller asks for the entire underlying image, save the copy and give them the
    // original data. The docs specifically warn that result.length must be ignored.
    if (width == dataWidth && height == dataHeight) {
      return yuvData
    }

    val area = width * height
    val matrix = new Array[Byte](area)
    var inputOffset = top * dataWidth + left

    // If the width matches the full width of the underlying data, perform a single copy.
    if (width == dataWidth) {
      System.arraycopy(yuvData, inputOffset, matrix, 0, area)
      return matrix
    }

    // Otherwise copy one cropped row at a time.
    val yuv = yuvData;
    for(y <- 0 to height) {
    //for (int y = 0; y < height; y++) {
      val outputOffset = y * width
      System.arraycopy(yuv, inputOffset, matrix, outputOffset, width)
      inputOffset += dataWidth
    }

    matrix
  }

  override def isCropSupported(): Boolean = true
}
```

>必要がなさそうなメソッドは省いていますので、もしも必要になった場合には各自で実装してください。

# MainActivityをScalaで書く

src/MainActivity.javaを削除して、新たにsrc/MainActivity.scalaクラスを作成します。

```scala:MainActivity.scala
package com.example.scalaqr

import scala.collection.JavaConversions._
import scala.util.control.Breaks.{ break, breakable }
import _root_.android.app.Activity
import _root_.android.os.Bundle
import android.os.Bundle
import android.app.Activity
import android.view.Menu
import android.content.Context
import android.util.Log
import android.graphics.Point
import android.hardware.Camera
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.View.OnClickListener
import android.view.WindowManager
import android.widget.Toast
import android.widget.Button
import com.google.zxing.BinaryBitmap
import com.google.zxing.MultiFormatReader
import com.google.zxing.Result
import com.google.zxing.common.HybridBinarizer

class MainActivity extends Activity {
  private val TAG = "ScalaQR"
  private val MIN_PREVIEW_PIXCELS = 320 * 240
  private val MAX_PREVIEW_PIXCELS = 800 * 480
  
  private var mSurfaceView: SurfaceView = null

  override protected def onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    mSurfaceView = findViewById(R.id.preview_view).asInstanceOf[SurfaceView]
    mSurfaceView.setOnClickListener(mSurfaceHolderCallback)
    val holder = mSurfaceView.getHolder()
    holder.addCallback(mSurfaceHolderCallback)
    holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
  }

  override def onCreateOptionsMenu(menu: Menu): Boolean = {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu)
    true
  }

  protected def isPortrait: Boolean =
    getResources().getConfiguration().orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT

  //
  // SurfaceHolderCallback methods
  //
  private val mSurfaceHolderCallback:SurfaceHolder.Callback
    with OnClickListener
    with Camera.AutoFocusCallback
  = new SurfaceHolder.Callback()
    with OnClickListener
    with Camera.AutoFocusCallback
  {
    private var mSurfaceHolder: SurfaceHolder = null
    private var mCamera: Camera = null

    //
    // OnClickListener
    //
    override def onClick(v: View) {
      if(v == mSurfaceView){
    	  // オートフォーカスON
    	  mCamera.autoFocus(mSurfaceHolderCallback)
      }
    }

    //
    // 生成されたとき
    //
    override def surfaceCreated(holder: SurfaceHolder) {
      mSurfaceHolder = holder
      try {
        // プレビューをセットする
    	mCamera = Camera.open()
        mCamera.setPreviewDisplay(holder)
        mCamera.setPreviewCallback(mPreviewCallback)
      } catch {
        case e: Exception => e.printStackTrace()
      }
    }

    //
    // 変更されたとき
    //
    override def surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
      val parameters = mCamera.getParameters()
      val p = pointFromCameraParameters(mCamera)
      parameters.setPreviewSize(p.x, p.y)

      if (isPortrait) {
        mCamera.setDisplayOrientation(90)
      } else {
        mCamera.setDisplayOrientation(0)
      }

      // width, heightを変更する
      mCamera.setParameters(parameters)
      mCamera.startPreview()
    }

    //
    // 破棄されたとき
    //
    override def surfaceDestroyed(holder: SurfaceHolder) {
      mCamera.release()
    }

    //
    // Camera.AutoFocusCallback methods
    //
    override def onAutoFocus(success: Boolean, camera: Camera) {
      if (success) // 現在のプレビューをデータに変換
        camera.setOneShotPreviewCallback(mPreviewCallback)
    }

    private def pointFromCameraParameters(camera: Camera): Point = {
      val parameters = camera.getParameters()
      val manager: WindowManager = 
         getApplication().getSystemService(Context.WINDOW_SERVICE).asInstanceOf[WindowManager]
      val display = manager.getDefaultDisplay()
      val width = display.getWidth()
      val height = display.getHeight()
      val screenSize = new Point(width, height)
      //Log.d(TAG, "screenSize = " + screenSize.toString())
      val previewSize = findPreviewSize(parameters, screenSize, false)
      //Log.d(TAG, "previewSize = " + previewSize.toString())

      previewSize
    }

    private def findPreviewSize(parameters: Camera#Parameters, screenSize: Point, portrait: Boolean): Point = {
      var previewSize: Point = null

      val list = parameters.getSupportedPreviewSizes()
      breakable {
        for (
          supportPreviewSize <- list
          if supportPreviewSize.width * supportPreviewSize.height >= MIN_PREVIEW_PIXCELS
          if supportPreviewSize.width * supportPreviewSize.height <= MAX_PREVIEW_PIXCELS
        ) {
          val supportedWidth = if (portrait) supportPreviewSize.height else supportPreviewSize.width
          val supportedHeight = if (portrait) supportPreviewSize.width else supportPreviewSize.height
          previewSize = new Point(supportedWidth, supportedHeight)
          break
        }
      }

      if (previewSize == null) {
        val defaultPreviewSize = parameters.getPreviewSize()
        previewSize = new Point(defaultPreviewSize.width, defaultPreviewSize.height)
      }
      //Log.d(TAG, "found previewPoint = " + previewSize.toString())

      previewSize
    }
  }

  //
  // オートフォーカスでピントがあったときに呼ばれる（？）
  // 　　QRコードを解析する
  //
  private val mPreviewCallback = new Camera.PreviewCallback() {
    //
    // Camera.PreviewCallback methods
    //
    override def onPreviewFrame(data: Array[Byte], camera: Camera) {
      //
      //  バーコード読み取り
      //

      // 読み込む範囲
      val previewWidth = camera.getParameters().getPreviewSize().width
      val previewHeight = camera.getParameters().getPreviewSize().height

      // プレビューデータから BinaryBitmap を生成 
      val source = new YUVLuminanceSource(
        data, previewWidth, previewHeight, 0, 0, previewWidth, previewHeight)
      val bitmap = new BinaryBitmap(new HybridBinarizer(source))

      // バーコードを読み込む
      val reader = new MultiFormatReader()
      var result: Result = null
      try {
        result = reader.decode(bitmap)
      } catch {
        case e: Exception => Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_SHORT).show()
      }

      if (result == null) {
        Toast.makeText(getApplicationContext(), "can not scan QR code.", Toast.LENGTH_LONG).show()
      } else {
        val text = result.getText()
        Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG).show()
      }
    }
  }

}
```

問題がなければ、これで動くはずです。
起動できたら、画面を一度タップするとQRコードの読み取りを始めます。

>うまく動かない場合には、プロジェクトの作成から確認しましょう。
>特に、[前回の記事](http://qiita.com/JunSuzukiJapan@github/items/2fb8cddadd1fd94226f1)のなかの「右クリック->"Add AndroidProguardScala Nature"」を忘れがちなので注意。

# 補足

画面が表示されるのに、QRコードが読み取れないときには、
うまく動いてないように見えても実は動いていることがあります。
ピントが合うのが**めちゃくちゃ遅い**です。
実用性は皆無ですが、一応こんなことも出来るということで…。