Android Studioを使ってエミュレータ上でカメラを起動する

# はじめに
Android Studioでカメラ機能を実装する際は以下の３つのライブラリが使用できるそうです

- CameraX
- Camera2
- Camera(サポート終了)

CameraXは多くのAndroid端末で使用可能な上、実装が簡単とのことなので、本記事ではAndroid studio & CameraXを用いてエミュレータ上でカメラを起動する方法を解説していきます

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [実行環境](#実行環境)
1. [プロジェクトの作成](#プロジェクトの作成)
1. [コーディング](#コーディング)
1. [カメラ設定](#カメラ設定)
1. [実行](#実行)
1. [参考文献](#参考文献)
1. [今後の展望](#今後の展望)

# 実行環境
- Windows11
- Android Studio Giraffe

# プロジェクトの作成

New -> projectからEmpty Views Activityを作成。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287818/4c9990b6-5afd-db52-4dfe-4cb9d1516ad9.png)

以下の通りプロジェクト名をCameraXAppに、Minimum SDKをAPI21(CameraXを利用するための最小要件)に変更してFinishをクリック(今回はKotolinを使用して作成します)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287818/521f8772-84a8-b7a4-bd2b-c4d5f3a90cf1.png)

# コーディング

以下の通りbuild.gradleファイルを編集してカメラ機能を提供しているライブラリをインポートする

```
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "com.example.cameraxapp"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.cameraxapp"
        minSdk = 21
        targetSdk = 33
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    buildFeatures {
        viewBinding = true
        dataBinding = true
    }
}

dependencies {
    implementation ("androidx.camera:camera-core:1.1.0-beta01")
    implementation ("androidx.camera:camera-camera2:1.1.0-beta01")
    implementation ("androidx.camera:camera-lifecycle:1.1.0-beta01")
    implementation ("androidx.camera:camera-video:1.1.0-beta01")
    implementation ("androidx.camera:camera-view:1.1.0-beta01" )
    implementation ("androidx.camera:camera-extensions:1.1.0-beta01")
    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.10.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
```

以下の通りres/layout/activity_main.xmlのactivity_mainを編集してUIを作成する

```
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:id="@+id/image_capture_button"
        android:layout_width="110dp"
        android:layout_height="110dp"
        android:layout_marginBottom="50dp"
        android:layout_marginEnd="50dp"
        android:elevation="2dp"
        android:text="@string/take_photo"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintEnd_toStartOf="@id/vertical_centerline" />

    <Button
        android:id="@+id/video_capture_button"
        android:layout_width="110dp"
        android:layout_height="110dp"
        android:layout_marginBottom="50dp"
        android:layout_marginStart="50dp"
        android:elevation="2dp"
        android:text="@string/start_capture"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/vertical_centerline" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/vertical_centerline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent=".50" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

以下の通りres/values/strings.xmlを編集してアプリ名・ボタン名を追加する

```
<resources>
    <string name="app_name">CameraXApp</string>
    <string name="take_photo">Take Photo</string>
    <string name="start_capture">Start Capture</string>
    <string name="stop_capture">Stop Capture</string>
</resources>
```

以下の通りAndroidManifest.xmlを編集して権限のリクエストを行う
```
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-feature android:name="android.hardware.camera.any" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.CameraXApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```
以下の通りMainActivity.ktを編集してアプリの本体を作成する
```
package com.example.cameraxapp

import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ImageCapture
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.cameraxapp.databinding.ActivityMainBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import android.widget.Toast
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.core.Preview
import androidx.camera.core.CameraSelector
import android.util.Log
import androidx.camera.core.ImageCaptureException
import java.text.SimpleDateFormat
import java.util.Locale

typealias LumaListener = (luma: Double) -> Unit

class MainActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding

    private var imageCapture: ImageCapture? = null

    private var videoCapture: VideoCapture<Recorder>? = null
    private var recording: Recording? = null

    private lateinit var cameraExecutor: ExecutorService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }

        // Set up the listeners for take photo and video capture buttons
        viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
        viewBinding.videoCaptureButton.setOnClickListener { captureVideo() }

        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(
                    this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT
                ).show()
                finish()
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

    private fun takePhoto() {
        // Get a stable reference of the modifiable image capture use case
        val imageCapture = imageCapture ?: return

        // Create time stamped name and MediaStore entry.
        val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
            .format(System.currentTimeMillis())
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
                put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
            }
        }

        // Create output options object which contains file + metadata
        val outputOptions = ImageCapture.OutputFileOptions
            .Builder(contentResolver,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues)
            .build()

        // Set up image capture listener, which is triggered after photo has
        // been taken
        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
                }

                override fun
                        onImageSaved(output: ImageCapture.OutputFileResults){
                    val msg = "Photo capture succeeded: ${output.savedUri}"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d(TAG, msg)
                }
            }
        )
    }

    private fun captureVideo() {}

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            // Preview
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
                }

            imageCapture = ImageCapture.Builder()
                .build()

            // Select back camera as a default
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture)

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }


    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    companion object {
        private const val TAG = "CameraXApp"
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS =
            mutableListOf (
                Manifest.permission.CAMERA,
                Manifest.permission.RECORD_AUDIO
            ).apply {
                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                    add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                }
            }.toTypedArray()
    }
}
```

以上デコーディングは終了です

# ビルド設定&実行

エミュレータ上でカメラを起動する設定を行います

上部メニュー -> Tools -> Device Managerをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287818/9ce81eed-c684-b4e1-b5ec-a1edf3f89b54.png)

Create Deviceをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287818/ba0784c7-60b0-d38a-d75a-a1edb55c0fa3.png)

Pixel5を選択 -> Nextをクリック（ご自身の環境に合わせて好きなデバイスを選択してください）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287818/e4be43f2-9fb1-6cd9-47a6-976f1283fc58.png)

Select a system imageでSをダウンロード（もしかしたらこの設定は必要ないかもしれません）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287818/f8b81385-077f-37ba-2355-d1bbec98216f.png)

ダウンロードが完了したらNextをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287818/8451f9c4-0618-dbe7-5464-6049b493ee74.png)


Show Advanced Settingsをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287818/8ddfc04e-e22d-fa4b-3311-e28510c86db0.png)

CameraのFrontとBackでWebcam0を選択して、右下のFinishをクリック（エミュレータ上でWebカメを使う設定）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287818/5f1c42dd-47d4-76d9-4028-5a495b496f89.png)

アプリを実行するとエミュレータ上でカメラが起動します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287818/b4386346-df2e-0200-afe9-d298b1a9cff8.png)

Take Photoを押すとGoogle Photoに写真が保存されるようになっているのですが、現状うまく処理が実行できていません。調べてみたところエミュレータ上で写真を保存しようとするとエラーがでるようです。実機ではうまくいくようなので、実機が手に入り次第確認してみようと思います。

# 参考文献
- [CameraXの概要](https://developer.android.com/training/camerax?hl=ja)
- [Android EmulatorでPCカメラを使用する方法](https://note.com/danchi_kun/n/n8594179e5b00)

# 今後の展望
以下のサイトを参考に、QRコードやJANコードの読み取りにも挑戦予定。
- [AndroidでQRコードを読み取る](https://qiita.com/cozyk100/items/8e4cc408382c6a7ec71f)