Merge pull request #370 from KryptKode/fix/fast-switch-mode-inconsistencies

fix inconsistencies when user switches camera mode fast
This commit is contained in:
Tibor Kaputa 2022-11-23 17:45:26 +01:00 committed by GitHub
commit 6e794d4cf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 174 additions and 233 deletions

View File

@ -69,7 +69,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
implementation 'androidx.window:window:1.1.0-alpha03' implementation 'androidx.window:window:1.1.0-alpha03'
def camerax_version = '1.2.0-rc01' def camerax_version = '1.2.0-beta01'
implementation "androidx.camera:camera-core:$camerax_version" implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-video:$camerax_version" implementation "androidx.camera:camera-video:$camerax_version"

View File

@ -6,11 +6,8 @@ import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Bitmap import android.graphics.Bitmap
import android.hardware.SensorManager import android.hardware.SensorManager
import android.hardware.camera2.CameraCharacteristics
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.view.* import android.view.*
import android.widget.LinearLayout import android.widget.LinearLayout
@ -40,11 +37,11 @@ import com.simplemobiletools.camera.views.FocusCircleView
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.Release import com.simplemobiletools.commons.models.Release
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.layout_flash.* import kotlinx.android.synthetic.main.layout_flash.*
import kotlinx.android.synthetic.main.layout_top.* import kotlinx.android.synthetic.main.layout_top.*
import java.util.concurrent.TimeUnit
import kotlin.math.abs
class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener { class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener {
private companion object { private companion object {
@ -54,7 +51,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
private const val MIN_SWIPE_DISTANCE_X = 100 private const val MIN_SWIPE_DISTANCE_X = 100
} }
lateinit var mTimerHandler: Handler
private lateinit var defaultScene: Scene private lateinit var defaultScene: Scene
private lateinit var flashModeScene: Scene private lateinit var flashModeScene: Scene
private lateinit var mOrientationEventListener: OrientationEventListener private lateinit var mOrientationEventListener: OrientationEventListener
@ -62,15 +58,26 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
private var mPreview: MyPreview? = null private var mPreview: MyPreview? = null
private var mediaSizeToggleGroup: MaterialButtonToggleGroup? = null private var mediaSizeToggleGroup: MaterialButtonToggleGroup? = null
private var mPreviewUri: Uri? = null private var mPreviewUri: Uri? = null
private var mIsInPhotoMode = true
private var mIsCameraAvailable = false
private var mIsHardwareShutterHandled = false private var mIsHardwareShutterHandled = false
private var mCurrVideoRecTimer = 0 private var mLastHandledOrientation = 0
var mLastHandledOrientation = 0
private val tabSelectedListener = object : TabSelectedListener { private val tabSelectedListener = object : TabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) { override fun onTabSelected(tab: TabLayout.Tab) {
handleTogglePhotoVideo() handlePermission(PERMISSION_RECORD_AUDIO) {
if (it) {
when (tab.position) {
VIDEO_MODE_INDEX -> mPreview?.initVideoMode()
PHOTO_MODE_INDEX -> mPreview?.initPhotoMode()
else -> throw IllegalStateException("Unsupported tab position ${tab.position}")
}
} else {
toast(R.string.no_audio_permissions)
selectPhotoTab()
if (isVideoCaptureIntent()) {
finish()
}
}
}
} }
} }
@ -79,7 +86,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
appLaunched(BuildConfig.APPLICATION_ID) appLaunched(BuildConfig.APPLICATION_ID)
requestWindowFeature(Window.FEATURE_NO_TITLE) requestWindowFeature(Window.FEATURE_NO_TITLE)
initVariables() initVariables()
tryInitCamera() tryInitCamera()
supportActionBar?.hide() supportActionBar?.hide()
@ -107,6 +113,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
if (!triggerListener) { if (!triggerListener) {
removeTabListener() removeTabListener()
} }
camera_mode_tab.getTabAt(PHOTO_MODE_INDEX)?.select() camera_mode_tab.getTabAt(PHOTO_MODE_INDEX)?.select()
setTabListener() setTabListener()
} }
@ -130,17 +137,13 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (hasStorageAndCameraPermissions()) { if (hasStorageAndCameraPermissions()) {
resumeCameraItems() val isInPhotoMode = isInPhotoMode()
setupPreviewImage(mIsInPhotoMode) setupPreviewImage(isInPhotoMode)
mFocusCircleView.setStrokeColor(getProperPrimaryColor()) mFocusCircleView.setStrokeColor(getProperPrimaryColor())
if (isVideoCaptureIntent() && mIsInPhotoMode) {
handleTogglePhotoVideo()
checkButtons()
}
toggleBottomButtons(enabled = true) toggleBottomButtons(enabled = true)
mOrientationEventListener.enable() mOrientationEventListener.enable()
} }
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
ensureTransparentNavigationBar() ensureTransparentNavigationBar()
} }
@ -156,7 +159,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
return return
} }
hideTimer()
mOrientationEventListener.disable() mOrientationEventListener.disable()
} }
@ -172,18 +174,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
} }
private fun initVariables() { private fun initVariables() {
mIsInPhotoMode = if (isVideoCaptureIntent()) {
false
} else if (isImageCaptureIntent()) {
true
} else {
config.initPhotoMode
}
mIsCameraAvailable = false
mIsHardwareShutterHandled = false mIsHardwareShutterHandled = false
mCurrVideoRecTimer = 0
mLastHandledOrientation = 0
config.lastUsedCamera = CameraCharacteristics.LENS_FACING_BACK.toString()
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
@ -218,19 +209,25 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
if (grantedCameraPermission) { if (grantedCameraPermission) {
handleStoragePermission { grantedStoragePermission -> handleStoragePermission { grantedStoragePermission ->
if (grantedStoragePermission) { if (grantedStoragePermission) {
if (mIsInPhotoMode) { val isInPhotoMode = isInPhotoMode()
initializeCamera() if (isInPhotoMode) {
initializeCamera(true)
} else { } else {
handlePermission(PERMISSION_RECORD_AUDIO) { grantedRecordAudioPermission -> handlePermission(PERMISSION_RECORD_AUDIO) { grantedRecordAudioPermission ->
if (grantedRecordAudioPermission) { if (grantedRecordAudioPermission) {
initializeCamera() initializeCamera(false)
} else { } else {
toast(R.string.no_audio_permissions) toast(R.string.no_audio_permissions)
togglePhotoVideoMode() if (isThirdPartyIntent()) {
finish()
} else {
// re-initialize in photo mode
config.initPhotoMode = true
tryInitCamera() tryInitCamera()
} }
} }
} }
}
} else { } else {
toast(R.string.no_storage_permissions) toast(R.string.no_storage_permissions)
finish() finish()
@ -243,6 +240,16 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
} }
} }
private fun isInPhotoMode(): Boolean {
return mPreview?.isInPhotoMode() ?: if (isVideoCaptureIntent()) {
false
} else if (isImageCaptureIntent()) {
true
} else {
config.initPhotoMode
}
}
private fun handleStoragePermission(callback: (granted: Boolean) -> Unit) { private fun handleStoragePermission(callback: (granted: Boolean) -> Unit) {
if (isTiramisuPlus()) { if (isTiramisuPlus()) {
handlePermission(PERMISSION_READ_MEDIA_IMAGES) { grantedReadImages -> handlePermission(PERMISSION_READ_MEDIA_IMAGES) { grantedReadImages ->
@ -263,24 +270,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
private fun isVideoCaptureIntent(): Boolean = intent?.action == MediaStore.ACTION_VIDEO_CAPTURE private fun isVideoCaptureIntent(): Boolean = intent?.action == MediaStore.ACTION_VIDEO_CAPTURE
private fun checkImageCaptureIntent() {
if (isImageCaptureIntent()) {
hideIntentButtons()
val output = intent.extras?.get(MediaStore.EXTRA_OUTPUT)
if (output != null && output is Uri) {
mPreview?.setTargetUri(output)
}
}
}
private fun checkVideoCaptureIntent() {
if (isVideoCaptureIntent()) {
mIsInPhotoMode = false
hideIntentButtons()
shutter.setImageResource(R.drawable.ic_video_rec_vector)
}
}
private fun createToggleGroup(): MaterialButtonToggleGroup { private fun createToggleGroup(): MaterialButtonToggleGroup {
return MaterialButtonToggleGroup(this).apply { return MaterialButtonToggleGroup(this).apply {
isSingleSelection = true isSingleSelection = true
@ -288,7 +277,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
} }
} }
private fun initializeCamera() { private fun initializeCamera(isInPhotoMode: Boolean) {
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
initButtons() initButtons()
initModeSwitcher() initModeSwitcher()
@ -313,8 +302,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
WindowInsetsCompat.CONSUMED WindowInsetsCompat.CONSUMED
} }
checkVideoCaptureIntent() if (isInPhotoMode) {
if (mIsInPhotoMode) {
selectPhotoTab() selectPhotoTab()
} else { } else {
selectVideoTab() selectVideoTab()
@ -327,25 +315,18 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
listener = this, listener = this,
outputUri = outputUri, outputUri = outputUri,
isThirdPartyIntent = isThirdPartyIntent, isThirdPartyIntent = isThirdPartyIntent,
initInPhotoMode = mIsInPhotoMode, initInPhotoMode = isInPhotoMode,
) )
checkImageCaptureIntent()
mPreview?.setIsImageCaptureIntent(isImageCaptureIntent())
val imageDrawable = if (config.lastUsedCamera == CameraCharacteristics.LENS_FACING_BACK.toString()) { mFocusCircleView = FocusCircleView(this)
R.drawable.ic_camera_front_vector
} else {
R.drawable.ic_camera_rear_vector
}
toggle_camera.setImageResource(imageDrawable)
mFocusCircleView = FocusCircleView(applicationContext)
view_holder.addView(mFocusCircleView) view_holder.addView(mFocusCircleView)
mTimerHandler = Handler(Looper.getMainLooper())
setupPreviewImage(true) setupPreviewImage(true)
initFlashModeTransitionNames() initFlashModeTransitionNames()
if (isThirdPartyIntent) {
hideIntentButtons()
}
} }
private fun initFlashModeTransitionNames() { private fun initFlashModeTransitionNames() {
@ -357,9 +338,9 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
} }
private fun initButtons() { private fun initButtons() {
toggle_camera.setOnClickListener { toggleCamera() } toggle_camera.setOnClickListener { mPreview!!.toggleFrontBackCamera() }
last_photo_video_preview.setOnClickListener { showLastMediaPreview() } last_photo_video_preview.setOnClickListener { showLastMediaPreview() }
toggle_flash.setOnClickListener { toggleFlash() } toggle_flash.setOnClickListener { mPreview!!.handleFlashlightClick() }
shutter.setOnClickListener { shutterPressed() } shutter.setOnClickListener { shutterPressed() }
settings.setShadowIcon(R.drawable.ic_settings_vector) settings.setShadowIcon(R.drawable.ic_settings_vector)
@ -382,7 +363,13 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun initModeSwitcher() { private fun initModeSwitcher() {
val gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() { val gestureDetector = GestureDetectorCompat(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
// we have to return true here so ACTION_UP
// (and onFling) can be dispatched
return true
}
override fun onFling(event1: MotionEvent, event2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { override fun onFling(event1: MotionEvent, event2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
// these can be null even if the docs say they cannot, getting event1.x in itself can cause crashes // these can be null even if the docs say they cannot, getting event1.x in itself can cause crashes
try { try {
@ -430,11 +417,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
mPreview?.setFlashlightState(flashMode) mPreview?.setFlashlightState(flashMode)
} }
private fun toggleCamera() {
if (checkCameraAvailable()) {
mPreview!!.toggleFrontBackCamera()
}
}
private fun showLastMediaPreview() { private fun showLastMediaPreview() {
if (mPreviewUri != null) { if (mPreviewUri != null) {
@ -443,24 +425,8 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
} }
} }
private fun toggleFlash() {
if (checkCameraAvailable()) {
if (mIsInPhotoMode) {
showFlashOptions(true)
} else {
mPreview?.toggleFlashlight()
}
}
}
private fun shutterPressed() { private fun shutterPressed() {
if (checkCameraAvailable()) { if (isInPhotoMode()) {
handleShutter()
}
}
private fun handleShutter() {
if (mIsInPhotoMode) {
toggleBottomButtons(enabled = false) toggleBottomButtons(enabled = false)
change_resolution.isEnabled = true change_resolution.isEnabled = true
mPreview?.tryTakePicture() mPreview?.tryTakePicture()
@ -474,71 +440,15 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
startActivity(intent) startActivity(intent)
} }
private fun handleTogglePhotoVideo() { override fun onInitPhotoMode() {
handlePermission(PERMISSION_RECORD_AUDIO) {
if (it) {
togglePhotoVideo()
} else {
toast(R.string.no_audio_permissions)
selectPhotoTab()
if (isVideoCaptureIntent()) {
finish()
}
}
}
}
private fun togglePhotoVideo() {
if (!checkCameraAvailable()) {
return
}
if (isVideoCaptureIntent()) {
mPreview?.initVideoMode()
}
mPreview?.setFlashlightState(FLASH_OFF)
hideTimer()
togglePhotoVideoMode()
checkButtons()
toggleBottomButtons(enabled = true)
}
private fun togglePhotoVideoMode() {
mIsInPhotoMode = !mIsInPhotoMode
config.initPhotoMode = mIsInPhotoMode
}
private fun checkButtons() {
if (mIsInPhotoMode) {
initPhotoMode()
} else {
tryInitVideoMode()
}
}
private fun initPhotoMode() {
shutter.setImageResource(R.drawable.ic_shutter_animated) shutter.setImageResource(R.drawable.ic_shutter_animated)
mPreview?.initPhotoMode()
setupPreviewImage(true) setupPreviewImage(true)
selectPhotoTab() selectPhotoTab()
} }
private fun tryInitVideoMode() { override fun onInitVideoMode() {
try {
mPreview?.initVideoMode()
initVideoButtons()
} catch (e: Exception) {
if (!isVideoCaptureIntent()) {
toast(R.string.video_mode_error)
}
}
}
private fun initVideoButtons() {
shutter.setImageResource(R.drawable.ic_video_rec_animated) shutter.setImageResource(R.drawable.ic_video_rec_animated)
setupPreviewImage(false) setupPreviewImage(false)
mPreview?.checkFlashlight()
selectVideoTab() selectVideoTab()
} }
@ -571,21 +481,8 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
} }
} }
private fun hideTimer() {
video_rec_curr_timer.text = 0.getFormattedDuration()
video_rec_curr_timer.beGone()
mCurrVideoRecTimer = 0
mTimerHandler.removeCallbacksAndMessages(null)
}
private fun resumeCameraItems() {
if (!mIsInPhotoMode) {
initVideoButtons()
}
}
private fun hasStorageAndCameraPermissions(): Boolean { private fun hasStorageAndCameraPermissions(): Boolean {
return if (mIsInPhotoMode) hasPhotoModePermissions() else hasVideoModePermissions() return if (isInPhotoMode()) hasPhotoModePermissions() else hasVideoModePermissions()
} }
private fun hasPhotoModePermissions(): Boolean { private fun hasPhotoModePermissions(): Boolean {
@ -641,17 +538,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
private fun rotate(view: View, degrees: Int) = view.animate().rotation(degrees.toFloat()).start() private fun rotate(view: View, degrees: Int) = view.animate().rotation(degrees.toFloat()).start()
private fun checkCameraAvailable(): Boolean {
if (!mIsCameraAvailable) {
toast(R.string.camera_unavailable)
}
return mIsCameraAvailable
}
override fun setCameraAvailable(available: Boolean) {
mIsCameraAvailable = available
}
override fun setHasFrontAndBackCamera(hasFrontAndBack: Boolean) { override fun setHasFrontAndBackCamera(hasFrontAndBack: Boolean) {
toggle_camera?.beVisibleIf(hasFrontAndBack) toggle_camera?.beVisibleIf(hasFrontAndBack)
} }
@ -792,11 +678,11 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
isFrontCamera: Boolean, isFrontCamera: Boolean,
onSelect: (index: Int, changed: Boolean) -> Unit onSelect: (index: Int, changed: Boolean) -> Unit
) { ) {
top_options.removeView(mediaSizeToggleGroup) top_options.removeView(mediaSizeToggleGroup)
val mediaSizeToggleGroup = createToggleGroup().apply { val mediaSizeToggleGroup = createToggleGroup().apply {
mediaSizeToggleGroup = this mediaSizeToggleGroup = this
} }
top_options.addView(mediaSizeToggleGroup) top_options.addView(mediaSizeToggleGroup)
val onItemClick = { clickedViewId: Int -> val onItemClick = { clickedViewId: Int ->
@ -824,6 +710,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
val params = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT).apply { val params = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
weight = 1f weight = 1f
} }
return (layoutInflater.inflate(R.layout.layout_button, null) as MaterialButton).apply { return (layoutInflater.inflate(R.layout.layout_button, null) as MaterialButton).apply {
layoutParams = params layoutParams = params
setShadowIcon(resolutionOption.imageDrawableResId) setShadowIcon(resolutionOption.imageDrawableResId)

View File

@ -35,10 +35,6 @@ class Config(context: Context) : BaseConfig(context) {
get() = prefs.getBoolean(FLIP_PHOTOS, true) get() = prefs.getBoolean(FLIP_PHOTOS, true)
set(flipPhotos) = prefs.edit().putBoolean(FLIP_PHOTOS, flipPhotos).apply() set(flipPhotos) = prefs.edit().putBoolean(FLIP_PHOTOS, flipPhotos).apply()
var lastUsedCamera: String
get() = prefs.getString(LAST_USED_CAMERA, "0")!!
set(cameraId) = prefs.edit().putString(LAST_USED_CAMERA, cameraId).apply()
var lastUsedCameraLens: Int var lastUsedCameraLens: Int
get() = prefs.getInt(LAST_USED_CAMERA_LENS, CameraSelector.LENS_FACING_BACK) get() = prefs.getInt(LAST_USED_CAMERA_LENS, CameraSelector.LENS_FACING_BACK)
set(lens) = prefs.edit().putInt(LAST_USED_CAMERA_LENS, lens).apply() set(lens) = prefs.edit().putInt(LAST_USED_CAMERA_LENS, lens).apply()

View File

@ -23,7 +23,8 @@ class CameraXInitializer(private val activity: BaseSimpleActivity) {
mediaOutputHelper, mediaOutputHelper,
cameraErrorHandler, cameraErrorHandler,
listener, listener,
initInPhotoMode, isThirdPartyIntent = isThirdPartyIntent,
initInPhotoMode = initInPhotoMode,
) )
} }

View File

@ -4,6 +4,8 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.hardware.SensorManager import android.hardware.SensorManager
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.os.Handler
import android.os.Looper
import android.util.Rational import android.util.Rational
import android.util.Size import android.util.Size
import android.view.* import android.view.*
@ -19,6 +21,7 @@ import androidx.camera.view.PreviewView.ScaleType
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.window.layout.WindowMetricsCalculator import androidx.window.layout.WindowMetricsCalculator
import com.bumptech.glide.load.ImageHeaderParser.UNKNOWN_ORIENTATION import com.bumptech.glide.load.ImageHeaderParser.UNKNOWN_ORIENTATION
@ -39,6 +42,7 @@ class CameraXPreview(
private val mediaOutputHelper: MediaOutputHelper, private val mediaOutputHelper: MediaOutputHelper,
private val cameraErrorHandler: CameraErrorHandler, private val cameraErrorHandler: CameraErrorHandler,
private val listener: CameraXPreviewListener, private val listener: CameraXPreviewListener,
private val isThirdPartyIntent: Boolean,
initInPhotoMode: Boolean, initInPhotoMode: Boolean,
) : MyPreview, DefaultLifecycleObserver { ) : MyPreview, DefaultLifecycleObserver {
@ -46,6 +50,7 @@ class CameraXPreview(
// Auto focus is 1/6 of the area. // Auto focus is 1/6 of the area.
private const val AF_SIZE = 1.0f / 6.0f private const val AF_SIZE = 1.0f / 6.0f
private const val AE_SIZE = AF_SIZE * 1.5f private const val AE_SIZE = AF_SIZE * 1.5f
private const val CAMERA_MODE_SWITCH_WAIT_TIME = 500L
} }
private val config = activity.config private val config = activity.config
@ -80,6 +85,29 @@ class CameraXPreview(
} }
} }
} }
private val startCameraHandler = Handler(Looper.getMainLooper())
private val photoModeRunnable = Runnable {
if (imageCapture == null) {
isPhotoCapture = true
if (!isThirdPartyIntent) { // we don't want to store the state for 3rd party intents
config.initPhotoMode = true
}
startCamera()
} else {
listener.onInitPhotoMode()
}
}
private val videoModeRunnable = Runnable {
if (videoCapture == null) {
isPhotoCapture = false
if (!isThirdPartyIntent) { // we don't want to store the state for 3rd party intents
config.initPhotoMode = false
}
startCamera()
} else {
listener.onInitVideoMode()
}
}
private var preview: Preview? = null private var preview: Preview? = null
private var cameraProvider: ProcessCameraProvider? = null private var cameraProvider: ProcessCameraProvider? = null
@ -92,13 +120,11 @@ class CameraXPreview(
private var flashMode = FLASH_MODE_OFF private var flashMode = FLASH_MODE_OFF
private var isPhotoCapture = initInPhotoMode private var isPhotoCapture = initInPhotoMode
private var lastRotation = 0 private var lastRotation = 0
private var lastCameraStartTime = 0L
init { init {
bindToLifeCycle() bindToLifeCycle()
mediaSoundHelper.loadSounds() mediaSoundHelper.loadSounds()
previewView.doOnLayout {
startCamera()
}
} }
private fun bindToLifeCycle() { private fun bindToLifeCycle() {
@ -106,13 +132,12 @@ class CameraXPreview(
} }
private fun startCamera(switching: Boolean = false) { private fun startCamera(switching: Boolean = false) {
imageQualityManager.initSupportedQualities() val cameraProviderFuture = ProcessCameraProvider.getInstance(activity.applicationContext)
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
cameraProviderFuture.addListener({ cameraProviderFuture.addListener({
try { try {
val provider = cameraProviderFuture.get() val provider = cameraProviderFuture.get()
cameraProvider = provider cameraProvider = provider
imageQualityManager.initSupportedQualities()
videoQualityManager.initSupportedQualities(provider) videoQualityManager.initSupportedQualities(provider)
bindCameraUseCases() bindCameraUseCases()
setupCameraObservers() setupCameraObservers()
@ -128,11 +153,11 @@ class CameraXPreview(
val resolution = if (isPhotoCapture) { val resolution = if (isPhotoCapture) {
imageQualityManager.getUserSelectedResolution(cameraSelector).also { imageQualityManager.getUserSelectedResolution(cameraSelector).also {
displaySelectedResolution(it.toResolutionOption()) listener.displaySelectedResolution(it.toResolutionOption())
} }
} else { } else {
val selectedQuality = videoQualityManager.getUserSelectedQuality(cameraSelector).also { val selectedQuality = videoQualityManager.getUserSelectedQuality(cameraSelector).also {
displaySelectedResolution(it.toResolutionOption()) listener.displaySelectedResolution(it.toResolutionOption())
} }
MySize(selectedQuality.width, selectedQuality.height) MySize(selectedQuality.width, selectedQuality.height)
} }
@ -178,10 +203,6 @@ class CameraXPreview(
setFlashlightState(config.flashlightState) setFlashlightState(config.flashlightState)
} }
private fun displaySelectedResolution(resolutionOption: ResolutionOption) {
listener.displaySelectedResolution(resolutionOption)
}
private fun getRotatedResolution(resolution: MySize, rotationDegrees: Int): Size { private fun getRotatedResolution(resolution: MySize, rotationDegrees: Int): Size {
return if (rotationDegrees == Surface.ROTATION_0 || rotationDegrees == Surface.ROTATION_180) { return if (rotationDegrees == Surface.ROTATION_0 || rotationDegrees == Surface.ROTATION_180) {
Size(resolution.height, resolution.width) Size(resolution.height, resolution.width)
@ -201,10 +222,12 @@ class CameraXPreview(
return if (isPhotoCapture) { return if (isPhotoCapture) {
buildImageCapture(resolution, rotation).also { buildImageCapture(resolution, rotation).also {
imageCapture = it imageCapture = it
videoCapture = null
} }
} else { } else {
buildVideoCapture().also { buildVideoCapture().also {
videoCapture = it videoCapture = it
imageCapture = null
} }
} }
} }
@ -242,11 +265,16 @@ class CameraXPreview(
private fun setupCameraObservers() { private fun setupCameraObservers() {
listener.setFlashAvailable(camera?.cameraInfo?.hasFlashUnit() ?: false) listener.setFlashAvailable(camera?.cameraInfo?.hasFlashUnit() ?: false)
listener.onChangeCamera(isFrontCameraInUse()) listener.onChangeCamera(isFrontCameraInUse())
if (isPhotoCapture) {
listener.onInitPhotoMode()
} else {
listener.onInitVideoMode()
}
camera?.cameraInfo?.cameraState?.observe(activity) { cameraState -> camera?.cameraInfo?.cameraState?.observe(activity) { cameraState ->
if (cameraState.error == null) {
when (cameraState.type) { when (cameraState.type) {
CameraState.Type.OPEN, CameraState.Type.OPENING,
CameraState.Type.OPENING -> { CameraState.Type.OPEN -> {
listener.setHasFrontAndBackCamera(hasFrontCamera() && hasBackCamera()) listener.setHasFrontAndBackCamera(hasFrontCamera() && hasBackCamera())
listener.setCameraAvailable(true) listener.setCameraAvailable(true)
} }
@ -256,8 +284,10 @@ class CameraXPreview(
listener.setCameraAvailable(false) listener.setCameraAvailable(false)
} }
} }
} else {
cameraErrorHandler.handleCameraError(cameraState?.error) listener.setCameraAvailable(false)
cameraErrorHandler.handleCameraError(cameraState.error)
}
} }
} }
@ -312,12 +342,21 @@ class CameraXPreview(
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
orientationEventListener.enable() orientationEventListener.enable()
previewView.doOnLayout {
if (owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
startCamera()
}
}
} }
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
orientationEventListener.disable() orientationEventListener.disable()
} }
override fun isInPhotoMode(): Boolean {
return isPhotoCapture
}
override fun showChangeResolution() { override fun showChangeResolution() {
val selectedResolution = if (isPhotoCapture) { val selectedResolution = if (isPhotoCapture) {
imageQualityManager.getUserSelectedResolution(cameraSelector).toResolutionOption() imageQualityManager.getUserSelectedResolution(cameraSelector).toResolutionOption()
@ -377,7 +416,15 @@ class CameraXPreview(
startCamera(switching = true) startCamera(switching = true)
} }
override fun toggleFlashlight() { override fun handleFlashlightClick() {
if (isPhotoCapture) {
listener.showFlashOptions(true)
} else {
toggleFlashlight()
}
}
private fun toggleFlashlight() {
val newFlashMode = if (isPhotoCapture) { val newFlashMode = if (isPhotoCapture) {
when (flashMode) { when (flashMode) {
FLASH_MODE_OFF -> FLASH_MODE_ON FLASH_MODE_OFF -> FLASH_MODE_ON
@ -396,17 +443,22 @@ class CameraXPreview(
} }
override fun setFlashlightState(state: Int) { override fun setFlashlightState(state: Int) {
var flashState = state
if (isPhotoCapture) { if (isPhotoCapture) {
camera?.cameraControl?.enableTorch(state == FLASH_ALWAYS_ON) camera?.cameraControl?.enableTorch(flashState == FLASH_ALWAYS_ON)
} else { } else {
camera?.cameraControl?.enableTorch(state == FLASH_ON || state == FLASH_ALWAYS_ON) camera?.cameraControl?.enableTorch(flashState == FLASH_ON || flashState == FLASH_ALWAYS_ON)
// reset to the FLASH_ON for video capture
if (flashState == FLASH_ALWAYS_ON) {
flashState = FLASH_ON
} }
val newFlashMode = state.toCameraXFlashMode() }
val newFlashMode = flashState.toCameraXFlashMode()
flashMode = newFlashMode flashMode = newFlashMode
imageCapture?.flashMode = newFlashMode imageCapture?.flashMode = newFlashMode
config.flashlightState = state config.flashlightState = flashState
listener.onChangeFlashMode(state) listener.onChangeFlashMode(flashState)
} }
override fun tryTakePicture() { override fun tryTakePicture() {
@ -467,13 +519,23 @@ class CameraXPreview(
} }
override fun initPhotoMode() { override fun initPhotoMode() {
isPhotoCapture = true debounceChangeCameraMode(photoModeRunnable)
startCamera()
} }
override fun initVideoMode() { override fun initVideoMode() {
isPhotoCapture = false debounceChangeCameraMode(videoModeRunnable)
startCamera() }
private fun debounceChangeCameraMode(cameraModeRunnable: Runnable) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastCameraStartTime > CAMERA_MODE_SWITCH_WAIT_TIME) {
cameraModeRunnable.run()
} else {
startCameraHandler.removeCallbacks(photoModeRunnable)
startCameraHandler.removeCallbacks(videoModeRunnable)
startCameraHandler.postDelayed(cameraModeRunnable, CAMERA_MODE_SWITCH_WAIT_TIME)
}
lastCameraStartTime = currentTime
} }
override fun toggleRecording() { override fun toggleRecording() {
@ -489,8 +551,7 @@ class CameraXPreview(
private fun startRecording() { private fun startRecording() {
val videoCapture = videoCapture ?: throw IllegalStateException("Camera initialization failed.") val videoCapture = videoCapture ?: throw IllegalStateException("Camera initialization failed.")
val mediaOutput = mediaOutputHelper.getVideoMediaOutput() val recording = when (val mediaOutput = mediaOutputHelper.getVideoMediaOutput()) {
val recording = when (mediaOutput) {
is MediaOutput.FileDescriptorMediaOutput -> { is MediaOutput.FileDescriptorMediaOutput -> {
FileDescriptorOutputOptions.Builder(mediaOutput.fileDescriptor).build() FileDescriptorOutputOptions.Builder(mediaOutput.fileDescriptor).build()
.let { videoCapture.output.prepareRecording(activity, it) } .let { videoCapture.output.prepareRecording(activity, it) }

View File

@ -5,7 +5,9 @@ import android.net.Uri
import com.simplemobiletools.camera.models.ResolutionOption import com.simplemobiletools.camera.models.ResolutionOption
interface CameraXPreviewListener { interface CameraXPreviewListener {
fun setCameraAvailable(available: Boolean) fun onInitPhotoMode()
fun onInitVideoMode()
fun setCameraAvailable(available: Boolean) {}
fun setHasFrontAndBackCamera(hasFrontAndBack: Boolean) fun setHasFrontAndBackCamera(hasFrontAndBack: Boolean)
fun setFlashAvailable(available: Boolean) fun setFlashAvailable(available: Boolean)
fun onChangeCamera(frontCamera: Boolean) fun onChangeCamera(frontCamera: Boolean)

View File

@ -1,18 +1,14 @@
package com.simplemobiletools.camera.interfaces package com.simplemobiletools.camera.interfaces
import android.net.Uri
interface MyPreview { interface MyPreview {
fun setTargetUri(uri: Uri) = Unit fun isInPhotoMode(): Boolean
fun setIsImageCaptureIntent(isImageCaptureIntent: Boolean) = Unit fun setFlashlightState(state: Int)
fun setFlashlightState(state: Int) = Unit
fun toggleFrontBackCamera() fun toggleFrontBackCamera()
fun toggleFlashlight() = Unit fun handleFlashlightClick()
fun tryTakePicture() fun tryTakePicture()
@ -22,7 +18,5 @@ interface MyPreview {
fun initVideoMode() fun initVideoMode()
fun checkFlashlight() = Unit fun showChangeResolution()
fun showChangeResolution() = Unit
} }