Add front camera preview and camera switch UI

This commit is contained in:
Miguel Angel 2026-09-07 16:45:15 -05:00
parent 5be04b0217
commit 9deeb40217
5 changed files with 594 additions and 21 deletions

View file

@ -57,6 +57,7 @@ class MainActivity : ComponentActivity() {
@Inject lateinit var authSessionStore: AuthSessionStore
@Volatile private var isAuthenticated = false
private var frontPreviewOpened = false
companion object {
private const val TAG = "MainActivity"
@ -97,7 +98,16 @@ class MainActivity : ComponentActivity() {
})
lifecycleScope.launch {
authSessionStore.token.collect { token -> isAuthenticated = !token.isNullOrBlank() }
authSessionStore.token.collect { token ->
isAuthenticated = !token.isNullOrBlank()
if (isAuthenticated && !frontPreviewOpened) {
frontPreviewOpened = true
runOnUiThread {
openFrontPreview()
}
}
}
}
registerHardwareReceiver()
@ -222,6 +232,20 @@ class MainActivity : ComponentActivity() {
}
private fun openFrontPreview() {
if (!isAuthenticated) return
startActivity(
android.content.Intent(
this,
com.bodycamera.twentyfoulabs.camera.M530CameraActivity::class.java
).putExtra(
com.bodycamera.twentyfoulabs.camera.M530CameraActivity.EXTRA_MODE,
com.bodycamera.twentyfoulabs.camera.M530CameraActivity.MODE_FRONT_PREVIEW
).addFlags(android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP)
)
}
private fun handlePhotoButton() {
startActivity(
android.content.Intent(
@ -374,3 +398,4 @@ fun AppNavigation(viewModel: MainViewModel, onOpenPhotoCamera: () -> Unit, onOpe

View file

@ -1,10 +1,11 @@
package com.bodycamera.twentyfoulabs.camera
package com.bodycamera.twentyfoulabs.camera
import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
@ -54,6 +55,7 @@ class M530CameraActivity : ComponentActivity() {
const val EXTRA_MODE = "mode"
const val MODE_PHOTO = "photo"
const val MODE_VIDEO = "video"
const val MODE_FRONT_PREVIEW = "front_preview"
}
private lateinit var previewView: PreviewView
@ -63,6 +65,7 @@ class M530CameraActivity : ComponentActivity() {
private lateinit var physicalHintText: TextView
private lateinit var captureButton: Button
private lateinit var closeButton: Button
private lateinit var switchCameraButton: Button
private lateinit var recordDot: TextView
private lateinit var chronometer: Chronometer
@ -73,10 +76,11 @@ class M530CameraActivity : ComponentActivity() {
private var cameraProvider: ProcessCameraProvider? = null
private var lastPhysicalActionMs = 0L
private var lastTouchActionMs = 0L
private var frontPreviewUsesFrontCamera = true
private val cameraPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) startCamera() else stateText.text = "Permiso de cámara requerido"
if (granted) startCamera() else stateText.text = "Permiso de camara requerido"
}
private val audioPermissionLauncher =
@ -140,7 +144,11 @@ class M530CameraActivity : ComponentActivity() {
}
modeText = TextView(this).apply {
text = if (mode == MODE_VIDEO) "VIDEO" else "FOTO"
text = when (mode) {
MODE_VIDEO -> "VIDEO"
MODE_FRONT_PREVIEW -> "FRONTAL"
else -> "FOTO"
}
setTextColor(if (mode == MODE_VIDEO) 0xFFFF3B30.toInt() else 0xFFFFC400.toInt())
textSize = 14f
setTypeface(typeface, Typeface.BOLD)
@ -173,10 +181,11 @@ class M530CameraActivity : ComponentActivity() {
setPadding(dp(10), dp(5), dp(10), dp(5))
setBackgroundColor(0x7A000000)
isClickable = false
visibility = if (mode == MODE_FRONT_PREVIEW) View.GONE else View.VISIBLE
}
recordDot = TextView(this).apply {
text = "●"
text = "*"
setTextColor(0xFFFF3B30.toInt())
textSize = 18f
visibility = View.GONE
@ -203,33 +212,89 @@ class M530CameraActivity : ComponentActivity() {
)
closeButton = Button(this).apply {
text = "×"
text = "X"
gravity = Gravity.CENTER
setTextColor(Color.WHITE)
textSize = 25f
textSize = 18f
setTypeface(typeface, Typeface.BOLD)
minWidth = 0
minHeight = 0
setPadding(0, 0, 0, 0)
background = circleDrawable(0xB0000000.toInt(), Color.WHITE, dp(1))
background = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(0x99000000.toInt())
setStroke(dp(1), 0xCCFFFFFF.toInt())
}
isClickable = true
isFocusable = true
elevation = dp(12).toFloat()
elevation = dp(8).toFloat()
contentDescription = "Cerrar"
setOnClickListener {
stopRecordingIfNeeded()
finish()
}
}
root.addView(
closeButton,
FrameLayout.LayoutParams(dp(54), dp(54), Gravity.TOP or Gravity.END).apply {
topMargin = dp(62)
FrameLayout.LayoutParams(dp(44), dp(44), Gravity.TOP or Gravity.END).apply {
topMargin = dp(60)
rightMargin = dp(12)
}
)
switchCameraButton = Button(this).apply {
text = "CAMARA POSTERIOR"
setTextColor(Color.WHITE)
textSize = 11f
setTypeface(typeface, Typeface.BOLD)
gravity = Gravity.CENTER
minWidth = 0
minHeight = 0
setPadding(dp(16), 0, dp(16), 0)
background = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = dp(24).toFloat()
setColor(0xB31A1A1A.toInt())
setStroke(dp(1), 0xCCFFFFFF.toInt())
}
visibility = if (mode == MODE_FRONT_PREVIEW) View.VISIBLE else View.GONE
elevation = dp(10).toFloat()
contentDescription = "Cambiar camara"
setOnClickListener {
if (mode == MODE_FRONT_PREVIEW) {
frontPreviewUsesFrontCamera = !frontPreviewUsesFrontCamera
text = if (frontPreviewUsesFrontCamera)
"CAMARA POSTERIOR"
else
"CAMARA FRONTAL"
startCamera()
}
}
}
root.addView(
switchCameraButton,
FrameLayout.LayoutParams(
dp(170),
dp(46),
Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
).apply {
bottomMargin = dp(76)
}
)
captureButton = Button(this).apply {
text = if (mode == MODE_VIDEO) "REC" else "FOTO"
visibility = if (mode == MODE_FRONT_PREVIEW) View.GONE else View.VISIBLE
setTextColor(Color.WHITE)
textSize = 15f
setTypeface(typeface, Typeface.BOLD)
@ -289,14 +354,22 @@ class M530CameraActivity : ComponentActivity() {
}
physicalHintText = TextView(this).apply {
text = if (mode == MODE_VIDEO) "F5 VIDEO" else "F2 FOTO"
text = when (mode) {
MODE_VIDEO -> "F5 VIDEO"
MODE_FRONT_PREVIEW -> "EN VIVO"
else -> "F2 FOTO"
}
setTextColor(if (mode == MODE_VIDEO) 0xFFFF3B30.toInt() else 0xFFFFC400.toInt())
textSize = 15f
setTypeface(typeface, Typeface.BOLD)
}
stateText = TextView(this).apply {
text = if (mode == MODE_VIDEO) "Preparando video..." else "Preparando cámara..."
text = when (mode) {
MODE_VIDEO -> "Preparando video..."
MODE_FRONT_PREVIEW -> "Preparando camara frontal..."
else -> "Preparando camara..."
}
setTextColor(Color.WHITE)
textSize = 13f
gravity = Gravity.END or Gravity.CENTER_VERTICAL
@ -335,10 +408,10 @@ class M530CameraActivity : ComponentActivity() {
if (now - lastTouchActionMs < 350) return
lastTouchActionMs = now
if (mode == MODE_VIDEO) {
toggleVideoRecording()
} else {
takePhoto()
when (mode) {
MODE_VIDEO -> toggleVideoRecording()
MODE_FRONT_PREVIEW -> Unit
else -> takePhoto()
}
}
@ -355,7 +428,25 @@ class M530CameraActivity : ComponentActivity() {
provider.unbindAll()
if (mode == MODE_VIDEO) {
if (mode == MODE_FRONT_PREVIEW) {
imageCapture = null
videoCapture = null
provider.bindToLifecycle(
this,
if (frontPreviewUsesFrontCamera)
CameraSelector.DEFAULT_FRONT_CAMERA
else
CameraSelector.DEFAULT_BACK_CAMERA,
preview
)
stateText.text = if (frontPreviewUsesFrontCamera)
"Camara frontal lista"
else
"Camara posterior lista"
captureButton.isEnabled = false
} else if (mode == MODE_VIDEO) {
val recorder = Recorder.Builder()
.setQualitySelector(
QualitySelector.from(
@ -397,7 +488,7 @@ class M530CameraActivity : ComponentActivity() {
captureButton.isEnabled = true
}
} catch (t: Throwable) {
stateText.text = "Error de cámara"
stateText.text = "Error de camara"
captureButton.isEnabled = false
}
}, ContextCompat.getMainExecutor(this))
@ -405,7 +496,7 @@ class M530CameraActivity : ComponentActivity() {
private fun takePhoto() {
val capture = imageCapture ?: run {
stateText.text = "Cámara no lista"
stateText.text = "Camara no lista"
return
}
@ -624,7 +715,7 @@ class M530CameraActivity : ComponentActivity() {
chronometer.visibility = View.GONE
recordDot.visibility = View.GONE
stateText.text = if (error) "Error de grabación" else "Video guardado"
stateText.text = if (error) "Error de grabacion" else "Video guardado"
physicalHintText.text = "F5 VIDEO"
captureButton.text = "REC"
@ -641,6 +732,7 @@ class M530CameraActivity : ComponentActivity() {
setBackgroundColor(Color.WHITE)
alpha = 0.75f
isClickable = false
visibility = if (mode == MODE_FRONT_PREVIEW) View.GONE else View.VISIBLE
}
val root = findViewById<FrameLayout>(android.R.id.content)
root.addView(
@ -712,3 +804,8 @@ class M530CameraActivity : ComponentActivity() {
private fun timestamp(): String =
SimpleDateFormat("yyyyMMdd_HHmmss_SSS", Locale.US).format(Date())
}