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

# はじめに
[前回](https://qiita.com/beyondseeker/items/72fd45dbcde7ad17faad)は、端末やカメラアプリから画像を取得して表示する簡単なアプリを作成しました。

しかし、カメラアプリで新規に写真撮影した場合の解像度が低いという不満がありました。

そこで今回は、カメラアプリに新規撮影を指示してフルサイズの画像を受け取る方法を模索してみます。

# フルサイズの画像取得方法

[前回の考察](https://qiita.com/beyondseeker/items/72fd45dbcde7ad17faad#%E8%80%83%E5%AF%9F)にも書いた通り、今回は [MediaStore#ACTION_IMAGE_CAPTURE](https://developer.android.com/reference/android/provider/MediaStore#ACTION_IMAGE_CAPTURE) に [EXTRA_OUTPUT](https://developer.android.com/reference/android/provider/MediaStore#EXTRA_OUTPUT) を指定する方法を用いることにします。

# 注意事項
Android 7.0 (API level 24) 以降は [EXTRA_OUTPUT](https://developer.android.com/reference/android/provider/MediaStore#EXTRA_OUTPUT) に渡す Uri の作成に Uri.fromFile(File) が使えないので、FileProvider を使用します。

> For more recent apps targeting Android 7.0 (API level 24) and higher, passing a file:// URI across a package boundary causes a FileUriExposedException. Therefore, we now present a more generic way of storing images using a FileProvider.

https://developer.android.com/training/camera/photobasics より引用

# 機能仕様
[前回](https://qiita.com/beyondseeker/items/72fd45dbcde7ad17faad#%E6%A9%9F%E8%83%BD%E4%BB%95%E6%A7%98)とほぼ同様です。

# 画面仕様
ボタンを1つだけにしました。※前回は[こちら](https://qiita.com/beyondseeker/items/72fd45dbcde7ad17faad#%E7%94%BB%E9%9D%A2%E4%BB%95%E6%A7%98)
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/9db5f2b6-12ba-719a-4d74-533cbe0c9f6d.png">

- GET IMAGE ボタン
    - 端末から画像コンテンツを取得するアプリもしくはカメラアプリを選択する chooser を起動する。
    - カメラが使えない場合はカメラアプリは chooser から除外される。

# 実装
## ◆ AndroidManifest.xml
追加部分のみ抜粋

```xml:AndroidManifest.xml
<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application ...>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.objectfanatics.ex_camera.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_path" />
        </provider>
    </application>
</manifest>
```

## ◆ file_path.xml
```xml:res/xml/file_path.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="/" />
</paths>
```

## ◆ activity_main.xml
<img width="120px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/9db5f2b6-12ba-719a-4d74-533cbe0c9f6d.png">

```kotlin: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">
    <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/get_image_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/get_image_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="get image"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
```

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

import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val launcher = registerForActivityResult(PickImageOrTakePicture(this, "画像を取得"), this::onImagePick)
        findViewById<View>(R.id.get_image_button).setOnClickListener { launcher.launch(null) }
    }

    private fun onImagePick(uri: Uri?) {
        findViewById<ImageView>(R.id.image_view).setImageURI(uri ?: return)
    }
}
```

## ◆ ActivityResultContract の独自実装
```kotlin:PickImageOrTakePicture
package com.objectfanatics.ex_camera

import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_PICK
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.content.FileProvider
import java.io.File
import java.io.IOException

class PickImageOrTakePicture(private val context: Context, private val chooserTitle: String) : ActivityResultContract<Void?, Uri?>() {
    private var imageUri: Uri? = null

    override fun createIntent(context: Context, input: Void?): Intent =
        Intent.createChooser(pickerIntent, chooserTitle).apply {
            val uri = context.createImageUri().apply { imageUri = this }
            if (context.hasCameraFeature && uri != null) {
                putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(takePictureIntent(uri)))
            }
        }

    override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
        val data = intent?.data
        return when {
            resultCode != RESULT_OK -> null
            data == null            -> imageUri
            else                    -> intent.data
        }
    }

    private fun Context.createImageUri(): Uri? {
        return try {
            val dir: File = getExternalFilesDir(Environment.DIRECTORY_PICTURES) ?: return null
            val file = File.createTempFile("IMG", ".jpg", dir)
            FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.fileprovider", file)
        } catch (e: IOException) {
            null
        }
    }

    companion object {
        private val Context.hasCameraFeature: Boolean
            get() = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)

        private val pickerIntent = Intent().apply {
            action = ACTION_PICK
            type = "image/*"
        }

        private fun takePictureIntent(input: Uri): Intent =
            Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, input) }
    }
}
```

# 実行
## ◆ 初期状態
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/9db5f2b6-12ba-719a-4d74-533cbe0c9f6d.png">

## ◆ Chooser 起動
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/abe7d28c-d097-fdfe-d952-363c4ee3ea29.png">

## ◆ カメラアプリ起動
シャッターを切る前
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/15fac78a-473e-dfd6-8fbd-13a0467894a4.png">

## ◆ 撮影直後
シャッターを切った直後
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/ee7e2038-fbe7-4cbb-1300-39d0cd1309ac.png">

## ◆ Gallery アプリ
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/40e108db-c558-3dcc-ccd5-c5dafcc710aa.png">

## ◆ 画像選択後
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/e26f1829-1a26-98a6-9db0-2bf56ec083a0.png">

# 今回やったこと
フルサイズの写真をカメラアプリから取得しました。

# おわりに
それでは、また。