DroidFS/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt

612 lines
27 KiB
Kotlin
Raw Normal View History

2020-08-05 14:06:54 +02:00
package sushi.hardcore.droidfs
2020-08-12 16:14:26 +02:00
import android.Manifest
2021-10-03 14:36:06 +02:00
import android.annotation.SuppressLint
2020-08-12 16:14:26 +02:00
import android.content.pm.PackageManager
2020-12-19 19:55:54 +01:00
import android.os.Build
2020-08-05 14:06:54 +02:00
import android.os.Bundle
2020-08-09 15:27:31 +02:00
import android.text.InputType
2020-12-19 19:55:54 +01:00
import android.util.Size
2022-04-16 13:58:58 +02:00
import android.view.*
2020-09-04 04:00:19 +02:00
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import android.widget.ImageView
2020-12-19 19:55:54 +01:00
import android.widget.RelativeLayout
2020-08-05 14:06:54 +02:00
import android.widget.Toast
2021-10-03 14:36:06 +02:00
import androidx.annotation.RequiresApi
2023-04-17 15:52:20 +02:00
import androidx.camera.core.AspectRatio
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
2023-09-10 19:12:51 +02:00
import androidx.camera.core.DynamicRange
2023-04-17 15:52:20 +02:00
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
2023-05-08 20:58:54 +02:00
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.extensions.ExtensionMode
import androidx.camera.extensions.ExtensionsManager
2020-12-19 19:55:54 +01:00
import androidx.camera.lifecycle.ProcessCameraProvider
2023-04-17 15:52:20 +02:00
import androidx.camera.video.MuxerOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.SucklessRecorder
import androidx.camera.video.SucklessRecording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
2021-10-03 14:36:06 +02:00
import androidx.core.app.ActivityCompat
2020-08-12 16:14:26 +02:00
import androidx.core.content.ContextCompat
2022-04-20 15:17:33 +02:00
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
2021-06-11 20:23:54 +02:00
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
2022-06-18 21:13:16 +02:00
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
2020-08-05 14:06:54 +02:00
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.video_recording.AsynchronousSeekableWriter
2023-04-17 15:52:20 +02:00
import sushi.hardcore.droidfs.video_recording.FFmpegMuxer
2021-10-03 14:36:06 +02:00
import sushi.hardcore.droidfs.video_recording.SeekableWriter
2021-11-09 11:12:09 +01:00
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
2022-03-24 20:08:23 +01:00
import sushi.hardcore.droidfs.widgets.EditTextDialog
2022-01-18 20:42:13 +01:00
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
2020-08-05 14:06:54 +02:00
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executor
2023-05-08 20:58:54 +02:00
import kotlin.math.pow
import kotlin.math.sqrt
2020-08-05 14:06:54 +02:00
2023-04-17 15:52:20 +02:00
@SuppressLint("RestrictedApi")
2020-09-04 04:00:19 +02:00
class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
2020-08-05 14:06:54 +02:00
companion object {
2021-10-03 14:36:06 +02:00
private const val CAMERA_PERMISSION_REQUEST_CODE = 0
private const val AUDIO_PERMISSION_REQUEST_CODE = 1
2020-08-05 14:06:54 +02:00
private const val fileNameRandomMin = 100000
private const val fileNameRandomMax = 999999
2021-06-07 16:34:50 +02:00
private val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
2020-08-05 14:06:54 +02:00
private val random = Random()
}
2021-06-11 20:23:54 +02:00
2020-08-09 15:27:31 +02:00
private var timerDuration = 0
set(value) {
field = value
if (value > 0){
2021-06-11 20:23:54 +02:00
binding.imageTimer.setImageResource(R.drawable.icon_timer_on)
2020-08-09 15:27:31 +02:00
} else {
2021-06-11 20:23:54 +02:00
binding.imageTimer.setImageResource(R.drawable.icon_timer_off)
2020-08-09 15:27:31 +02:00
}
}
2020-09-04 04:00:19 +02:00
private lateinit var sensorOrientationListener: SensorOrientationListener
private var currentRotation = 0
2020-09-04 04:00:19 +02:00
private var previousOrientation: Float = 0f
private lateinit var orientedIcons: List<ImageView>
2022-06-18 21:13:16 +02:00
private lateinit var encryptedVolume: EncryptedVolume
2020-08-05 14:06:54 +02:00
private lateinit var outputDirectory: String
private var permissionsGranted = false
private lateinit var executor: Executor
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var extensionsManager: ExtensionsManager
2021-10-03 14:36:06 +02:00
private lateinit var cameraSelector: CameraSelector
private val cameraPreview = Preview.Builder().build()
2020-12-19 19:55:54 +01:00
private var imageCapture: ImageCapture? = null
2023-04-17 15:52:20 +02:00
private var videoCapture: VideoCapture<SucklessRecorder>? = null
private var videoRecorder: SucklessRecorder? = null
private var videoRecording: SucklessRecording? = null
private var camera: Camera? = null
private var resolutions: List<Size>? = null
2020-12-19 19:55:54 +01:00
private var currentResolutionIndex: Int = 0
private var currentResolution: Size? = null
2023-04-17 15:52:20 +02:00
private val aspectRatios = arrayOf(AspectRatio.RATIO_16_9, AspectRatio.RATIO_4_3)
private var currentAspectRatioIndex = 0
private var qualities: List<Quality>? = null
private var currentQualityIndex = -1
private var captureMode = ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
2020-12-19 19:55:54 +01:00
private var isBackCamera = true
2021-10-03 14:36:06 +02:00
private var isInVideoMode = false
private var isRecording = false
private var isWaitingForTimer = false
2021-06-11 20:23:54 +02:00
private lateinit var binding: ActivityCameraBinding
2020-08-05 14:06:54 +02:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
2021-06-11 20:23:54 +02:00
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
2022-03-05 12:51:02 +01:00
supportActionBar?.hide()
encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
2020-08-05 14:06:54 +02:00
outputDirectory = intent.getStringExtra("path")!!
2020-12-19 19:55:54 +01:00
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED){
permissionsGranted = true
2020-12-19 19:55:54 +01:00
} else {
requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
}
} else {
permissionsGranted = true
2020-12-19 19:55:54 +01:00
}
executor = ContextCompat.getMainExecutor(this)
cameraPreview.setSurfaceProvider(binding.cameraPreview.surfaceProvider)
ProcessCameraProvider.getInstance(this).apply {
addListener({
cameraProvider = get()
2021-12-20 14:44:58 +01:00
ExtensionsManager.getInstanceAsync(this@CameraActivity, cameraProvider).apply {
addListener({
extensionsManager = get()
setupCamera()
}, executor)
}
}, executor)
}
2020-12-19 19:55:54 +01:00
binding.imageCaptureMode.setOnClickListener {
2023-04-17 15:52:20 +02:00
if (isInVideoMode) {
qualities?.let { qualities ->
val qualityNames = qualities.map {
when (it) {
Quality.UHD -> "UHD"
Quality.FHD -> "FHD"
Quality.HD -> "HD"
Quality.SD -> "SD"
else -> throw IllegalArgumentException("Invalid quality: $it")
}
}.toTypedArray()
CustomAlertDialogBuilder(this, theme)
.setTitle("Choose quality:")
.setSingleChoiceItems(qualityNames, currentQualityIndex) { dialog, which ->
currentQualityIndex = which
rebindUseCases()
dialog.dismiss()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
} else {
2023-04-17 15:52:20 +02:00
CustomAlertDialogBuilder(this, theme)
.setTitle(R.string.camera_optimization)
.setSingleChoiceItems(
arrayOf(getString(R.string.maximize_quality), getString(R.string.minimize_latency)),
if (captureMode == ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) 0 else 1
) { dialog, which ->
val newCaptureMode = if (which == 0) {
ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
} else {
ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
}
if (newCaptureMode != captureMode) {
captureMode = newCaptureMode
setCaptureModeIcon()
rebindUseCases()
}
2023-04-17 15:52:20 +02:00
dialog.dismiss()
}
2023-04-17 15:52:20 +02:00
.setNegativeButton(R.string.cancel, null)
.show()
}
}
2021-06-11 20:23:54 +02:00
binding.imageRatio.setOnClickListener {
2023-04-17 15:52:20 +02:00
if (isInVideoMode) {
2023-02-28 22:50:59 +01:00
CustomAlertDialogBuilder(this, theme)
2023-04-17 15:52:20 +02:00
.setTitle("Aspect ratio:")
.setSingleChoiceItems(arrayOf("16:9", "4:3"), currentAspectRatioIndex) { dialog, which ->
currentAspectRatioIndex = which
rebindUseCases()
dialog.dismiss()
2021-06-07 16:34:50 +02:00
}
.setNegativeButton(R.string.cancel, null)
.show()
2023-04-17 15:52:20 +02:00
} else {
resolutions?.let {
CustomAlertDialogBuilder(this, theme)
.setTitle(R.string.choose_resolution)
.setSingleChoiceItems(it.map { size -> size.toString() }.toTypedArray(), currentResolutionIndex) { dialog, which ->
currentResolution = resolutions!![which]
currentResolutionIndex = which
rebindUseCases()
dialog.dismiss()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
2021-06-07 16:34:50 +02:00
}
}
2021-06-11 20:23:54 +02:00
binding.imageTimer.setOnClickListener {
2022-03-24 20:08:23 +01:00
with (EditTextDialog(this, R.string.enter_timer_duration) {
try {
timerDuration = it.toInt()
} catch (e: NumberFormatException) {
Toast.makeText(this, R.string.invalid_number, Toast.LENGTH_SHORT).show()
2021-06-07 16:34:50 +02:00
}
2022-03-24 20:08:23 +01:00
}) {
binding.dialogEditText.inputType = InputType.TYPE_CLASS_NUMBER
show()
2021-06-07 16:34:50 +02:00
}
}
2021-06-11 20:23:54 +02:00
binding.imageFlash.setOnClickListener {
binding.imageFlash.setImageResource(if (isInVideoMode) {
when (imageCapture?.flashMode) {
ImageCapture.FLASH_MODE_ON -> {
camera?.cameraControl?.enableTorch(false)
imageCapture?.flashMode = ImageCapture.FLASH_MODE_OFF
R.drawable.icon_flash_off
}
else -> {
camera?.cameraControl?.enableTorch(true)
imageCapture?.flashMode = ImageCapture.FLASH_MODE_ON
R.drawable.icon_flash_on
}
2021-06-07 16:34:50 +02:00
}
} else {
when (imageCapture?.flashMode) {
ImageCapture.FLASH_MODE_AUTO -> {
imageCapture?.flashMode = ImageCapture.FLASH_MODE_ON
R.drawable.icon_flash_on
}
ImageCapture.FLASH_MODE_ON -> {
imageCapture?.flashMode = ImageCapture.FLASH_MODE_OFF
R.drawable.icon_flash_off
}
else -> {
imageCapture?.flashMode = ImageCapture.FLASH_MODE_AUTO
R.drawable.icon_flash_auto
}
2021-06-07 16:34:50 +02:00
}
})
}
2021-10-03 14:36:06 +02:00
binding.imageModeSwitch.setOnClickListener {
isInVideoMode = !isInVideoMode
2023-04-17 15:52:20 +02:00
rebindUseCases()
binding.imageFlash.setImageResource(if (isInVideoMode) {
2021-10-03 14:36:06 +02:00
binding.recordVideoButton.visibility = View.VISIBLE
binding.takePhotoButton.visibility = View.GONE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), AUDIO_PERMISSION_REQUEST_CODE)
}
}
2022-05-01 19:48:16 +02:00
binding.imageModeSwitch.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.icon_photo)?.mutate()?.also {
it.setTint(ContextCompat.getColor(this, R.color.neutralIconTint))
})
2023-04-17 15:52:20 +02:00
setCaptureModeIcon()
imageCapture?.flashMode = ImageCapture.FLASH_MODE_OFF
R.drawable.icon_flash_off
2021-10-03 14:36:06 +02:00
} else {
binding.recordVideoButton.visibility = View.GONE
binding.takePhotoButton.visibility = View.VISIBLE
binding.imageModeSwitch.setImageResource(R.drawable.icon_video)
imageCapture?.flashMode = ImageCapture.FLASH_MODE_AUTO
R.drawable.icon_flash_auto
})
2021-10-03 14:36:06 +02:00
}
2021-06-11 20:23:54 +02:00
binding.imageCameraSwitch.setOnClickListener {
2021-06-07 16:34:50 +02:00
isBackCamera = if (isBackCamera) {
binding.imageCameraSwitch.setImageResource(R.drawable.icon_camera_back)
2021-06-07 16:34:50 +02:00
false
} else {
binding.imageCameraSwitch.setImageResource(R.drawable.icon_camera_front)
if (isInVideoMode) {
//reset flash state
imageCapture?.flashMode = ImageCapture.FLASH_MODE_OFF
binding.imageFlash.setImageResource(R.drawable.icon_flash_off)
}
2021-06-07 16:34:50 +02:00
true
}
resolutions = null
2023-04-17 15:52:20 +02:00
qualities = null
2021-06-07 16:34:50 +02:00
setupCamera()
}
2021-06-11 20:23:54 +02:00
binding.takePhotoButton.onClick = ::onClickTakePhoto
2021-10-03 14:36:06 +02:00
binding.recordVideoButton.setOnClickListener { onClickRecordVideo() }
orientedIcons = listOf(binding.imageRatio, binding.imageTimer, binding.imageCaptureMode, binding.imageFlash, binding.imageModeSwitch, binding.imageCameraSwitch)
2020-12-19 19:55:54 +01:00
sensorOrientationListener = SensorOrientationListener(this)
val scaleGestureDetector = ScaleGestureDetector(this, object : ScaleGestureDetector.SimpleOnScaleGestureListener(){
override fun onScale(detector: ScaleGestureDetector): Boolean {
val currentZoomRatio = camera?.cameraInfo?.zoomState?.value?.zoomRatio ?: 0F
camera?.cameraControl?.setZoomRatio(currentZoomRatio*detector.scaleFactor)
2020-12-19 19:55:54 +01:00
return true
}
})
2021-06-11 20:23:54 +02:00
binding.cameraPreview.setOnTouchListener { view, event ->
2021-06-07 16:34:50 +02:00
view.performClick()
when (event.action) {
2020-12-19 19:55:54 +01:00
MotionEvent.ACTION_DOWN -> true
MotionEvent.ACTION_UP -> {
2021-06-11 20:23:54 +02:00
val factory = binding.cameraPreview.meteringPointFactory
2021-06-07 16:34:50 +02:00
val point = factory.createPoint(event.x, event.y)
2020-12-19 19:55:54 +01:00
val action = FocusMeteringAction.Builder(point).build()
camera?.cameraControl?.startFocusAndMetering(action)
2020-12-19 19:55:54 +01:00
true
}
2021-06-07 16:34:50 +02:00
MotionEvent.ACTION_MOVE -> scaleGestureDetector.onTouchEvent(event)
2020-12-19 19:55:54 +01:00
else -> false
}
}
}
2021-10-03 14:36:06 +02:00
@RequiresApi(Build.VERSION_CODES.M)
2020-12-19 19:55:54 +01:00
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
2021-06-07 16:34:50 +02:00
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
2021-10-03 14:36:06 +02:00
if (grantResults.size == 1) {
when (requestCode) {
CAMERA_PERMISSION_REQUEST_CODE -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
permissionsGranted = true
setupCamera()
} else {
2023-02-28 22:50:59 +01:00
CustomAlertDialogBuilder(this, theme)
2020-12-19 19:55:54 +01:00
.setTitle(R.string.error)
.setMessage(R.string.camera_perm_needed)
.setCancelable(false)
2023-03-07 23:25:17 +01:00
.setPositiveButton(R.string.ok) { _, _ -> finish() }.show()
2020-12-19 19:55:54 +01:00
}
2021-10-03 14:36:06 +02:00
AUDIO_PERMISSION_REQUEST_CODE -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (videoCapture != null) {
cameraProvider.unbind(videoCapture)
camera = cameraProvider.bindToLifecycle(this, cameraSelector, videoCapture)
}
}
2020-12-19 19:55:54 +01:00
}
}
}
2023-04-17 15:52:20 +02:00
private fun setCaptureModeIcon() {
binding.imageCaptureMode.setImageResource(if (isInVideoMode) {
R.drawable.icon_high_quality
} else {
if (captureMode == ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) {
R.drawable.icon_speed
} else {
R.drawable.icon_high_quality
}
})
}
2021-06-07 16:34:50 +02:00
private fun adaptPreviewSize(resolution: Size) {
val screenWidth = resources.displayMetrics.widthPixels
val screenHeight = resources.displayMetrics.heightPixels
var height = (resolution.height * (screenWidth.toFloat() / resolution.width)).toInt()
var width = screenWidth
if (height > screenHeight) {
width = (width * (screenHeight.toFloat() / height)).toInt()
height = screenHeight
}
binding.cameraPreview.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
addRule(RelativeLayout.CENTER_IN_PARENT)
2020-12-19 19:55:54 +01:00
}
}
private fun refreshImageCapture() {
imageCapture = ImageCapture.Builder()
.setCaptureMode(captureMode)
.setFlashMode(imageCapture?.flashMode ?: ImageCapture.FLASH_MODE_AUTO)
2023-05-08 20:58:54 +02:00
.setResolutionSelector(ResolutionSelector.Builder().setResolutionFilter { supportedSizes, _ ->
resolutions = supportedSizes.sortedBy {
-it.width*it.height
2021-10-03 14:36:06 +02:00
}
2023-05-08 20:58:54 +02:00
currentResolution?.let { targetResolution ->
return@setResolutionFilter supportedSizes.sortedBy {
sqrt((it.width - targetResolution.width).toDouble().pow(2) + (it.height - targetResolution.height).toDouble().pow(2))
}
}
supportedSizes
}.build())
.setTargetRotation(currentRotation)
.build()
}
2021-10-03 14:36:06 +02:00
private fun refreshVideoCapture() {
2023-04-17 15:52:20 +02:00
val recorderBuilder = SucklessRecorder.Builder()
.setExecutor(executor)
.setAspectRatio(aspectRatios[currentAspectRatioIndex])
if (currentQualityIndex != -1) {
recorderBuilder.setQualitySelector(QualitySelector.from(qualities!![currentQualityIndex]))
}
videoRecorder = recorderBuilder.build()
videoCapture = VideoCapture.withOutput(videoRecorder!!).apply {
targetRotation = currentRotation
}
}
2023-04-17 15:52:20 +02:00
private fun rebindUseCases(): UseCase {
cameraProvider.unbindAll()
val currentUseCase = (if (isInVideoMode) {
refreshVideoCapture()
camera = cameraProvider.bindToLifecycle(this, cameraSelector, cameraPreview, videoCapture)
if (qualities == null) {
2023-09-10 19:12:51 +02:00
qualities = SucklessRecorder.getVideoCapabilities(camera!!.cameraInfo).getSupportedQualities(DynamicRange.UNSPECIFIED)
2020-12-19 19:55:54 +01:00
}
2023-04-17 15:52:20 +02:00
videoCapture
} else {
refreshImageCapture()
camera = cameraProvider.bindToLifecycle(this, cameraSelector, cameraPreview, imageCapture)
imageCapture
})!!
adaptPreviewSize(currentUseCase.attachedSurfaceResolution!!.swap())
return currentUseCase
}
private fun setupCamera() {
if (permissionsGranted && ::extensionsManager.isInitialized && ::cameraProvider.isInitialized) {
cameraSelector = if (isBackCamera){ CameraSelector.DEFAULT_BACK_CAMERA } else { CameraSelector.DEFAULT_FRONT_CAMERA }
if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.AUTO)) {
cameraSelector = extensionsManager.getExtensionEnabledCameraSelector(cameraSelector, ExtensionMode.AUTO)
}
rebindUseCases()
}
2020-12-19 19:55:54 +01:00
}
2021-10-03 14:36:06 +02:00
private fun getOutputPath(isVideo: Boolean): String {
val baseName = if (isVideo) {"VID"} else {"IMG"}+'_'+dateFormat.format(Date())+'_'
2022-06-18 21:13:16 +02:00
var outputPath: String
2020-08-05 14:06:54 +02:00
do {
2022-06-18 21:13:16 +02:00
val fileName = baseName+(random.nextInt(fileNameRandomMax-fileNameRandomMin)+fileNameRandomMin)+'.'+ if (isVideo) {"mp4"} else {"jpg"}
outputPath = PathUtils.pathJoin(outputDirectory, fileName)
} while (encryptedVolume.pathExists(outputPath))
return outputPath
2021-10-03 14:36:06 +02:00
}
private fun startTimerThen(action: () -> Unit) {
2020-08-09 15:27:31 +02:00
if (timerDuration > 0){
2021-06-11 20:23:54 +02:00
binding.textTimer.visibility = View.VISIBLE
isWaitingForTimer = true
2022-04-20 15:17:33 +02:00
lifecycleScope.launch {
2020-08-09 15:27:31 +02:00
for (i in timerDuration downTo 1){
2022-04-20 15:17:33 +02:00
binding.textTimer.text = i.toString()
delay(1000)
2020-08-09 15:27:31 +02:00
}
2022-03-24 20:08:23 +01:00
if (!isFinishing) {
2022-04-20 15:17:33 +02:00
action()
binding.textTimer.visibility = View.GONE
2020-08-09 15:27:31 +02:00
}
isWaitingForTimer = false
2022-04-20 15:17:33 +02:00
}
2020-08-09 15:27:31 +02:00
} else {
action()
}
}
private fun onClickTakePhoto() {
if (!isWaitingForTimer) {
val outputPath = getOutputPath(false)
startTimerThen {
imageCapture?.let { imageCapture ->
val outputBuff = ByteArrayOutputStream()
val outputOptions = ImageCapture.OutputFileOptions.Builder(outputBuff).build()
imageCapture.takePicture(outputOptions, executor, object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
binding.takePhotoButton.onPhotoTaken()
2022-06-18 21:13:16 +02:00
if (encryptedVolume.importFile(ByteArrayInputStream(outputBuff.toByteArray()), outputPath)) {
Toast.makeText(applicationContext, getString(R.string.picture_save_success, outputPath), Toast.LENGTH_SHORT).show()
} else {
2023-02-28 22:50:59 +01:00
CustomAlertDialogBuilder(this@CameraActivity, theme)
.setTitle(R.string.error)
.setMessage(R.string.picture_save_failed)
.setPositiveButton(R.string.ok, null)
.show()
}
}
override fun onError(exception: ImageCaptureException) {
binding.takePhotoButton.onPhotoTaken()
Toast.makeText(applicationContext, exception.message, Toast.LENGTH_SHORT).show()
}
})
}
}
2021-10-03 14:36:06 +02:00
}
}
@SuppressLint("MissingPermission")
private fun onClickRecordVideo() {
if (isRecording) {
2023-04-17 15:52:20 +02:00
videoRecording?.stop()
} else if (!isWaitingForTimer) {
2021-10-03 14:36:06 +02:00
val path = getOutputPath(true)
val fileHandle = encryptedVolume.openFileWriteMode(path)
if (fileHandle == -1L) {
CustomAlertDialogBuilder(this, theme)
.setTitle(R.string.error)
.setMessage(R.string.file_creation_failed)
.setPositiveButton(R.string.ok, null)
.show()
return
}
val writer = AsynchronousSeekableWriter(object : SeekableWriter {
private var offset = 0L
2023-04-17 15:52:20 +02:00
override fun close() {
encryptedVolume.closeFile(fileHandle)
}
2023-04-17 15:52:20 +02:00
override fun seek(offset: Long) {
this.offset = offset
}
2023-04-17 15:52:20 +02:00
override fun write(buffer: ByteArray, size: Int) {
offset += encryptedVolume.write(fileHandle, offset, buffer, 0, size.toLong())
}
})
val pendingRecording = videoRecorder!!.prepareRecording(
this,
MuxerOutputOptions(FFmpegMuxer(writer))
).also {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
it.withAudioEnabled()
}
}
startTimerThen {
writer.start()
videoRecording = pendingRecording.start(executor) {
val buttons = arrayOf(binding.imageCaptureMode, binding.imageRatio, binding.imageTimer, binding.imageModeSwitch, binding.imageCameraSwitch)
2023-04-17 15:52:20 +02:00
when (it) {
is VideoRecordEvent.Start -> {
binding.recordVideoButton.setImageResource(R.drawable.stop_recording_video_button)
for (i in buttons) {
i.isEnabled = false
i.alpha = 0.5F
}
2023-04-17 15:52:20 +02:00
isRecording = true
}
is VideoRecordEvent.Finalize -> {
if (it.hasError()) {
it.cause?.printStackTrace()
Toast.makeText(applicationContext, it.cause?.message ?: ("Error: " + it.error), Toast.LENGTH_SHORT).show()
2023-04-17 15:52:20 +02:00
videoRecording?.close()
videoRecording = null
} else {
Toast.makeText(applicationContext, getString(R.string.video_save_success, path), Toast.LENGTH_SHORT).show()
}
binding.recordVideoButton.setImageResource(R.drawable.record_video_button)
for (i in buttons) {
i.isEnabled = true
i.alpha = 1F
}
2023-04-17 15:52:20 +02:00
isRecording = false
}
}
2023-04-17 15:52:20 +02:00
}
}
2020-08-09 15:27:31 +02:00
}
2020-08-05 14:06:54 +02:00
}
override fun onPause() {
super.onPause()
2020-09-04 04:00:19 +02:00
sensorOrientationListener.remove(this)
}
2020-09-04 04:00:19 +02:00
override fun onResume() {
super.onResume()
2023-03-07 23:25:17 +01:00
if (encryptedVolume.isClosed()) {
finish()
} else {
sensorOrientationListener.addListener(this)
}
2020-09-04 04:00:19 +02:00
}
override fun onOrientationChange(newOrientation: Int) {
2022-04-16 13:58:58 +02:00
val realOrientation = when (newOrientation) {
Surface.ROTATION_0 -> 0f
Surface.ROTATION_90 -> 90f
Surface.ROTATION_180 -> 180f
else -> 270f
}
2020-09-04 04:00:19 +02:00
val rotateAnimation = RotateAnimation(previousOrientation, when {
2022-04-16 13:58:58 +02:00
realOrientation - previousOrientation > 180 -> realOrientation - 360
realOrientation - previousOrientation < -180 -> realOrientation + 360
else -> realOrientation
2020-09-04 04:00:19 +02:00
}, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotateAnimation.duration = 300
rotateAnimation.interpolator = LinearInterpolator()
rotateAnimation.fillAfter = true
orientedIcons.map { it.startAnimation(rotateAnimation) }
2022-04-16 13:58:58 +02:00
previousOrientation = realOrientation
imageCapture?.targetRotation = newOrientation
videoCapture?.targetRotation = newOrientation
currentRotation = newOrientation
2020-09-04 04:00:19 +02:00
}
}
private fun Size.swap(): Size {
return Size(height, width)
2020-08-05 14:06:54 +02:00
}