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