meili-epfl/Meili

View on GitHub
app/src/main/java/com/github/epfl/meili/photo/CameraActivity.kt

Summary

Maintainability
A
0 mins
Test Coverage
C
72%
package com.github.epfl.meili.photo

import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_FORWARD_RESULT
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.github.epfl.meili.R
import java.io.File
import java.text.SimpleDateFormat
import java.util.*

class CameraActivity : AppCompatActivity() {
    companion object {
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        private const val PRESS_DELAY = 200L
        private const val TAG = "CameraActivity"
        const val URI_KEY = "URI_KEY"
        const val EXTENSION = ".jpg"
        const val EDIT_PHOTO = "EDIT_PHOTO"
    }

    private var imageCapture: ImageCapture? = null // is null when camera hasn't started
    private lateinit var cameraProvider: ProcessCameraProvider
    private lateinit var preview: Preview
    private lateinit var camera: Camera

    private lateinit var outputDirectory: File // directory where photos get saved

    private lateinit var cameraButton: ImageButton
    private lateinit var switchCameraButton: ImageButton
    private lateinit var previewView: PreviewView

    private var lensFacing: Int =
        CameraSelector.LENS_FACING_BACK // which direction is the camera facing

    private var editPhoto = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportActionBar?.hide()
        setContentView(R.layout.activity_camera)

        outputDirectory = applicationContext.filesDir

        cameraButton = findViewById(R.id.camera_capture_button)
        switchCameraButton = findViewById(R.id.camera_switch_button)
        previewView = findViewById(R.id.camera_preview)

        previewView.post {
            initializeUiControls()
            startCameraIfPermitted()
        }
        previewView.setOnTouchListener(getPreviewTouchListener())

        makePhotosHaveOrientation()

