Androidでアプリ連携して画像を取得する（その１）

# はじめに
Android で端末やカメラアプリなどから画像を取得する方法を、アプリを作りながら模索していこうと思います。

# 機能仕様
- カメラアプリから新規に撮影した画像を取得することができる。
- 端末から画像コンテンツを取得することができる。
- カメラが使えない場合にはカメラを利用する機能だけを使えないようにする。
- 取得した画像を表示する。

# 画面仕様
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/c1aac247-f88a-419d-2f4b-de6dda544dcf.png">

- GET CONTENT ボタン
    - 端末から画像コンテンツを選択する。
- CAMERA ボタン
    - カメラを立ち上げる。
    - カメラが使えない場合は disabled になる。
- BOTH ボタン
    - 上記2ボタンと同様の機能を持ったアプリを選択する chooser が表示される。
    - カメラが使えない場合は disabled になる。

# 前提

## ◆ fragment は 1.3.0-alpha06 以降
***androidx.fragment:fragment:1.3.0-alpha06*** 以降を利用します。

これより前のバージョンだと registerForActivityResult(後述) を利用した際に `java.lang.IllegalArgumentException: Can only use lower 16 bits for requestCode` というエラーで落ちることがあります。

```groovy
implementation "androidx.activity:activity:1.2.0-alpha06"
implementation "androidx.fragment:fragment:1.3.0-alpha06"
```

## ◆ startActivityForResult() は使わない
`startActivityForResult()` や `onActivityResult()` は利用せず、Activity 1.2.0-alpha02 及び Fragment 1.3.0-alpha02 で導入された `registerForActivityResult()` を利用します。

`registerForActivityResult()` を利用すると、今まで `startActivityForResult()` や `onActivityResult()` を用いていた処理を簡潔に記述することができます。

例えば、端末内部の画像をピックアップする場合は、以下のように記述できます：

```kotlin
// 実機に保存されている content を mimeType 指定で取得するアプリの launcher
val launcher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
    // activity result を受け取った後に実行される処理
    imageView.setImageURI(uri)
}
// launcher を起動する(実機に保存されている content を mimeType 指定で取得するアプリを立ち上げる)
launcher.launch("image/*")
```

# 実装
## ◆ activity_main.xml
<img width="120px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/c1aac247-f88a-419d-2f4b-de6dda544dcf.png">

```xml:activity_main.xml
<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginBottom="16dp">
    <ImageView
        android:id="@+id/image_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:gravity="center"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toTopOf="@id/camera_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/get_content_button"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="get content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/camera_button"
        app:layout_constraintStart_toStartOf="parent" />
    <Button
        android:id="@+id/camera_button"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="camera"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/both_button"
        app:layout_constraintStart_toEndOf="@id/get_content_button" />
    <Button
        android:id="@+id/both_button"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="both"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/camera_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
```

## ◆ MainActivity
```kotlin:MainActivity.kt
package com.objectfanatics.ex_camera

import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    private val imageView: ImageView by lazy { findViewById<ImageView>(R.id.image_view) }
    private val imageCaptureButton: View by lazy { findViewById<View>(R.id.camera_button) }
    private val getContentButton: View by lazy { findViewById<View>(R.id.get_content_button) }
    private val bothButton: View by lazy { findViewById<View>(R.id.both_button) }

    private val hasCameraFeature get() = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        enableNormalFunction()
        when {
            hasCameraFeature -> enableCameraFunction()
            else             -> disableCameraFunction()
        }
    }

    private fun enableNormalFunction() {
        // 実機に保存されている content を mimeType 指定で取得する。
        val getContent = registerForActivityResult(ActivityResultContracts.GetContent(), this::onContent)
        getContentButton.setOnClickListener { getContent.launch("image/*") }

        // GetContent と TakePicturePreview と同等の機能を chooser で選択可能にした版。
        val both = registerForActivityResult(GetContentOrTakePicturePreviewChooser(this, "画像を選択"), this::onPicture)
        bothButton.setOnClickListener { both.launch(null) }
    }

    private fun enableCameraFunction() {
        // 小さな画像をカメラから取得する。実機で試したところ 260*195 の画像だった。
        val takePicturePreview = registerForActivityResult(ActivityResultContracts.TakePicturePreview(), this::onPicture)
        imageCaptureButton.setOnClickListener { takePicturePreview.launch(null) }
    }

    private fun disableCameraFunction() {
        imageCaptureButton.isEnabled = false
    }

    private fun onContent(uri: Uri?) {
        imageView.setImageURI(uri ?: return)
    }

    private fun onPicture(bitmap: Bitmap?) {
        imageView.setImageBitmap(bitmap ?: return)
    }
}
```

