app/src/main/java/com/github/epfl/meili/photo/CameraActivity.kt
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()
}
}