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