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 7699ae89..15691056 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -620,7 +620,11 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera change_resolution.isEnabled = false settings.isEnabled = false - shutter.isSelected = true + shutter.post { + if (!isDestroyed) { + shutter.isSelected = true + } + } } override fun onVideoRecordingStopped() { diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaActionSound.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaActionSound.kt new file mode 100644 index 00000000..a94f9412 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaActionSound.kt @@ -0,0 +1,197 @@ +package com.simplemobiletools.camera.helpers + +import android.content.Context +import android.media.AudioAttributes +import android.media.MediaPlayer +import android.media.SoundPool +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.util.Log +import java.io.File + +/** + * Inspired by [android.media.MediaActionSound] + */ +class MediaActionSound(private val context: Context) { + companion object { + private const val NUM_MEDIA_SOUND_STREAMS = 1 + private val SOUND_DIRS = arrayOf( + "/product/media/audio/ui/", + "/system/media/audio/ui/" + ) + private val SOUND_FILES = arrayOf( + "camera_click.ogg", + "camera_focus.ogg", + "VideoRecord.ogg", + "VideoStop.ogg" + ) + private const val TAG = "MediaActionSound" + const val SHUTTER_CLICK = 0 + const val START_VIDEO_RECORDING = 2 + const val STOP_VIDEO_RECORDING = 3 + private const val STATE_NOT_LOADED = 0 + private const val STATE_LOADING = 1 + private const val STATE_LOADING_PLAY_REQUESTED = 2 + private const val STATE_LOADED = 3 + } + + private class SoundState(val name: Int) { + var id = 0 // 0 is an invalid sample ID. + var state: Int = STATE_NOT_LOADED + var path: String? = null + } + + private var soundPool: SoundPool? = SoundPool.Builder() + .setMaxStreams(NUM_MEDIA_SOUND_STREAMS) + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) + .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + ).build() + private var mediaPlayer: MediaPlayer? = null + private var playCompletionRunnable: Runnable? = null + + private val mSounds: Array = arrayOfNulls(SOUND_FILES.size) + private val playTimeHandler = Handler(Looper.getMainLooper()) + private val mLoadCompleteListener = SoundPool.OnLoadCompleteListener { _, sampleId, status -> + for (sound in mSounds) { + if (sound!!.id != sampleId) { + continue + } + var soundToBePlayed: SoundState? = null + synchronized(sound) { + if (status != 0) { + sound.state = STATE_NOT_LOADED + sound.id = 0 + Log.e(TAG, "OnLoadCompleteListener() error: $status loading sound: ${sound.name}") + return@OnLoadCompleteListener + } + when (sound.state) { + STATE_LOADING -> sound.state = STATE_LOADED + STATE_LOADING_PLAY_REQUESTED -> { + soundToBePlayed = sound + sound.state = STATE_LOADED + } + else -> Log.e(TAG, "OnLoadCompleteListener() called in wrong state: ${sound.state} for sound: ${sound.name}") + } + } + if (soundToBePlayed != null) { + playSoundPool(soundToBePlayed!!) + } + break + } + } + + init { + soundPool!!.setOnLoadCompleteListener(mLoadCompleteListener) + for (i in mSounds.indices) { + mSounds[i] = SoundState(i) + } + } + + private fun loadSound(sound: SoundState?): Int { + val soundFileName = SOUND_FILES[sound!!.name] + for (soundDir in SOUND_DIRS) { + val soundPath = soundDir + soundFileName + sound.path = soundPath + val id = soundPool!!.load(soundPath, 1) + if (id > 0) { + sound.state = STATE_LOADING + sound.id = id + return id + } + } + return 0 + } + + fun load(soundName: Int) { + if (soundName < 0 || soundName >= SOUND_FILES.size) { + throw RuntimeException("Unknown sound requested: $soundName") + } + val sound = mSounds[soundName] + synchronized(sound!!) { + when (sound.state) { + STATE_NOT_LOADED -> { + loadSound(sound).let { soundId -> + if (soundId <= 0) { + Log.e(TAG, "load() error loading sound: $soundName") + } + } + } + else -> Log.e(TAG, "load() called in wrong state: $sound for sound: $soundName") + } + } + } + + fun play(soundName: Int, onPlayComplete: (() -> Unit)? = null) { + if (soundName < 0 || soundName >= SOUND_FILES.size) { + throw RuntimeException("Unknown sound requested: $soundName") + } + removeHandlerCallbacks() + if (onPlayComplete != null) { + playCompletionRunnable = Runnable { + onPlayComplete.invoke() + } + } + val sound = mSounds[soundName] + synchronized(sound!!) { + when (sound.state) { + STATE_NOT_LOADED -> { + val soundId = loadSound(sound) + if (soundId <= 0) { + Log.e(TAG, "play() error loading sound: $soundName") + } else { + sound.state = STATE_LOADING_PLAY_REQUESTED + } + } + STATE_LOADING -> sound.state = STATE_LOADING_PLAY_REQUESTED + STATE_LOADED -> { + playSoundPool(sound) + } + else -> Log.e(TAG, "play() called in wrong state: ${sound.state} for sound: $soundName") + } + } + } + + private fun playSoundPool(sound: SoundState) { + if (playCompletionRunnable != null) { + val duration = getSoundDuration(sound.path!!) + playTimeHandler.postDelayed(playCompletionRunnable!!, duration) + } + soundPool!!.play(sound.id, 1.0f, 1.0f, 0, 0, 1.0f) + } + + fun release() { + if (soundPool != null) { + for (sound in mSounds) { + synchronized(sound!!) { + sound.state = STATE_NOT_LOADED + sound.id = 0 + } + } + soundPool!!.release() + soundPool = null + } + removeHandlerCallbacks() + releaseMediaPlayer() + } + + private fun removeHandlerCallbacks() { + playCompletionRunnable?.let { playTimeHandler.removeCallbacks(it) } + playCompletionRunnable = null + } + + private fun releaseMediaPlayer() { + mediaPlayer?.release() + mediaPlayer = null + } + + private fun getSoundDuration(soundPath: String): Long { + releaseMediaPlayer() + mediaPlayer = MediaPlayer.create(context, Uri.fromFile(File(soundPath))) + return mediaPlayer!!.duration.toLong() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSoundHelper.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSoundHelper.kt index abc2e71a..1e32a822 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSoundHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSoundHelper.kt @@ -1,9 +1,9 @@ package com.simplemobiletools.camera.helpers -import android.media.MediaActionSound +import android.content.Context -class MediaSoundHelper { - private val mediaActionSound = MediaActionSound() +class MediaSoundHelper(context: Context) { + private val mediaActionSound = MediaActionSound(context) fun loadSounds() { mediaActionSound.load(MediaActionSound.START_VIDEO_RECORDING) @@ -15,11 +15,15 @@ class MediaSoundHelper { mediaActionSound.play(MediaActionSound.SHUTTER_CLICK) } - fun playStartVideoRecordingSound() { - mediaActionSound.play(MediaActionSound.START_VIDEO_RECORDING) + fun playStartVideoRecordingSound(onPlayComplete: () -> Unit) { + mediaActionSound.play(MediaActionSound.START_VIDEO_RECORDING, onPlayComplete) } fun playStopVideoRecordingSound() { mediaActionSound.play(MediaActionSound.STOP_VIDEO_RECORDING) } + + fun release() { + mediaActionSound.release() + } } 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 1da6da7f..8146dcb7 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -57,7 +57,7 @@ class CameraXPreview( private val contentResolver = activity.contentResolver private val mainExecutor = ContextCompat.getMainExecutor(activity) private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager - private val mediaSoundHelper = MediaSoundHelper() + private val mediaSoundHelper = MediaSoundHelper(activity) private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate() private val videoQualityManager = VideoQualityManager(activity) private val imageQualityManager = ImageQualityManager(activity) @@ -353,6 +353,11 @@ class CameraXPreview( orientationEventListener.disable() } + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + mediaSoundHelper.release() + } + override fun isInPhotoMode(): Boolean { return isPhotoCapture } @@ -540,7 +545,14 @@ class CameraXPreview( override fun toggleRecording() { if (currentRecording == null || recordingState is VideoRecordEvent.Finalize) { - startRecording() + if (config.isSoundEnabled) { + mediaSoundHelper.playStartVideoRecordingSound(onPlayComplete = { + startRecording() + }) + listener.onVideoRecordingStarted() + } else { + startRecording() + } } else { currentRecording?.stop() currentRecording = null @@ -571,7 +583,6 @@ class CameraXPreview( recordingState = recordEvent when (recordEvent) { is VideoRecordEvent.Start -> { - playStartVideoRecordingSoundIfEnabled() listener.onVideoRecordingStarted() } @@ -598,12 +609,6 @@ class CameraXPreview( } } - private fun playStartVideoRecordingSoundIfEnabled() { - if (config.isSoundEnabled) { - mediaSoundHelper.playStartVideoRecordingSound() - } - } - private fun playStopVideoRecordingSoundIfEnabled() { if (config.isSoundEnabled) { mediaSoundHelper.playStopVideoRecordingSound()