1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.camera.integration.view.util
18 
19 import android.app.Dialog
20 import android.content.ContentValues
21 import android.content.Context
22 import android.graphics.Bitmap
23 import android.graphics.BitmapFactory
24 import android.graphics.Canvas
25 import android.graphics.Paint
26 import android.graphics.RectF
27 import android.os.Environment
28 import android.provider.MediaStore
29 import android.util.Size
30 import android.view.View
31 import android.widget.ImageView
32 import android.widget.Toast
33 import androidx.annotation.VisibleForTesting
34 import androidx.camera.core.FlashState
35 import androidx.camera.core.ImageCapture.OnImageCapturedCallback
36 import androidx.camera.core.ImageCapture.OnImageSavedCallback
37 import androidx.camera.core.ImageCapture.OutputFileOptions
38 import androidx.camera.core.ImageCapture.OutputFileResults
39 import androidx.camera.core.ImageCaptureException
40 import androidx.camera.core.ImageProxy
41 import androidx.camera.core.impl.utils.TransformUtils
42 import androidx.camera.core.impl.utils.executor.CameraXExecutors
43 import androidx.camera.integration.view.R
44 import androidx.camera.view.CameraController
45 import java.util.concurrent.Executor
46 
47 fun interface ToastMessenger {
48     fun show(message: String)
49 }
50 
51 /** Take a picture based on the current configuration. */
CameraControllernull52 fun CameraController.takePicture(
53     context: Context,
54     executor: Executor,
55     toastMessenger: ToastMessenger,
56     onDisk: () -> Boolean,
57 ) {
58     try {
59         if (onDisk()) {
60             takePictureOnDisk(
61                 context,
62                 executor,
63                 toastMessenger,
64                 onImageSaved = { results ->
65                     toastMessenger.show("Image saved to: " + results.savedUri)
66                 },
67                 onError = { e -> toastMessenger.show("Failed to save picture: " + e.message) }
68             )
69         } else {
70             takePicture(
71                 executor,
72                 object : OnImageCapturedCallback() {
73                     override fun onCaptureSuccess(image: ImageProxy) {
74                         displayImage(context, image)
75                     }
76 
77                     override fun onError(exception: ImageCaptureException) {
78                         toastMessenger.show(
79                             "Failed to capture in-memory picture: " + exception.message
80                         )
81                     }
82                 }
83             )
84         }
85     } catch (exception: RuntimeException) {
86         toastMessenger.show("Failed to take picture: " + exception.message)
87     }
88 }
89 
90 /** Displays a [ImageProxy] in a pop-up dialog. */
displayImagenull91 private fun displayImage(context: Context, image: ImageProxy) {
92     val rotationDegrees = image.imageInfo.rotationDegrees
93     val cropped: Bitmap = getCroppedBitmap(image)
94     image.close()
95 
96     CameraXExecutors.mainThreadExecutor().execute {
97         val dialog = Dialog(context)
98         dialog.setContentView(R.layout.image_dialog)
99         val imageView = dialog.findViewById<View>(R.id.dialog_image) as ImageView
100         imageView.setImageBitmap(cropped)
101         imageView.rotation = rotationDegrees.toFloat()
102         dialog.findViewById<View>(R.id.dialog_button).setOnClickListener { dialog.dismiss() }
103         dialog.show()
104 
105         val flashState =
106             when (image.imageInfo.flashState) {
107                 FlashState.FIRED -> "FIRED"
108                 FlashState.UNAVAILABLE -> "UNAVAILABLE"
109                 FlashState.NOT_FIRED -> "NOT_FIRED"
110                 else -> "UNKNOWN"
111             }
112         Toast.makeText(context, "Flash state: $flashState", Toast.LENGTH_SHORT).show()
113     }
114 }
115 
116 @VisibleForTesting
CameraControllernull117 fun CameraController.takePictureOnDisk(
118     context: Context,
119     executor: Executor,
120     toastMessenger: ToastMessenger,
121     onImageSaved: (OutputFileResults) -> Unit = {},
<lambda>null122     onError: (ImageCaptureException) -> Unit = {},
123 ) {
124     createDefaultPictureFolderIfNotExist(toastMessenger)
125     val contentValues = ContentValues()
126     contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
127     val outputFileOptions =
128         OutputFileOptions.Builder(
129                 context.contentResolver,
130                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
131                 contentValues
132             )
133             .build()
134     takePicture(
135         outputFileOptions,
136         executor,
137         object : OnImageSavedCallback {
onImageSavednull138             override fun onImageSaved(outputFileResults: OutputFileResults) {
139                 onImageSaved(outputFileResults)
140             }
141 
onErrornull142             override fun onError(exception: ImageCaptureException) {
143                 onError(exception)
144             }
145         }
146     )
147 }
148 
149 /** Converts the [ImageProxy] to [Bitmap] with crop rect applied. */
getCroppedBitmapnull150 private fun getCroppedBitmap(image: ImageProxy): Bitmap {
151     val byteBuffer = image.planes[0].buffer
152     val bytes = ByteArray(byteBuffer.remaining())
153     byteBuffer[bytes]
154     val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
155 
156     val cropRect = image.cropRect
157     val newSize = Size(cropRect.width(), cropRect.height())
158     val cropped = Bitmap.createBitmap(newSize.width, newSize.height, Bitmap.Config.ARGB_8888)
159 
160     val croppingTransform =
161         TransformUtils.getRectToRect(
162             RectF(cropRect),
163             RectF(0f, 0f, cropRect.width().toFloat(), cropRect.height().toFloat()),
164             0
165         )
166 
167     val canvas = Canvas(cropped)
168     canvas.drawBitmap(bitmap, croppingTransform, Paint())
169     canvas.save()
170 
171     bitmap.recycle()
172     return cropped
173 }
174 
createDefaultPictureFolderIfNotExistnull175 private fun createDefaultPictureFolderIfNotExist(toastMessenger: ToastMessenger) {
176     val pictureFolder =
177         Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
178     if (!pictureFolder.exists()) {
179         if (!pictureFolder.mkdir()) {
180             toastMessenger.show("Failed to create directory: $pictureFolder")
181         }
182     }
183 }
184