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 65b1e4fa..43868954 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -53,6 +53,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 TIMER_2_SECONDS = 2001 } private lateinit var defaultScene: Scene @@ -60,6 +61,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera private lateinit var timerScene: Scene private lateinit var mOrientationEventListener: OrientationEventListener private lateinit var mFocusCircleView: FocusCircleView + private lateinit var mediaSoundHelper: MediaSoundHelper private var mPreview: MyPreview? = null private var mediaSizeToggleGroup: MaterialButtonToggleGroup? = null private var mPreviewUri: Uri? = null @@ -172,6 +174,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera override fun onDestroy() { super.onDestroy() mPreview = null + mediaSoundHelper.release() } override fun onBackPressed() { @@ -182,6 +185,8 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera private fun initVariables() { mIsHardwareShutterHandled = false + mediaSoundHelper = MediaSoundHelper(this) + mediaSoundHelper.loadSounds() } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { @@ -321,6 +326,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera mPreview = CameraXInitializer(this).createCameraXPreview( preview_view, listener = this, + mediaSoundHelper = mediaSoundHelper, outputUri = outputUri, isThirdPartyIntent = isThirdPartyIntent, initInPhotoMode = isInPhotoMode, @@ -486,6 +492,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } private fun cancelTimer() { + mediaSoundHelper.stopTimerCountdown2SecondsSound() countDownTimer?.cancel() countDownTimer = null resetViewsOnTimerFinish() @@ -854,10 +861,19 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera hideViewsOnTimerStart() shutter.setImageState(intArrayOf(R.attr.state_timer_cancel), true) timer_text.beVisible() + var playSound = true countDownTimer = object : CountDownTimer(timerMode.millisInFuture, 1000) { override fun onTick(millisUntilFinished: Long) { val seconds = (TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished) + 1).toString() timer_text.setText(seconds) + if (playSound && config.isSoundEnabled) { + if (millisUntilFinished <= TIMER_2_SECONDS) { + mediaSoundHelper.playTimerCountdown2SecondsSound() + playSound = false + } else { + mediaSoundHelper.playTimerCountdownSound() + } + } } override fun onFinish() { diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaActionSound.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaActionSound.kt index a8ac8dda..892eba42 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaActionSound.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaActionSound.kt @@ -8,6 +8,8 @@ import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log +import androidx.annotation.RawRes +import com.simplemobiletools.camera.R import java.io.File /** @@ -15,59 +17,57 @@ import java.io.File */ class MediaActionSound(private val context: Context) { companion object { + val SHUTTER_CLICK = MediaSound.ManufacturerSound("camera_click.ogg") + val FOCUS_COMPLETE = MediaSound.ManufacturerSound("camera_focus.ogg") + val START_VIDEO_RECORDING = MediaSound.ManufacturerSound("VideoRecord.ogg") + val STOP_VIDEO_RECORDING = MediaSound.ManufacturerSound("VideoStop.ogg") + val TIMER_COUNTDOWN = MediaSound.RawResSound(R.raw.beep) + val TIMER_COUNTDOWN_2_SECONDS = MediaSound.RawResSound(R.raw.beep_2_secs) + 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 val SOUND_DIRS = arrayOf("/product/media/audio/ui/", "/system/media/audio/ui/") private const val TAG = "MediaActionSound" - const val SHUTTER_CLICK = 0 - const val FOCUS_COMPLETE = 1 - 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 val SOUNDS = arrayOf(SHUTTER_CLICK, FOCUS_COMPLETE, START_VIDEO_RECORDING, STOP_VIDEO_RECORDING, TIMER_COUNTDOWN, TIMER_COUNTDOWN_2_SECONDS) } - private class SoundState(val name: Int) { - var id = 0 // 0 is an invalid sample ID. + sealed class MediaSound { + class ManufacturerSound(val fileName: String, var path: String = "") : MediaSound() + class RawResSound(@RawRes val resId: Int) : MediaSound() + } + + private class SoundState( + val mediaSound: MediaSound, + // 0 is an invalid sample ID. + var loadId: Int = 0, + var streamId: Int = 0, 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 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 sounds = SOUNDS.map { SoundState(it) } + private val playTimeHandler = Handler(Looper.getMainLooper()) private val mLoadCompleteListener = SoundPool.OnLoadCompleteListener { _, sampleId, status -> - for (sound in mSounds) { - if (sound!!.id != sampleId) { + for (sound in sounds) { + if (sound.loadId != 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}") + sound.loadId = 0 + Log.e(TAG, "OnLoadCompleteListener() error: $status loading sound: ${sound.mediaSound}") return@OnLoadCompleteListener } when (sound.state) { @@ -76,11 +76,11 @@ class MediaActionSound(private val context: Context) { soundToBePlayed = sound sound.state = STATE_LOADED } - else -> Log.e(TAG, "OnLoadCompleteListener() called in wrong state: ${sound.state} for sound: ${sound.name}") + else -> Log.e(TAG, "OnLoadCompleteListener() called in wrong state: ${sound.state} for sound: ${sound.mediaSound}") } } if (soundToBePlayed != null) { - playSoundPool(soundToBePlayed!!) + playWithSoundPool(soundToBePlayed!!) } break } @@ -88,89 +88,113 @@ class MediaActionSound(private val context: Context) { 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 + private fun loadSound(sound: SoundState): Int { + var id = 0 + when (val mediaSound = sound.mediaSound) { + is MediaSound.ManufacturerSound -> { + for (soundDir in SOUND_DIRS) { + val soundPath = soundDir + mediaSound.fileName + mediaSound.path = soundPath + id = soundPool!!.load(soundPath, 1) + break + } + } + is MediaSound.RawResSound -> { + id = soundPool!!.load(context, mediaSound.resId, 1) } } + + if (id > 0) { + sound.state = STATE_LOADING + sound.loadId = 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!!) { + fun load(mediaSound: MediaSound) { + val sound = sounds.first { it.mediaSound == mediaSound } + synchronized(sound) { when (sound.state) { STATE_NOT_LOADED -> { loadSound(sound).let { soundId -> if (soundId <= 0) { - Log.e(TAG, "load() error loading sound: $soundName") + Log.e(TAG, "load() error loading sound: $mediaSound") } } } - else -> Log.e(TAG, "load() called in wrong state: $sound for sound: $soundName") + else -> Log.e(TAG, "load() called in wrong state: $sound for sound: $mediaSound") } } } - fun play(soundName: Int, onPlayComplete: (() -> Unit)? = null) { - if (soundName < 0 || soundName >= SOUND_FILES.size) { - throw RuntimeException("Unknown sound requested: $soundName") - } + fun play(mediaSound: MediaSound, onPlayComplete: (() -> Unit)? = null) { removeHandlerCallbacks() if (onPlayComplete != null) { playCompletionRunnable = Runnable { onPlayComplete.invoke() } } - val sound = mSounds[soundName] - synchronized(sound!!) { + val sound = sounds.first { it.mediaSound == mediaSound } + synchronized(sound) { when (sound.state) { STATE_NOT_LOADED -> { val soundId = loadSound(sound) if (soundId <= 0) { - Log.e(TAG, "play() error loading sound: $soundName") + Log.e(TAG, "play() error loading sound: $mediaSound") } else { sound.state = STATE_LOADING_PLAY_REQUESTED } } STATE_LOADING -> sound.state = STATE_LOADING_PLAY_REQUESTED STATE_LOADED -> { - playSoundPool(sound) + playWithSoundPool(sound) } - else -> Log.e(TAG, "play() called in wrong state: ${sound.state} for sound: $soundName") + else -> Log.e(TAG, "play() called in wrong state: ${sound.state} for sound: $mediaSound") } } } - private fun playSoundPool(sound: SoundState) { + private fun playWithSoundPool(sound: SoundState) { if (playCompletionRunnable != null) { - val duration = getSoundDuration(sound.path!!) + val duration = getSoundDuration(sound.mediaSound) playTimeHandler.postDelayed(playCompletionRunnable!!, duration) } - soundPool!!.play(sound.id, 1.0f, 1.0f, 0, 0, 1.0f) + val streamId = soundPool!!.play(sound.loadId, 1.0f, 1.0f, 0, 0, 1.0f) + sound.streamId = streamId + } + + private fun getSoundDuration(mediaSound: MediaSound): Long { + releaseMediaPlayer() + mediaPlayer = when (mediaSound) { + is MediaSound.ManufacturerSound -> MediaPlayer.create(context, Uri.fromFile(File(mediaSound.path))) + is MediaSound.RawResSound -> MediaPlayer.create(context, mediaSound.resId) + } + return mediaPlayer!!.duration.toLong() + } + + fun stop(mediaSound: MediaSound) { + val sound = sounds.first { it.mediaSound == mediaSound } + synchronized(sound) { + when (sound.state) { + STATE_LOADED -> { + soundPool!!.stop(sound.streamId) + } + else -> Log.w(TAG, "stop() should be called after sound is loaded for sound: $mediaSound") + } + } } fun release() { if (soundPool != null) { - for (sound in mSounds) { - synchronized(sound!!) { + for (sound in sounds) { + synchronized(sound) { sound.state = STATE_NOT_LOADED - sound.id = 0 + sound.loadId = 0 + sound.streamId = 0 } } soundPool?.release() @@ -189,10 +213,4 @@ class MediaActionSound(private val context: Context) { 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 1e32a822..c7ab0e8c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSoundHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSoundHelper.kt @@ -9,6 +9,8 @@ class MediaSoundHelper(context: Context) { mediaActionSound.load(MediaActionSound.START_VIDEO_RECORDING) mediaActionSound.load(MediaActionSound.STOP_VIDEO_RECORDING) mediaActionSound.load(MediaActionSound.SHUTTER_CLICK) + mediaActionSound.load(MediaActionSound.TIMER_COUNTDOWN) + mediaActionSound.load(MediaActionSound.TIMER_COUNTDOWN_2_SECONDS) } fun playShutterSound() { @@ -23,6 +25,18 @@ class MediaSoundHelper(context: Context) { mediaActionSound.play(MediaActionSound.STOP_VIDEO_RECORDING) } + fun playTimerCountdownSound() { + mediaActionSound.play(MediaActionSound.TIMER_COUNTDOWN) + } + + fun playTimerCountdown2SecondsSound() { + mediaActionSound.play(MediaActionSound.TIMER_COUNTDOWN_2_SECONDS) + } + + fun stopTimerCountdown2SecondsSound() { + mediaActionSound.stop(MediaActionSound.TIMER_COUNTDOWN_2_SECONDS) + } + fun release() { mediaActionSound.release() } 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 4a28c7f8..88b01e69 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXInitializer.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXInitializer.kt @@ -4,6 +4,7 @@ import android.net.Uri import androidx.camera.view.PreviewView import com.simplemobiletools.camera.helpers.CameraErrorHandler import com.simplemobiletools.camera.helpers.MediaOutputHelper +import com.simplemobiletools.camera.helpers.MediaSoundHelper import com.simplemobiletools.commons.activities.BaseSimpleActivity class CameraXInitializer(private val activity: BaseSimpleActivity) { @@ -11,6 +12,7 @@ class CameraXInitializer(private val activity: BaseSimpleActivity) { fun createCameraXPreview( previewView: PreviewView, listener: CameraXPreviewListener, + mediaSoundHelper: MediaSoundHelper, outputUri: Uri?, isThirdPartyIntent: Boolean, initInPhotoMode: Boolean, @@ -20,6 +22,7 @@ class CameraXInitializer(private val activity: BaseSimpleActivity) { return CameraXPreview( activity, previewView, + mediaSoundHelper, mediaOutputHelper, cameraErrorHandler, listener, 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 b9295223..23aef8b2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -37,6 +37,7 @@ import com.simplemobiletools.commons.helpers.ensureBackgroundThread class CameraXPreview( private val activity: AppCompatActivity, private val previewView: PreviewView, + private val mediaSoundHelper: MediaSoundHelper, private val mediaOutputHelper: MediaOutputHelper, private val cameraErrorHandler: CameraErrorHandler, private val listener: CameraXPreviewListener, @@ -49,14 +50,12 @@ class CameraXPreview( 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 const val TOGGLE_FLASH_DELAY = 700L } private val config = activity.config 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(activity) private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate() private val videoQualityManager = VideoQualityManager(activity) private val imageQualityManager = ImageQualityManager(activity) @@ -123,7 +122,6 @@ class CameraXPreview( init { bindToLifeCycle() - mediaSoundHelper.loadSounds() } private fun bindToLifeCycle() { @@ -354,11 +352,6 @@ class CameraXPreview( orientationEventListener.disable() } - override fun onDestroy(owner: LifecycleOwner) { - super.onDestroy(owner) - mediaSoundHelper.release() - } - override fun isInPhotoMode(): Boolean { return isPhotoCapture } diff --git a/app/src/main/res/raw/beep.mp3 b/app/src/main/res/raw/beep.mp3 new file mode 100644 index 00000000..a3c18102 Binary files /dev/null and b/app/src/main/res/raw/beep.mp3 differ diff --git a/app/src/main/res/raw/beep_2_secs.mp3 b/app/src/main/res/raw/beep_2_secs.mp3 new file mode 100644 index 00000000..9df77f1d Binary files /dev/null and b/app/src/main/res/raw/beep_2_secs.mp3 differ