## ◆ ActivityResultContract の独自実装
[GetContent](https://developer.android.com/reference/kotlin/androidx/activity/result/contract/ActivityResultContracts.GetContent) と [TakePicturePreview](https://developer.android.com/reference/kotlin/androidx/activity/result/contract/ActivityResultContracts.TakePicturePreview) 双方の対象アプリを chooser で選択させる機能を持つ [ActivityResultContract](https://developer.android.com/reference/kotlin/androidx/activity/result/contract/ActivityResultContract) です。

ただし、カメラが利用不可能な場合はカメラ機能は候補から外すようになっています。

```kotlin:GetContentOrTakePicturePreviewChooser.kt
package com.objectfanatics.ex_camera

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.provider.MediaStore
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts

class GetContentOrTakePicturePreviewChooser(private val context: Context, private val chooserTitle: String) : ActivityResultContract<Void?, Bitmap>() {
    private val getContentIntent get() = ActivityResultContracts.GetContent().createIntent(context, "image/*")
    private val takePicturePreviewIntent get() = ActivityResultContracts.TakePicturePreview().createIntent(context, null)
    private val hasCameraFeature get() = context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)

    override fun createIntent(context: Context, input: Void?): Intent {
        return Intent.createChooser(getContentIntent, chooserTitle).apply {
            if (hasCameraFeature) {
                putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(takePicturePreviewIntent))
            }
        }
    }

    override fun getSynchronousResult(context: Context, input: Void?): SynchronousResult<Bitmap?>? = null

    override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? {
        if (intent == null || resultCode != Activity.RESULT_OK) return null

        val bitmap: Bitmap? = intent.getParcelableExtra<Bitmap>("data")
        val data: Uri? = intent.data

        return when {
            bitmap != null -> bitmap
            data != null   -> MediaStore.Images.Media.getBitmap(context.contentResolver, data)
            else           -> null
        }
    }
}
```

# 実行
## ◆ 初期状態
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/c0e4a980-9602-c373-667b-fa8a2ba4e952.png">

## ◆ コンテンツ選択
***GET CONTENT ボタン押下により起動***
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/44c1942d-2f68-f5e2-823c-8ed6a09a299b.png">

## ◆ カメラアプリ起動
***CAMERA ボタン押下により起動***
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/ec23d1f0-432e-43b3-fdb2-f71c03744e67.png">

## ◆ chooser 起動
***BOTH ボタン押下により起動***
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/f8ed0504-d3f2-ae75-f152-d29d88039ce1.png">

## ◆ 画像選択後
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/20b6a6f9-15cb-c5ca-71a2-da5a0617f9be.png">

# 今回やったこと
- 端末やカメラアプリから画像を取得して表示する簡単なアプリを作成しました。
- [registerForActivityResult()](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultCaller#registerforactivityresult) を使用しました。
- [ActivityResultContract](https://developer.android.com/reference/kotlin/androidx/activity/result/contract/ActivityResultContract) を使用しました。
- [GetContent](https://developer.android.com/reference/kotlin/androidx/activity/result/contract/ActivityResultContracts.GetContent) と [TakePicturePreview](https://developer.android.com/reference/kotlin/androidx/activity/result/contract/ActivityResultContracts.TakePicturePreview) 双方の対象アプリを [Intent#createChooser](https://developer.android.com/reference/kotlin/android/content/Intent#createchooser) で選択させる機能を持つ [ActivityResultContract](https://developer.android.com/reference/kotlin/androidx/activity/result/contract/ActivityResultContract) を作成しました。

# 考察

## ◆ registerForActivityResult について
いや～、めっちゃ便利ですね。早く stable 版が出てほしいところです。

バージョンを ***fragment:1.3.0-alpha06*** に上げる前のバグはちょっとハマりました、、、。

## ◆ カメラアプリ側のバグ対策
手元の実機では動くのに、エミュレータ(Android10 / API level 29)でカメラアプリが落ちるという問題がありました。

動作としては、カメラアプリの起動を試みると、カメラアプリ側が以下のエラーを吐いて落ち、元の画面に戻るという流れになります。

```
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.android.camera2/com.android.camera.CaptureActivity}: java.lang.NullPointerException: Attempt to get length of null array
```

どうやら Android 10 付属のカメラアプリのバグらしく、以下のように何でもいいのでダミーのパーミッションを入れてあげれば回避できるようです[^1]。(※実際に回避できました)

```
<manifest ...
    <!-- https://stackoverflow.com/questions/62296554/camera-intent-permissions-in-android-10#comment110177675_62296554 -->
    <uses-permission android:name="WORKAROUND_FOR_EMULATOR_CAMERA_APP_BUG" />
</manifest>
```

## ◆ TakePicturePreview の解像度
今回は [TakePicturePreview](https://developer.android.com/reference/kotlin/androidx/activity/result/contract/ActivityResultContracts.TakePicturePreview) を利用してカメラで撮影した画像を取得しています。

内部的には [MediaStore.ACTION_IMAGE_CAPTURE](https://developer.android.com/reference/kotlin/android/provider/MediaStore#action_image_capture) を指定しているのですが、画像は非常に粗いです。

> The caller may pass an extra EXTRA_OUTPUT to control where this image will be written. If the EXTRA_OUTPUT is not present, then a small sized image is returned as a Bitmap object in the extra field.

ドキュメントには *EXTRA_OUTPUT* 無しでは small sized image が返ると書かれており、実際に実機で試してみたところ、画像のサイズは 260 * 195 でした。

*EXTRA_OUTPUT* を使えばいい感じの解像度が得られそうな気がしますね。

ということで、[次回](https://qiita.com/beyondseeker/items/430d1132a1528cc51e65)は、写真の解像度向上を模索してみようと思います。

[^1]: https://stackoverflow.com/questions/62296554/camera-intent-permissions-in-android-10#comment110177675_62296554