Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions app/src/main/java/app/grapheneos/camera/CamConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ class CamConfig(private val mActivity: MainActivity) {

const val CAMERA_SOUNDS = "camera_sounds"

const val QUICK_VIDEO_HOLD = "quick_video_hold"

const val ENABLE_ZSL = "enable_zsl"

const val SELECT_HIGHEST_RESOLUTION = "select_highest_resolution"
Expand Down Expand Up @@ -160,6 +162,8 @@ class CamConfig(private val mActivity: MainActivity) {

const val CAMERA_SOUNDS = true

const val QUICK_VIDEO_HOLD = true

const val ENABLE_ZSL = false

const val SELECT_HIGHEST_RESOLUTION = false
Expand Down Expand Up @@ -297,6 +301,9 @@ class CamConfig(private val mActivity: MainActivity) {

private var currentMode: CameraMode = DEFAULT_CAMERA_MODE

val mode: CameraMode
get() = currentMode

var aspectRatio: Int
get() {
return when {
Expand Down Expand Up @@ -410,6 +417,19 @@ class CamConfig(private val mActivity: MainActivity) {
editor.apply()
}

var quickVideoHold: Boolean
get() {
return commonPref.getBoolean(
SettingValues.Key.QUICK_VIDEO_HOLD,
SettingValues.Default.QUICK_VIDEO_HOLD
)
}
set(value) {
val editor = commonPref.edit()
editor.putBoolean(SettingValues.Key.QUICK_VIDEO_HOLD, value)
editor.apply()
}

var scanAllCodes: Boolean
get() {
return commonPref.getBoolean(
Expand Down Expand Up @@ -761,6 +781,13 @@ class CamConfig(private val mActivity: MainActivity) {
editor.putBoolean(SettingValues.Key.CAMERA_SOUNDS, SettingValues.Default.CAMERA_SOUNDS)
}

if (!commonPref.contains(SettingValues.Key.QUICK_VIDEO_HOLD)) {
editor.putBoolean(
SettingValues.Key.QUICK_VIDEO_HOLD,
SettingValues.Default.QUICK_VIDEO_HOLD
)
}

// Note: This is a workaround to keep save image/video as previewed 'on' by
// default starting from v73 and 'off' by default for versions before that
//
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package app.grapheneos.camera.capturer

import app.grapheneos.camera.CameraMode
import app.grapheneos.camera.ui.activities.CaptureActivity
import app.grapheneos.camera.ui.activities.MainActivity

class QuickVideoController(private val mActivity: MainActivity) {

private val camConfig get() = mActivity.camConfig
private val videoCapturer get() = mActivity.videoCapturer

private var pressActive = false
private var startPending = false
private var sourceMode: CameraMode? = null
private var hardwareKeyCode: Int? = null

val isEngaged: Boolean
get() = startPending || sourceMode != null

private val isEligible: Boolean
get() = mActivity !is CaptureActivity
&& !mActivity.requiresVideoModeOnly
&& !camConfig.isVideoMode
&& !camConfig.isQRMode
&& camConfig.quickVideoHold
&& mActivity.timerDuration == 0
&& !videoCapturer.isRecording

fun onPressDown() {
pressActive = true
}

fun start(): Boolean {
pressActive = true
if (!isEligible) {
pressActive = false
return false
}

sourceMode = camConfig.mode
startPending = true

camConfig.switchMode(CameraMode.VIDEO)
startIfPending()
return true
}

fun startFromHardwareKey(keyCode: Int): Boolean {
if (!start()) {
return false
}
hardwareKeyCode = keyCode
return true
}

fun onCameraReady() {
startIfPending()
}

fun release(): Boolean {
pressActive = false

if (startPending) {
startPending = false
restoreSourceMode()
return true
}

val source = sourceMode ?: return false

if (videoCapturer.isRecording) {
videoCapturer.stopRecording {
if (sourceMode == source) {
restoreSourceMode()
}
}
} else {
restoreSourceMode()
}

return true
}

fun onHardwareKeyRelease(keyCode: Int): Boolean {
if (hardwareKeyCode != keyCode) {
return false
}
hardwareKeyCode = null
return release()
}

fun reset() {
pressActive = false
startPending = false
sourceMode = null
hardwareKeyCode = null
}

private fun startIfPending() {
if (!startPending || !camConfig.isVideoMode) {
return
}
startPending = false

if (!pressActive) {
restoreSourceMode()
return
}

videoCapturer.startRecording(forceAudio = true)
if (!videoCapturer.isRecording) {
restoreSourceMode()
}
}

private fun restoreSourceMode() {
val source = sourceMode ?: return
sourceMode = null
startPending = false

if (!videoCapturer.isRecording && camConfig.mode != source) {
camConfig.switchMode(source)
}
}
}
26 changes: 23 additions & 3 deletions app/src/main/java/app/grapheneos/camera/capturer/VideoCapturer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class VideoCapturer(private val mActivity: MainActivity) {
private val videoFileFormat = ".mp4"

private var recording: Recording? = null
private var onFinalizeCallback: (() -> Unit)? = null
private var forceAudioOnNextStart = false

var isMuted = false
private set
Expand Down Expand Up @@ -149,7 +151,11 @@ class VideoCapturer(private val mActivity: MainActivity) {
return null
}

fun startRecording() {
fun startRecording(forceAudio: Boolean = false) {
if (forceAudio) {
forceAudioOnNextStart = true
}

if (camConfig.camera == null) return
val recorder = camConfig.videoCapture?.output ?: return
if (isRecording) return
Expand All @@ -161,8 +167,9 @@ class VideoCapturer(private val mActivity: MainActivity) {
includeAudio = false

val ctx = mActivity
val shouldIncludeAudio = forceAudioOnNextStart || ctx.settingsDialog.includeAudioToggle.isChecked

if (ctx.settingsDialog.includeAudioToggle.isChecked) {
if (shouldIncludeAudio) {
if (ctx.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PERMISSION_GRANTED) {
includeAudio = true
} else {
Expand All @@ -171,6 +178,7 @@ class VideoCapturer(private val mActivity: MainActivity) {
return
}
}
forceAudioOnNextStart = false

val recordingCtx = try {
createRecordingContext(recorder, fileName)!!
Expand Down Expand Up @@ -205,6 +213,9 @@ class VideoCapturer(private val mActivity: MainActivity) {
}

if (event is VideoRecordEvent.Finalize) {
val finalizeCallback = onFinalizeCallback
onFinalizeCallback = null

afterRecordingStops()

camConfig.mPlayer.playVRStopSound()
Expand All @@ -213,12 +224,14 @@ class VideoCapturer(private val mActivity: MainActivity) {
when (event.error) {
VideoRecordEvent.Finalize.ERROR_NO_VALID_DATA -> {
ctx.showMessage(R.string.recording_too_short_to_be_saved)
finalizeCallback?.invoke()
return@start
}
VideoRecordEvent.Finalize.ERROR_ENCODING_FAILED,
VideoRecordEvent.Finalize.ERROR_RECORDER_ERROR,
VideoRecordEvent.Finalize.ERROR_UNKNOWN -> {
ctx.showMessage(ctx.getString(R.string.unable_to_save_video_verbose, event.error))
finalizeCallback?.invoke()
return@start
}
else -> {
Expand Down Expand Up @@ -251,6 +264,8 @@ class VideoCapturer(private val mActivity: MainActivity) {
if (ctx is VideoCaptureActivity) {
ctx.afterRecording(uri)
}

finalizeCallback?.invoke()
}
}

Expand Down Expand Up @@ -394,11 +409,16 @@ class VideoCapturer(private val mActivity: MainActivity) {
recording?.mute(false)
}

fun stopRecording() {
fun stopRecording(onStopped: (() -> Unit)? = null) {
onFinalizeCallback = onStopped
recording?.stop()
recording?.close()
recording = null
}

fun clearForceAudioForNextStart() {
forceAudioOnNextStart = false
}
}

@Throws(Exception::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import app.grapheneos.camera.ITEM_TYPE_IMAGE
import app.grapheneos.camera.ITEM_TYPE_VIDEO
import app.grapheneos.camera.R
import app.grapheneos.camera.capturer.ImageCapturer
import app.grapheneos.camera.capturer.QuickVideoController
import app.grapheneos.camera.capturer.VideoCapturer
import app.grapheneos.camera.capturer.getVideoThumbnail
import app.grapheneos.camera.databinding.ActivityMainBinding
Expand Down Expand Up @@ -247,6 +248,7 @@ open class MainActivity : AppCompatActivity(),
lateinit var micOffIcon: ImageView

private var shouldRestartRecording = false
private val quickVideo = QuickVideoController(this)

fun startFocusTimer() {
handler.postDelayed(runnable, autoCenterFocusDuration)
Expand All @@ -265,6 +267,7 @@ open class MainActivity : AppCompatActivity(),
return@registerForActivityResult
}
showAudioPermissionDeniedDialog {
videoCapturer.clearForceAudioForNextStart()
videoCapturer.startRecording()
}
}
Expand Down Expand Up @@ -508,6 +511,10 @@ open class MainActivity : AppCompatActivity(),
}

override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
if (quickVideo.onHardwareKeyRelease(keyCode)) {
return true
}

// there are no camera controls in qr mode
if (camConfig.isQRMode) {
return super.onKeyUp(keyCode, event)
Expand Down Expand Up @@ -536,12 +543,24 @@ open class MainActivity : AppCompatActivity(),

override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
if (event?.repeatCount == 0) {
event.startTracking()
}
// Pretend as if the event was handled by the app (avoid volume bar from appearing)
return true
}
return super.onKeyDown(keyCode, event)
}

override fun onKeyLongPress(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
quickVideo.startFromHardwareKey(keyCode)
return true
}

return super.onKeyLongPress(keyCode, event)
}

override fun onResume() {
super.onResume()
resumeOrientationSensor()
Expand Down Expand Up @@ -659,6 +678,7 @@ open class MainActivity : AppCompatActivity(),
}

restartRecordingIfPermissionsWasUnavailable()
quickVideo.onCameraReady()
} else {
previewGrid.visibility = View.INVISIBLE
val lastFrame = lastFrame
Expand Down Expand Up @@ -756,7 +776,30 @@ open class MainActivity : AppCompatActivity(),
}

captureButton = binding.captureButton
captureButton.setOnLongClickListener {
quickVideo.start()
}
captureButton.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
quickVideo.onPressDown()
}

MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
if (quickVideo.release()) {
return@setOnTouchListener true
}
}
}

false
}
captureButton.setOnClickListener {
if (quickVideo.isEngaged) {
return@setOnClickListener
}

resetAutoSleep()
if (camConfig.isVideoMode) {
if (videoCapturer.isRecording) {
Expand Down Expand Up @@ -1776,6 +1819,7 @@ open class MainActivity : AppCompatActivity(),
override fun onStop() {
super.onStop()
isStarted = false
quickVideo.reset()
if (this::videoCapturer.isInitialized && videoCapturer.isRecording) {
videoCapturer.stopRecording()
}
Expand Down
Loading