        editPhoto = intent.getBooleanExtra(EDIT_PHOTO, true)
    }

    private fun getImageSavedCallback(photoFile: File): ImageCapture.OnImageSavedCallback =
        object : ImageCapture.OnImageSavedCallback {
            override fun onError(exc: ImageCaptureException) {
                Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
            }

            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                if (editPhoto) {
                    val intent =
                        Intent(applicationContext, PhotoCropActivity::class.java)
                    intent.flags = intent.flags or FLAG_ACTIVITY_FORWARD_RESULT
                    intent.putExtra(URI_KEY, Uri.fromFile(photoFile))
                    startActivity(intent)
                } else {
                    val intent = Intent()
                    intent.data = Uri.fromFile(photoFile)
                    setResult(RESULT_OK, intent)
                    finish()
                }
            }
        }

    private fun initializeUiControls() {
        cameraButton.setOnClickListener {
            imageCapture?.let { imageCapture ->
                // Create time-stamped output file to hold the image
                val photoFile = getFile()

                // Create output options object which contains file + metadata
                val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

                // Setup image capture listener which is triggered after photo has been taken
                imageCapture.takePicture(
                    outputOptions,
                    ContextCompat.getMainExecutor(this),
                    getImageSavedCallback(photoFile)
                )
            }
        }

        setupSwitchCameraButton()
    }

    private fun getFile() = File(
        outputDirectory,
        SimpleDateFormat(
            FILENAME_FORMAT,
            Locale.US
        ).format(System.currentTimeMillis()) + EXTENSION
    )

    private fun setupSwitchCameraButton() {
        // Setup for button used to switch cameras
        switchCameraButton.let {

            // Disable the button until the camera is set up
            it.isEnabled = false

            // Listener for button used to switch cameras. Only called if the button is enabled
            it.setOnClickListener {
                lensFacing = if (lensFacing == CameraSelector.LENS_FACING_FRONT)
                    CameraSelector.LENS_FACING_BACK
                else
                    CameraSelector.LENS_FACING_FRONT

                // Re-bind use cases to update selected camera
                buildCamera()
            }
        }
    }

    private fun setUpCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            cameraProvider = cameraProviderFuture.get() // Guaranteed to exist

            // Select lensFacing depending on the available cameras
            lensFacing = when {
                hasBackCamera() -> CameraSelector.LENS_FACING_BACK
                hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
                else -> throw IllegalStateException("No cameras available")
            }

            // Decide if camera switching should be enabled
            updateSwitchCameraButton()

            buildCamera()
        }, ContextCompat.getMainExecutor(this))
    }

    private fun buildCamera() {
        imageCapture = ImageCapture.Builder()
            .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
            .build()

        cameraProvider.unbindAll()

        preview = Preview.Builder().build()

        val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()

        try {
            camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
            preview.setSurfaceProvider(previewView.surfaceProvider)
        } catch (exc: Exception) {
            Log.e(TAG, "Cannot setup camera", exc)
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        // Checks if the request code is the same as the one sent to the device
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                setUpCamera() // When authorized, start the camera
            } else {
                finish()
            }
        }
    }

    /**
     * Used to detect if volume down button is pressed to take photo
     */
    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        return when (keyCode) {
            KeyEvent.KEYCODE_VOLUME_DOWN -> { // Take photo when volume down is pressed
                cameraButton.apply {
                    performClick()
                    isPressed = true
                    invalidate()
                    postDelayed({ // Press for a small delay to show that button has been pressed
                        invalidate()
                        isPressed = false
                    }, PRESS_DELAY)
                }
                true
            }
            else -> super.onKeyDown(keyCode, event)
        }
    }

    /**
     * Creates the touch listener for the previewView
     */
    private fun getPreviewTouchListener(): (View, MotionEvent) -> Boolean {
        // Pinch to zoom
        val pinchListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
            override fun onScale(detector: ScaleGestureDetector): Boolean {
                val currentZoomRatio = camera.cameraInfo.zoomState.value?.zoomRatio ?: 0F
                camera.cameraControl.setZoomRatio(currentZoomRatio * detector.scaleFactor)
                return true
            }
        }
        val scaleGestureDetector = ScaleGestureDetector(applicationContext, pinchListener)

        return { view: View, motionEvent: MotionEvent ->
            view.performClick()
            // Touch to focus camera
            scaleGestureDetector.onTouchEvent(motionEvent)
            when (motionEvent.action) {
                MotionEvent.ACTION_DOWN -> true
                MotionEvent.ACTION_UP -> {
                    val factory = previewView.meteringPointFactory
                    val point = factory.createPoint(motionEvent.x, motionEvent.y)
                    val action = FocusMeteringAction.Builder(point).build()
                    camera.cameraControl.startFocusAndMetering(action)
                    true
                }
                else -> false
            }
        }
    }


    /** Only enable button to switch cameras if both the cameras are available */
    private fun updateSwitchCameraButton() {
        try {
            switchCameraButton.isEnabled = hasBackCamera() && hasFrontCamera()
        } catch (exception: CameraInfoUnavailableException) {
            switchCameraButton.isEnabled = false
        }
    }

    private fun hasBackCamera(): Boolean {
        return cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)
    }

    private fun hasFrontCamera(): Boolean {
        return cameraProvider.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)
    }


    /** Checks if device has access to start the camera, if not, ask the user for permission */
    private fun startCameraIfPermitted() {
        if (allPermissionsGranted()) {
            setUpCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
            )
        }
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it
        ) == PackageManager.PERMISSION_GRANTED
    }


    /** Sets up rotation metadata for Image Capture use case */
    private fun makePhotosHaveOrientation() {
        if (imageCapture == null) {
            Log.d(TAG, "Camera is not set up correctly")
            return
        }

        val orientationEventListener = object : OrientationEventListener(this as Context) {
            override fun onOrientationChanged(orientation: Int) {
                // Monitors orientation values to determine the target rotation value
                val rotation: Int = when (orientation) {
                    in 45..134 -> Surface.ROTATION_270
                    in 135..224 -> Surface.ROTATION_180
                    in 225..314 -> Surface.ROTATION_90
                    else -> Surface.ROTATION_0
                }

                imageCapture!!.targetRotation = rotation
            }
        }
        orientationEventListener.enable()
    }
}