From c858e5b9083a3e4eaa062dafcfd8005e248ba5e6 Mon Sep 17 00:00:00 2001 From: darthpaul Date: Fri, 11 Nov 2022 00:44:53 +0000 Subject: [PATCH 1/4] fix inconsistencies when user switches camera mode fast - debounce switching by 300ms when toggling between video/photo capture - setCameraAvailable only when there is no error and the camera state is Type.OPEN --- .../camera/activities/MainActivity.kt | 17 +++++++++--- .../camera/implementations/CameraXPreview.kt | 27 ++++++++++--------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt index 307f33da..7c0a8845 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -52,6 +52,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera const val PHOTO_MODE_INDEX = 1 const val VIDEO_MODE_INDEX = 0 private const val MIN_SWIPE_DISTANCE_X = 100 + private const val DELAY_BETWEEN_MODE_SWITCH = 300L } lateinit var mTimerHandler: Handler @@ -67,10 +68,14 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera private var mIsHardwareShutterHandled = false private var mCurrVideoRecTimer = 0 var mLastHandledOrientation = 0 + private val togglePhotoVideoRunnable = Runnable { + handleTogglePhotoVideo() + } private val tabSelectedListener = object : TabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab) { - handleTogglePhotoVideo() + camera_mode_tab.removeCallbacks(togglePhotoVideoRunnable) + camera_mode_tab.postDelayed(togglePhotoVideoRunnable, DELAY_BETWEEN_MODE_SWITCH) } } @@ -382,7 +387,13 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera @SuppressLint("ClickableViewAccessibility") 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 { // these can be null even if the docs say they cannot if (event1 == null || event2 == null) { @@ -404,7 +415,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } }) - camera_mode_tab.setOnTouchListener { _, event -> + bottom_overlay.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index 07497ee8..2b9400e6 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -244,20 +244,23 @@ class CameraXPreview( listener.onChangeCamera(isFrontCameraInUse()) camera?.cameraInfo?.cameraState?.observe(activity) { cameraState -> - when (cameraState.type) { - CameraState.Type.OPEN, - CameraState.Type.OPENING -> { - listener.setHasFrontAndBackCamera(hasFrontCamera() && hasBackCamera()) - listener.setCameraAvailable(true) - } - CameraState.Type.PENDING_OPEN, - CameraState.Type.CLOSING, - CameraState.Type.CLOSED -> { - listener.setCameraAvailable(false) + if (cameraState.error == null) { + when (cameraState.type) { + CameraState.Type.OPEN-> { + listener.setHasFrontAndBackCamera(hasFrontCamera() && hasBackCamera()) + listener.setCameraAvailable(true) + } + CameraState.Type.OPENING, + CameraState.Type.PENDING_OPEN, + CameraState.Type.CLOSING, + CameraState.Type.CLOSED -> { + listener.setCameraAvailable(false) + } } + } else { + listener.setCameraAvailable(false) + cameraErrorHandler.handleCameraError(cameraState.error) } - - cameraErrorHandler.handleCameraError(cameraState?.error) } } From fc2296e2ae10a5f4b303b97a8d8aa00751325720 Mon Sep 17 00:00:00 2001 From: darthpaul Date: Wed, 23 Nov 2022 14:32:16 +0000 Subject: [PATCH 2/4] fix inconsistencies when user switches camera mode fast - debounce switching by 500ms when toggling between video/photo capture - ensure there is only one source of truth for the current camera mode - this is the CameraXPreview - refactor the MainActivity to depend on the CameraXPreview for the camera mode - remove unused code in CameraXPreviewListener, MyPreview and in MainActivity --- .../camera/activities/MainActivity.kt | 234 ++++-------------- .../camera/helpers/Config.kt | 4 - .../implementations/CameraXInitializer.kt | 3 +- .../camera/implementations/CameraXPreview.kt | 112 +++++++-- .../implementations/CameraXPreviewListener.kt | 4 +- .../camera/interfaces/MyPreview.kt | 14 +- 6 files changed, 148 insertions(+), 223 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt index 7c0a8845..5a61dea2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -6,11 +6,8 @@ import android.content.Intent import android.content.res.ColorStateList import android.graphics.Bitmap import android.hardware.SensorManager -import android.hardware.camera2.CameraCharacteristics import android.net.Uri import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.provider.MediaStore import android.view.* import android.widget.LinearLayout @@ -52,10 +49,8 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera const val PHOTO_MODE_INDEX = 1 const val VIDEO_MODE_INDEX = 0 private const val MIN_SWIPE_DISTANCE_X = 100 - private const val DELAY_BETWEEN_MODE_SWITCH = 300L } - lateinit var mTimerHandler: Handler private lateinit var defaultScene: Scene private lateinit var flashModeScene: Scene private lateinit var mOrientationEventListener: OrientationEventListener @@ -63,19 +58,26 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera private var mPreview: MyPreview? = null private var mediaSizeToggleGroup: MaterialButtonToggleGroup? = null private var mPreviewUri: Uri? = null - private var mIsInPhotoMode = true - private var mIsCameraAvailable = false private var mIsHardwareShutterHandled = false - private var mCurrVideoRecTimer = 0 - var mLastHandledOrientation = 0 - private val togglePhotoVideoRunnable = Runnable { - handleTogglePhotoVideo() - } + private var mLastHandledOrientation = 0 private val tabSelectedListener = object : TabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab) { - camera_mode_tab.removeCallbacks(togglePhotoVideoRunnable) - camera_mode_tab.postDelayed(togglePhotoVideoRunnable, DELAY_BETWEEN_MODE_SWITCH) + 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() + } + } + } } } @@ -84,7 +86,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera super.onCreate(savedInstanceState) appLaunched(BuildConfig.APPLICATION_ID) requestWindowFeature(Window.FEATURE_NO_TITLE) - initVariables() tryInitCamera() supportActionBar?.hide() @@ -135,14 +136,9 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera override fun onResume() { super.onResume() if (hasStorageAndCameraPermissions()) { - resumeCameraItems() - setupPreviewImage(mIsInPhotoMode) + val isInPhotoMode = isInPhotoMode() + setupPreviewImage(isInPhotoMode) mFocusCircleView.setStrokeColor(getProperPrimaryColor()) - - if (isVideoCaptureIntent() && mIsInPhotoMode) { - handleTogglePhotoVideo() - checkButtons() - } toggleBottomButtons(enabled = true) mOrientationEventListener.enable() } @@ -160,8 +156,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera if (!hasStorageAndCameraPermissions() || isAskingPermissions) { return } - - hideTimer() mOrientationEventListener.disable() } @@ -177,18 +171,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } private fun initVariables() { - mIsInPhotoMode = if (isVideoCaptureIntent()) { - false - } else if (isImageCaptureIntent()) { - true - } else { - config.initPhotoMode - } - mIsCameraAvailable = false mIsHardwareShutterHandled = false - mCurrVideoRecTimer = 0 - mLastHandledOrientation = 0 - config.lastUsedCamera = CameraCharacteristics.LENS_FACING_BACK.toString() } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { @@ -223,16 +206,22 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera if (grantedCameraPermission) { handleStoragePermission { grantedStoragePermission -> if (grantedStoragePermission) { - if (mIsInPhotoMode) { - initializeCamera() + val isInPhotoMode = isInPhotoMode() + if (isInPhotoMode) { + initializeCamera(true) } else { handlePermission(PERMISSION_RECORD_AUDIO) { grantedRecordAudioPermission -> if (grantedRecordAudioPermission) { - initializeCamera() + initializeCamera(false) } else { toast(R.string.no_audio_permissions) - togglePhotoVideoMode() - tryInitCamera() + if (isThirdPartyIntent()) { + finish() + } else { + // re-initialize in photo mode + config.initPhotoMode = true + tryInitCamera() + } } } } @@ -248,6 +237,17 @@ 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) { if (isTiramisuPlus()) { handlePermission(PERMISSION_READ_MEDIA_IMAGES) { grantedReadImages -> @@ -268,24 +268,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera 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 { return MaterialButtonToggleGroup(this).apply { isSingleSelection = true @@ -293,7 +275,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } } - private fun initializeCamera() { + private fun initializeCamera(isInPhotoMode: Boolean) { setContentView(R.layout.activity_main) initButtons() initModeSwitcher() @@ -318,8 +300,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera WindowInsetsCompat.CONSUMED } - checkVideoCaptureIntent() - if (mIsInPhotoMode) { + if (isInPhotoMode) { selectPhotoTab() } else { selectVideoTab() @@ -332,25 +313,18 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera listener = this, outputUri = outputUri, isThirdPartyIntent = isThirdPartyIntent, - initInPhotoMode = mIsInPhotoMode, + initInPhotoMode = isInPhotoMode, ) - checkImageCaptureIntent() - mPreview?.setIsImageCaptureIntent(isImageCaptureIntent()) - val imageDrawable = if (config.lastUsedCamera == CameraCharacteristics.LENS_FACING_BACK.toString()) { - R.drawable.ic_camera_front_vector - } else { - R.drawable.ic_camera_rear_vector - } - - toggle_camera.setImageResource(imageDrawable) - - mFocusCircleView = FocusCircleView(applicationContext) + mFocusCircleView = FocusCircleView(this) view_holder.addView(mFocusCircleView) - mTimerHandler = Handler(Looper.getMainLooper()) setupPreviewImage(true) initFlashModeTransitionNames() + + if (isThirdPartyIntent) { + hideIntentButtons() + } } private fun initFlashModeTransitionNames() { @@ -362,9 +336,9 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } private fun initButtons() { - toggle_camera.setOnClickListener { toggleCamera() } + toggle_camera.setOnClickListener { mPreview!!.toggleFrontBackCamera() } last_photo_video_preview.setOnClickListener { showLastMediaPreview() } - toggle_flash.setOnClickListener { toggleFlash() } + toggle_flash.setOnClickListener { mPreview!!.handleFlashlightClick() } shutter.setOnClickListener { shutterPressed() } settings.setShadowIcon(R.drawable.ic_settings_vector) @@ -437,11 +411,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera mPreview?.setFlashlightState(flashMode) } - private fun toggleCamera() { - if (checkCameraAvailable()) { - mPreview!!.toggleFrontBackCamera() - } - } private fun showLastMediaPreview() { if (mPreviewUri != null) { @@ -450,24 +419,8 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } } - private fun toggleFlash() { - if (checkCameraAvailable()) { - if (mIsInPhotoMode) { - showFlashOptions(true) - } else { - mPreview?.toggleFlashlight() - } - } - } - private fun shutterPressed() { - if (checkCameraAvailable()) { - handleShutter() - } - } - - private fun handleShutter() { - if (mIsInPhotoMode) { + if (isInPhotoMode()) { toggleBottomButtons(enabled = false) change_resolution.isEnabled = true mPreview?.tryTakePicture() @@ -481,71 +434,15 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera startActivity(intent) } - private fun handleTogglePhotoVideo() { - 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() { + override fun onInitPhotoMode() { shutter.setImageResource(R.drawable.ic_shutter_animated) - mPreview?.initPhotoMode() setupPreviewImage(true) selectPhotoTab() } - private fun tryInitVideoMode() { - try { - mPreview?.initVideoMode() - initVideoButtons() - } catch (e: Exception) { - if (!isVideoCaptureIntent()) { - toast(R.string.video_mode_error) - } - } - } - - private fun initVideoButtons() { + override fun onInitVideoMode() { shutter.setImageResource(R.drawable.ic_video_rec_animated) setupPreviewImage(false) - mPreview?.checkFlashlight() selectVideoTab() } @@ -578,21 +475,9 @@ 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 { - return if (mIsInPhotoMode) hasPhotoModePermissions() else hasVideoModePermissions() + return if (isInPhotoMode()) hasPhotoModePermissions() else hasVideoModePermissions() } private fun hasPhotoModePermissions(): Boolean { @@ -648,17 +533,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera 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) { toggle_camera?.beVisibleIf(hasFrontAndBack) } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt index 0652b069..98728de1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt @@ -35,10 +35,6 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getBoolean(FLIP_PHOTOS, true) 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 get() = prefs.getInt(LAST_USED_CAMERA_LENS, CameraSelector.LENS_FACING_BACK) set(lens) = prefs.edit().putInt(LAST_USED_CAMERA_LENS, lens).apply() diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXInitializer.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXInitializer.kt index fce3bea2..4a28c7f8 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXInitializer.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXInitializer.kt @@ -23,7 +23,8 @@ class CameraXInitializer(private val activity: BaseSimpleActivity) { mediaOutputHelper, cameraErrorHandler, listener, - initInPhotoMode, + isThirdPartyIntent = isThirdPartyIntent, + initInPhotoMode = initInPhotoMode, ) } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index 2b9400e6..1da6da7f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -4,6 +4,8 @@ import android.annotation.SuppressLint import android.content.Context import android.hardware.SensorManager import android.hardware.display.DisplayManager +import android.os.Handler +import android.os.Looper import android.util.Rational import android.util.Size import android.view.* @@ -19,6 +21,7 @@ import androidx.camera.view.PreviewView.ScaleType import androidx.core.content.ContextCompat import androidx.core.view.doOnLayout import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.window.layout.WindowMetricsCalculator import com.bumptech.glide.load.ImageHeaderParser.UNKNOWN_ORIENTATION @@ -39,6 +42,7 @@ class CameraXPreview( private val mediaOutputHelper: MediaOutputHelper, private val cameraErrorHandler: CameraErrorHandler, private val listener: CameraXPreviewListener, + private val isThirdPartyIntent: Boolean, initInPhotoMode: Boolean, ) : MyPreview, DefaultLifecycleObserver { @@ -46,6 +50,7 @@ class CameraXPreview( // Auto focus is 1/6 of the area. private const val AF_SIZE = 1.0f / 6.0f private const val AE_SIZE = AF_SIZE * 1.5f + private const val CAMERA_MODE_SWITCH_WAIT_TIME = 500L } 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 cameraProvider: ProcessCameraProvider? = null @@ -92,13 +120,11 @@ class CameraXPreview( private var flashMode = FLASH_MODE_OFF private var isPhotoCapture = initInPhotoMode private var lastRotation = 0 + private var lastCameraStartTime = 0L init { bindToLifeCycle() mediaSoundHelper.loadSounds() - previewView.doOnLayout { - startCamera() - } } private fun bindToLifeCycle() { @@ -106,13 +132,12 @@ class CameraXPreview( } private fun startCamera(switching: Boolean = false) { - imageQualityManager.initSupportedQualities() - - val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) + val cameraProviderFuture = ProcessCameraProvider.getInstance(activity.applicationContext) cameraProviderFuture.addListener({ try { val provider = cameraProviderFuture.get() cameraProvider = provider + imageQualityManager.initSupportedQualities() videoQualityManager.initSupportedQualities(provider) bindCameraUseCases() setupCameraObservers() @@ -128,11 +153,11 @@ class CameraXPreview( val resolution = if (isPhotoCapture) { imageQualityManager.getUserSelectedResolution(cameraSelector).also { - displaySelectedResolution(it.toResolutionOption()) + listener.displaySelectedResolution(it.toResolutionOption()) } } else { val selectedQuality = videoQualityManager.getUserSelectedQuality(cameraSelector).also { - displaySelectedResolution(it.toResolutionOption()) + listener.displaySelectedResolution(it.toResolutionOption()) } MySize(selectedQuality.width, selectedQuality.height) } @@ -178,10 +203,6 @@ class CameraXPreview( setFlashlightState(config.flashlightState) } - private fun displaySelectedResolution(resolutionOption: ResolutionOption) { - listener.displaySelectedResolution(resolutionOption) - } - private fun getRotatedResolution(resolution: MySize, rotationDegrees: Int): Size { return if (rotationDegrees == Surface.ROTATION_0 || rotationDegrees == Surface.ROTATION_180) { Size(resolution.height, resolution.width) @@ -201,10 +222,12 @@ class CameraXPreview( return if (isPhotoCapture) { buildImageCapture(resolution, rotation).also { imageCapture = it + videoCapture = null } } else { buildVideoCapture().also { videoCapture = it + imageCapture = null } } } @@ -242,15 +265,19 @@ class CameraXPreview( private fun setupCameraObservers() { listener.setFlashAvailable(camera?.cameraInfo?.hasFlashUnit() ?: false) listener.onChangeCamera(isFrontCameraInUse()) - + if (isPhotoCapture) { + listener.onInitPhotoMode() + } else { + listener.onInitVideoMode() + } camera?.cameraInfo?.cameraState?.observe(activity) { cameraState -> if (cameraState.error == null) { when (cameraState.type) { - CameraState.Type.OPEN-> { + CameraState.Type.OPENING, + CameraState.Type.OPEN -> { listener.setHasFrontAndBackCamera(hasFrontCamera() && hasBackCamera()) listener.setCameraAvailable(true) } - CameraState.Type.OPENING, CameraState.Type.PENDING_OPEN, CameraState.Type.CLOSING, CameraState.Type.CLOSED -> { @@ -315,12 +342,21 @@ class CameraXPreview( override fun onStart(owner: LifecycleOwner) { orientationEventListener.enable() + previewView.doOnLayout { + if (owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + startCamera() + } + } } override fun onStop(owner: LifecycleOwner) { orientationEventListener.disable() } + override fun isInPhotoMode(): Boolean { + return isPhotoCapture + } + override fun showChangeResolution() { val selectedResolution = if (isPhotoCapture) { imageQualityManager.getUserSelectedResolution(cameraSelector).toResolutionOption() @@ -380,7 +416,15 @@ class CameraXPreview( startCamera(switching = true) } - override fun toggleFlashlight() { + override fun handleFlashlightClick() { + if (isPhotoCapture) { + listener.showFlashOptions(true) + } else { + toggleFlashlight() + } + } + + private fun toggleFlashlight() { val newFlashMode = if (isPhotoCapture) { when (flashMode) { FLASH_MODE_OFF -> FLASH_MODE_ON @@ -399,17 +443,22 @@ class CameraXPreview( } override fun setFlashlightState(state: Int) { + var flashState = state if (isPhotoCapture) { - camera?.cameraControl?.enableTorch(state == FLASH_ALWAYS_ON) + camera?.cameraControl?.enableTorch(flashState == FLASH_ALWAYS_ON) } 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 imageCapture?.flashMode = newFlashMode - config.flashlightState = state - listener.onChangeFlashMode(state) + config.flashlightState = flashState + listener.onChangeFlashMode(flashState) } override fun tryTakePicture() { @@ -470,13 +519,23 @@ class CameraXPreview( } override fun initPhotoMode() { - isPhotoCapture = true - startCamera() + debounceChangeCameraMode(photoModeRunnable) } override fun initVideoMode() { - isPhotoCapture = false - startCamera() + debounceChangeCameraMode(videoModeRunnable) + } + + 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() { @@ -492,8 +551,7 @@ class CameraXPreview( private fun startRecording() { val videoCapture = videoCapture ?: throw IllegalStateException("Camera initialization failed.") - val mediaOutput = mediaOutputHelper.getVideoMediaOutput() - val recording = when (mediaOutput) { + val recording = when (val mediaOutput = mediaOutputHelper.getVideoMediaOutput()) { is MediaOutput.FileDescriptorMediaOutput -> { FileDescriptorOutputOptions.Builder(mediaOutput.fileDescriptor).build() .let { videoCapture.output.prepareRecording(activity, it) } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt index 530d514a..5c9f1866 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt @@ -5,7 +5,9 @@ import android.net.Uri import com.simplemobiletools.camera.models.ResolutionOption interface CameraXPreviewListener { - fun setCameraAvailable(available: Boolean) + fun onInitPhotoMode() + fun onInitVideoMode() + fun setCameraAvailable(available: Boolean) {} fun setHasFrontAndBackCamera(hasFrontAndBack: Boolean) fun setFlashAvailable(available: Boolean) fun onChangeCamera(frontCamera: Boolean) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt index 02447fc4..f3b770a8 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt @@ -1,18 +1,14 @@ package com.simplemobiletools.camera.interfaces -import android.net.Uri - interface MyPreview { - fun setTargetUri(uri: Uri) = Unit + fun isInPhotoMode(): Boolean - fun setIsImageCaptureIntent(isImageCaptureIntent: Boolean) = Unit - - fun setFlashlightState(state: Int) = Unit + fun setFlashlightState(state: Int) fun toggleFrontBackCamera() - fun toggleFlashlight() = Unit + fun handleFlashlightClick() fun tryTakePicture() @@ -22,7 +18,5 @@ interface MyPreview { fun initVideoMode() - fun checkFlashlight() = Unit - - fun showChangeResolution() = Unit + fun showChangeResolution() } From edfa69f6ac27e3da757b59d1901f2c2cd316edca Mon Sep 17 00:00:00 2001 From: darthpaul Date: Wed, 23 Nov 2022 14:45:32 +0000 Subject: [PATCH 3/4] revert swiping target and CameraX to 1.2.0-beta01 - reverting because VideoCapture fails to initialize on some devices - revert swiping target to the tab --- app/build.gradle | 2 +- .../com/simplemobiletools/camera/activities/MainActivity.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8f87a484..3d086b5d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,7 +69,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1" 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-camera2:$camerax_version" implementation "androidx.camera:camera-video:$camerax_version" diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt index 6498d2db..52a0cac4 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -393,7 +393,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } }) - bottom_overlay.setOnTouchListener { _, event -> + camera_mode_tab.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) } } From ffe8be73c5fa2f46d0d781e6c279d0b7f5a0660b Mon Sep 17 00:00:00 2001 From: Tibor Kaputa Date: Wed, 23 Nov 2022 17:44:19 +0100 Subject: [PATCH 4/4] minor code style formatting changes --- .../camera/activities/MainActivity.kt | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt index 52a0cac4..7699ae89 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -37,11 +37,11 @@ import com.simplemobiletools.camera.views.FocusCircleView import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.* 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.layout_flash.* import kotlinx.android.synthetic.main.layout_top.* +import java.util.concurrent.TimeUnit +import kotlin.math.abs class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener { private companion object { @@ -68,7 +68,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera when (tab.position) { VIDEO_MODE_INDEX -> mPreview?.initVideoMode() PHOTO_MODE_INDEX -> mPreview?.initPhotoMode() - else -> throw IllegalStateException("Unsupported tab position ${tab.position}") + else -> throw IllegalStateException("Unsupported tab position ${tab.position}") } } else { toast(R.string.no_audio_permissions) @@ -113,6 +113,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera if (!triggerListener) { removeTabListener() } + camera_mode_tab.getTabAt(PHOTO_MODE_INDEX)?.select() setTabListener() } @@ -142,6 +143,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera toggleBottomButtons(enabled = true) mOrientationEventListener.enable() } + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) ensureTransparentNavigationBar() } @@ -156,6 +158,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera if (!hasStorageAndCameraPermissions() || isAskingPermissions) { return } + mOrientationEventListener.disable() } @@ -238,14 +241,13 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } private fun isInPhotoMode(): Boolean { - return mPreview?.isInPhotoMode() - ?: if (isVideoCaptureIntent()) { - false - } else if (isImageCaptureIntent()) { - true - } else { - config.initPhotoMode - } + return mPreview?.isInPhotoMode() ?: if (isVideoCaptureIntent()) { + false + } else if (isImageCaptureIntent()) { + true + } else { + config.initPhotoMode + } } private fun handleStoragePermission(callback: (granted: Boolean) -> Unit) { @@ -479,7 +481,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } } - private fun hasStorageAndCameraPermissions(): Boolean { return if (isInPhotoMode()) hasPhotoModePermissions() else hasVideoModePermissions() } @@ -677,11 +678,11 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera isFrontCamera: Boolean, onSelect: (index: Int, changed: Boolean) -> Unit ) { - top_options.removeView(mediaSizeToggleGroup) val mediaSizeToggleGroup = createToggleGroup().apply { mediaSizeToggleGroup = this } + top_options.addView(mediaSizeToggleGroup) val onItemClick = { clickedViewId: Int -> @@ -709,6 +710,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera val params = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT).apply { weight = 1f } + return (layoutInflater.inflate(R.layout.layout_button, null) as MaterialButton).apply { layoutParams = params setShadowIcon(resolutionOption.imageDrawableResId)