1 /*
<lambda>null2  * Copyright 2021 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.extensions.validation
18 
19 import android.annotation.SuppressLint
20 import android.content.Intent
21 import android.content.res.Configuration
22 import android.os.Bundle
23 import android.util.Log
24 import android.view.GestureDetector
25 import android.view.GestureDetector.SimpleOnGestureListener
26 import android.view.MotionEvent
27 import android.view.ScaleGestureDetector
28 import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
29 import android.widget.Button
30 import android.widget.ImageButton
31 import android.widget.Toast
32 import androidx.annotation.OptIn
33 import androidx.appcompat.app.AppCompatActivity
34 import androidx.camera.core.Camera
35 import androidx.camera.core.CameraControl
36 import androidx.camera.core.CameraInfo
37 import androidx.camera.core.DisplayOrientedMeteringPointFactory
38 import androidx.camera.core.ExperimentalGetImage
39 import androidx.camera.core.FocusMeteringAction
40 import androidx.camera.core.FocusMeteringResult
41 import androidx.camera.core.ImageCapture
42 import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
43 import androidx.camera.core.ImageCapture.FLASH_MODE_OFF
44 import androidx.camera.core.ImageCapture.FLASH_MODE_ON
45 import androidx.camera.core.ImageCaptureException
46 import androidx.camera.core.ImageProxy
47 import androidx.camera.core.MeteringPointFactory
48 import androidx.camera.core.Preview
49 import androidx.camera.core.impl.utils.executor.CameraXExecutors
50 import androidx.camera.extensions.ExtensionMode
51 import androidx.camera.extensions.ExtensionsManager
52 import androidx.camera.integration.extensions.INVALID_EXTENSION_MODE
53 import androidx.camera.integration.extensions.INVALID_LENS_FACING
54 import androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_CAMERA_ID
55 import androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_ERROR_CODE
56 import androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_EXTENSION_MODE
57 import androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES
58 import androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_IMAGE_URI
59 import androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_LENS_FACING
60 import androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_REQUEST_CODE
61 import androidx.camera.integration.extensions.R
62 import androidx.camera.integration.extensions.ValidationErrorCode.ERROR_CODE_BIND_TO_LIFECYCLE_FAILED
63 import androidx.camera.integration.extensions.ValidationErrorCode.ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT
64 import androidx.camera.integration.extensions.ValidationErrorCode.ERROR_CODE_NONE
65 import androidx.camera.integration.extensions.ValidationErrorCode.ERROR_CODE_SAVE_IMAGE_FAILED
66 import androidx.camera.integration.extensions.ValidationErrorCode.ERROR_CODE_TAKE_PICTURE_FAILED
67 import androidx.camera.integration.extensions.utils.CameraSelectorUtil.createCameraSelectorById
68 import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
69 import androidx.camera.integration.extensions.utils.FileUtil
70 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromInt
71 import androidx.camera.lifecycle.ProcessCameraProvider
72 import androidx.camera.view.PreviewView
73 import androidx.concurrent.futures.await
74 import androidx.core.content.ContextCompat
75 import androidx.core.math.MathUtils
76 import androidx.lifecycle.lifecycleScope
77 import com.google.common.util.concurrent.FutureCallback
78 import com.google.common.util.concurrent.Futures
79 import com.google.common.util.concurrent.ListenableFuture
80 import kotlinx.coroutines.launch
81 
82 private const val TAG = "ImageCaptureActivity"
83 
84 /**
85  * Activity to capture a image in CameraX extension modes.
86  *
87  * This activity will return the saved image URI to the caller activity.
88  */
89 class ImageCaptureActivity : AppCompatActivity() {
90 
91     private var extensionMode = INVALID_EXTENSION_MODE
92     private var extensionEnabled = true
93     private val result = Intent()
94     private var lensFacing = INVALID_LENS_FACING
95     private lateinit var cameraProvider: ProcessCameraProvider
96     private lateinit var extensionsManager: ExtensionsManager
97     private lateinit var cameraId: String
98     private lateinit var viewFinder: PreviewView
99     private lateinit var imageCapture: ImageCapture
100     private lateinit var camera: Camera
101     private var flashMode = FLASH_MODE_OFF
102     private var evToast: Toast? = null
103 
104     private val evFutureCallback: FutureCallback<Int?> =
105         object : FutureCallback<Int?> {
106             override fun onSuccess(result: Int?) {
107                 val ev =
108                     result!! * camera.cameraInfo.exposureState.exposureCompensationStep.toFloat()
109                 Log.d(TAG, "success new EV: $ev")
110                 showEVToast(String.format("EV: %.2f", ev))
111             }
112 
113             override fun onFailure(t: Throwable) {
114                 Log.d(TAG, "failed $t")
115             }
116         }
117 
118     override fun onCreate(savedInstanceState: Bundle?) {
119         super.onCreate(savedInstanceState)
120         setContentView(R.layout.image_capture_activity)
121 
122         cameraId = intent?.getStringExtra(INTENT_EXTRA_KEY_CAMERA_ID)!!
123         lensFacing = intent.getIntExtra(INTENT_EXTRA_KEY_LENS_FACING, INVALID_LENS_FACING)
124         extensionMode = intent.getIntExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, INVALID_EXTENSION_MODE)
125 
126         result.putExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, extensionMode)
127         result.putExtra(INTENT_EXTRA_KEY_ERROR_CODE, ERROR_CODE_NONE)
128         val requestCode = intent.getIntExtra(INTENT_EXTRA_KEY_REQUEST_CODE, -1)
129         setResult(requestCode, result)
130 
131         supportActionBar?.title = resources.getString(R.string.camerax_extensions_validator)
132         supportActionBar!!.subtitle =
133             "Camera $cameraId [${getLensFacingStringFromInt(lensFacing)}]" +
134                 "[${getExtensionModeStringFromId(extensionMode)}]"
135 
136         viewFinder = findViewById(R.id.view_finder)
137 
138         lifecycleScope.launch {
139             initialize()
140             bindUseCases()
141             setupUiControls()
142         }
143     }
144 
145     @Suppress("DEPRECATION")
146     override fun onConfigurationChanged(newConfig: Configuration) {
147         super.onConfigurationChanged(newConfig)
148 
149         lifecycleScope.launch { bindUseCases() }
150     }
151 
152     override fun onDestroy() {
153         cameraProvider.unbindAll()
154         super.onDestroy()
155     }
156 
157     private suspend fun initialize() {
158         cameraProvider = ProcessCameraProvider.getInstance(this).await()
159         extensionsManager = ExtensionsManager.getInstanceAsync(this, cameraProvider).await()
160     }
161 
162     @SuppressLint("WrongConstant")
163     private fun bindUseCases() {
164         val cameraSelectorById = createCameraSelectorById(cameraId)
165 
166         if (!extensionsManager.isExtensionAvailable(cameraSelectorById, extensionMode)) {
167             result.putExtra(INTENT_EXTRA_KEY_ERROR_CODE, ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT)
168             finish()
169             return
170         }
171 
172         val extensionCameraSelector =
173             extensionsManager.getExtensionEnabledCameraSelector(cameraSelectorById, extensionMode)
174 
175         imageCapture = ImageCapture.Builder().setFlashMode(flashMode).build()
176         val preview = Preview.Builder().build()
177         preview.setSurfaceProvider(viewFinder.surfaceProvider)
178 
179         try {
180             cameraProvider.unbindAll()
181             camera =
182                 cameraProvider.bindToLifecycle(
183                     this,
184                     if (extensionEnabled) extensionCameraSelector else cameraSelectorById,
185                     imageCapture,
186                     preview
187                 )
188 
189             Log.d(TAG, "Extension mode is $extensionMode (enabled: $extensionEnabled)")
190         } catch (e: IllegalArgumentException) {
191             result.putExtra(INTENT_EXTRA_KEY_ERROR_CODE, ERROR_CODE_BIND_TO_LIFECYCLE_FAILED)
192             Log.e(
193                 TAG,
194                 "Failed to bind use cases with ${getExtensionModeStringFromId(extensionMode)}"
195             )
196             finish()
197             return
198         }
199     }
200 
201     @OptIn(markerClass = [ExperimentalGetImage::class])
202     private fun setupUiControls() {
203         // Sets up the flash toggle button
204         setUpFlashButton()
205 
206         // Sets up the EV +/- buttons
207         setUpEvButtons()
208 
209         // Sets up the tap-to-focus functions and zoom in/out by GestureDetector
210         setupViewFinderGestureControls()
211 
212         // Sets up the extension mode enabled/disabled toggle button
213         setUpExtensionToggleButton()
214 
215         // Sets up the capture button
216         val captureButton: ImageButton = findViewById(R.id.camera_capture_button)
217 
218         captureButton.setOnClickListener {
219             captureButton.isEnabled = false
220 
221             imageCapture.takePicture(
222                 ContextCompat.getMainExecutor(this),
223                 object : ImageCapture.OnImageCapturedCallback() {
224                     override fun onCaptureSuccess(image: ImageProxy) {
225                         val filenamePrefix =
226                             "[CameraXExtension][Camera-$cameraId][${
227                             getLensFacingStringFromInt(lensFacing)
228                         }][${getExtensionModeStringFromId(extensionMode)}]"
229                         val filename =
230                             if (extensionEnabled) {
231                                 "$filenamePrefix[Enabled]"
232                             } else {
233                                 "$filenamePrefix[Disabled]"
234                             }
235 
236                         val uri =
237                             FileUtil.saveImageToTempFile(image.image!!, filename, "", cacheDir)
238 
239                         if (uri == null) {
240                             result.putExtra(
241                                 INTENT_EXTRA_KEY_ERROR_CODE,
242                                 ERROR_CODE_SAVE_IMAGE_FAILED
243                             )
244                         } else {
245                             result.putExtra(INTENT_EXTRA_KEY_IMAGE_URI, uri)
246                             result.putExtra(
247                                 INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES,
248                                 image.imageInfo.rotationDegrees
249                             )
250                         }
251                         finish()
252                     }
253 
254                     override fun onError(exception: ImageCaptureException) {
255                         result.putExtra(INTENT_EXTRA_KEY_ERROR_CODE, ERROR_CODE_TAKE_PICTURE_FAILED)
256                         finish()
257                     }
258                 }
259             )
260         }
261     }
262 
263     private fun setUpFlashButton() {
264         setFlashButtonResource()
265 
266         val flashToggleButton: ImageButton = findViewById(R.id.flash_toggle)
267 
268         flashToggleButton.setOnClickListener {
269             flashMode =
270                 when (flashMode) {
271                     FLASH_MODE_ON -> FLASH_MODE_OFF
272                     FLASH_MODE_OFF -> FLASH_MODE_AUTO
273                     FLASH_MODE_AUTO -> FLASH_MODE_ON
274                     else -> throw IllegalArgumentException("Invalid flash mode!")
275                 }
276 
277             imageCapture.flashMode = flashMode
278             setFlashButtonResource()
279         }
280     }
281 
282     private fun setFlashButtonResource() {
283         val flashToggleButton: ImageButton = findViewById(R.id.flash_toggle)
284 
285         flashToggleButton.setImageResource(
286             when (flashMode) {
287                 FLASH_MODE_ON -> R.drawable.ic_flash_on
288                 FLASH_MODE_OFF -> R.drawable.ic_flash_off
289                 FLASH_MODE_AUTO -> R.drawable.ic_flash_auto
290                 else -> throw IllegalArgumentException("Invalid flash mode!")
291             }
292         )
293     }
294 
295     private fun setUpEvButtons() {
296         val plusEvButton: Button = findViewById(R.id.plus_ev_button)
297         plusEvButton.setOnClickListener { plusEv() }
298 
299         val decEvButton: Button = findViewById(R.id.dec_ev_button)
300         decEvButton.setOnClickListener { decEv() }
301     }
302 
303     private fun plusEv() {
304         val range = camera.cameraInfo.exposureState.exposureCompensationRange
305         val ec = camera.cameraInfo.exposureState.exposureCompensationIndex
306 
307         if (range.contains(ec + 1)) {
308             val future: ListenableFuture<Int> =
309                 camera.cameraControl.setExposureCompensationIndex(ec + 1)
310             Futures.addCallback(future, evFutureCallback, CameraXExecutors.mainThreadExecutor())
311         } else {
312             showEVToast(
313                 String.format(
314                     "EV: %.2f",
315                     range.upper * camera.cameraInfo.exposureState.exposureCompensationStep.toFloat()
316                 )
317             )
318         }
319     }
320 
321     private fun decEv() {
322         val range = camera.cameraInfo.exposureState.exposureCompensationRange
323         val ec = camera.cameraInfo.exposureState.exposureCompensationIndex
324 
325         if (range.contains(ec - 1)) {
326             val future: ListenableFuture<Int> =
327                 camera.cameraControl.setExposureCompensationIndex(ec - 1)
328             Futures.addCallback(future, evFutureCallback, CameraXExecutors.mainThreadExecutor())
329         } else {
330             showEVToast(
331                 String.format(
332                     "EV: %.2f",
333                     range.lower * camera.cameraInfo.exposureState.exposureCompensationStep.toFloat()
334                 )
335             )
336         }
337     }
338 
339     internal fun showEVToast(message: String?) {
340         evToast?.cancel()
341         evToast = Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT)
342         evToast?.show()
343     }
344 
345     private fun setupViewFinderGestureControls() {
346         val onTapGestureListener: GestureDetector.OnGestureListener =
347             object : SimpleOnGestureListener() {
348                 override fun onSingleTapUp(e: MotionEvent): Boolean {
349                     val factory: MeteringPointFactory =
350                         DisplayOrientedMeteringPointFactory(
351                             viewFinder.getDisplay(),
352                             camera.getCameraInfo(),
353                             viewFinder.getWidth().toFloat(),
354                             viewFinder.getHeight().toFloat()
355                         )
356                     val action = FocusMeteringAction.Builder(factory.createPoint(e.x, e.y)).build()
357                     Futures.addCallback(
358                         camera.getCameraControl().startFocusAndMetering(action),
359                         object : FutureCallback<FocusMeteringResult?> {
360                             override fun onSuccess(result: FocusMeteringResult?) {
361                                 Log.d(TAG, "Focus and metering succeeded.")
362                             }
363 
364                             override fun onFailure(t: Throwable) {
365                                 Log.e(TAG, "Focus and metering failed.", t)
366                             }
367                         },
368                         CameraXExecutors.mainThreadExecutor()
369                     )
370                     return true
371                 }
372             }
373 
374         val scaleGestureListener: SimpleOnScaleGestureListener =
375             object : SimpleOnScaleGestureListener() {
376                 override fun onScale(detector: ScaleGestureDetector): Boolean {
377                     val cameraInfo: CameraInfo = camera.getCameraInfo()
378                     val newZoom = (cameraInfo.zoomState.value!!.zoomRatio * detector.scaleFactor)
379                     setZoomRatio(newZoom)
380                     return true
381                 }
382             }
383 
384         val tapGestureDetector = GestureDetector(this, onTapGestureListener)
385         val scaleDetector = ScaleGestureDetector(this, scaleGestureListener)
386         viewFinder.setOnTouchListener { _, e: MotionEvent ->
387             val tapEventProcessed = tapGestureDetector.onTouchEvent(e)
388             val scaleEventProcessed = scaleDetector.onTouchEvent(e)
389             tapEventProcessed || scaleEventProcessed
390         }
391     }
392 
393     internal fun setZoomRatio(newZoom: Float) {
394         val cameraInfo: CameraInfo = camera.getCameraInfo()
395         val cameraControl: CameraControl = camera.getCameraControl()
396         val clampedNewZoom =
397             MathUtils.clamp(
398                 newZoom,
399                 cameraInfo.zoomState.value!!.minZoomRatio,
400                 cameraInfo.zoomState.value!!.maxZoomRatio
401             )
402         Log.d(TAG, "setZoomRatio ratio: $clampedNewZoom")
403         val listenableFuture = cameraControl.setZoomRatio(clampedNewZoom)
404         Futures.addCallback(
405             listenableFuture,
406             object : FutureCallback<Void?> {
407                 override fun onSuccess(result: Void?) {
408                     Log.d(TAG, "setZoomRatio onSuccess: $clampedNewZoom")
409                 }
410 
411                 override fun onFailure(t: Throwable) {
412                     Log.d(TAG, "setZoomRatio failed, $t")
413                 }
414             },
415             ContextCompat.getMainExecutor(this)
416         )
417     }
418 
419     private fun setUpExtensionToggleButton() {
420         val extensionToggleButton: ImageButton = findViewById(R.id.extension_toggle)
421         setExtensionToggleButtonResource()
422 
423         extensionToggleButton.setOnClickListener {
424             extensionEnabled = !extensionEnabled
425             setExtensionToggleButtonResource()
426             bindUseCases()
427             setupUiControls()
428 
429             if (extensionEnabled) {
430                 Toast.makeText(this, "Effect is enabled!", Toast.LENGTH_SHORT).show()
431             } else {
432 
433                 Toast.makeText(this, "Effect is disabled!", Toast.LENGTH_SHORT).show()
434             }
435         }
436     }
437 
438     private fun setExtensionToggleButtonResource() {
439         val extensionToggleButton: ImageButton = findViewById(R.id.extension_toggle)
440 
441         if (!extensionEnabled) {
442             extensionToggleButton.setImageResource(R.drawable.outline_block)
443             return
444         }
445 
446         val resourceId =
447             when (extensionMode) {
448                 ExtensionMode.HDR -> R.drawable.outline_hdr_on
449                 ExtensionMode.BOKEH -> R.drawable.outline_portrait
450                 ExtensionMode.NIGHT -> R.drawable.outline_bedtime
451                 ExtensionMode.FACE_RETOUCH -> R.drawable.outline_face_retouching_natural
452                 ExtensionMode.AUTO -> R.drawable.outline_auto_awesome
453                 else -> throw IllegalArgumentException("Invalid extension mode!")
454             }
455 
456         extensionToggleButton.setImageResource(resourceId)
457     }
458 }
459