-
Notifications
You must be signed in to change notification settings - Fork 2
Ms 720 add audio notification for finger removal after scanning #913
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ef86e12
97b9f54
a80d3b6
82e2263
08cd780
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.simprints.fingerprint.capture.screen | ||
|
|
||
| import android.content.Context | ||
| import android.media.MediaPlayer | ||
| import androidx.preference.PreferenceManager | ||
| import com.simprints.fingerprint.capture.R | ||
| import com.simprints.fingerprint.infra.scanner.capture.FingerprintScanningStatusTracker | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import javax.inject.Inject | ||
|
|
||
| class FingerprintScanCompletionAudioNotifier @Inject constructor( | ||
| @ApplicationContext private val context: Context, | ||
| private val scanningStatusTracker: FingerprintScanningStatusTracker, | ||
| ) { | ||
| private var mediaPlayer: MediaPlayer? = null | ||
|
|
||
| suspend fun observeScanStatus() { | ||
| scanningStatusTracker.scanCompleted.collect { | ||
| if (isAudioEnabled()) playBeep() | ||
| } | ||
| } | ||
|
|
||
| private fun playBeep() { | ||
| if (mediaPlayer == null) { | ||
| mediaPlayer = MediaPlayer.create(context, R.raw.beep) | ||
| } | ||
| mediaPlayer?.start() | ||
| } | ||
|
|
||
| private fun isAudioEnabled(): Boolean { | ||
| return PreferenceManager.getDefaultSharedPreferences(context) | ||
| .getBoolean(AUDIO_PREFERENCE_KEY, true) | ||
| } | ||
|
|
||
| fun releaseMediaPlayer() { | ||
| mediaPlayer?.release() | ||
| mediaPlayer = null | ||
| } | ||
|
|
||
| companion object { | ||
| private const val AUDIO_PREFERENCE_KEY = "preference_enable_audio_on_scan_complete_key" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| package com.simprints.fingerprint.capture.screen | ||
|
|
||
| import android.content.Context | ||
| import android.content.SharedPreferences | ||
| import android.media.MediaPlayer | ||
| import androidx.preference.PreferenceManager | ||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import com.simprints.fingerprint.capture.R | ||
| import com.simprints.fingerprint.infra.scanner.capture.FingerprintScanningStatusTracker | ||
| import io.mockk.MockKAnnotations | ||
| import io.mockk.every | ||
| import io.mockk.impl.annotations.MockK | ||
| import io.mockk.impl.annotations.RelaxedMockK | ||
| import io.mockk.mockkStatic | ||
| import io.mockk.verify | ||
| import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.test.UnconfinedTestDispatcher | ||
| import kotlinx.coroutines.test.runTest | ||
| import org.junit.Before | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
|
|
||
| @OptIn(ExperimentalCoroutinesApi::class) | ||
| @RunWith(AndroidJUnit4::class) | ||
| class FingerprintScanCompletionAudioNotifierTest { | ||
|
|
||
| @MockK | ||
| private lateinit var context: Context | ||
| private lateinit var scanningStatusTracker: FingerprintScanningStatusTracker | ||
|
|
||
| @RelaxedMockK | ||
| private lateinit var mediaPlayer: MediaPlayer | ||
|
|
||
| @MockK | ||
| private lateinit var sharedPreferences: SharedPreferences | ||
|
|
||
| private lateinit var notifier: FingerprintScanCompletionAudioNotifier | ||
| private val testDispatcher = UnconfinedTestDispatcher() | ||
|
|
||
| @Before | ||
| fun setup() { | ||
| MockKAnnotations.init(this) | ||
| mockkStatic(PreferenceManager::class) | ||
| mockkStatic(MediaPlayer::class) | ||
| scanningStatusTracker = FingerprintScanningStatusTracker() | ||
| every { PreferenceManager.getDefaultSharedPreferences(context) } returns sharedPreferences | ||
| every { MediaPlayer.create(context, R.raw.beep) } returns mediaPlayer | ||
|
|
||
| notifier = FingerprintScanCompletionAudioNotifier(context, scanningStatusTracker) | ||
| } | ||
|
|
||
| @Test | ||
| fun `playBeep should be called when scan completes and audio is enabled`() = | ||
| runTest(testDispatcher) { | ||
| // Given | ||
| every { sharedPreferences.getBoolean(any(), any()) } returns true | ||
|
|
||
| // When | ||
| val job = launch { notifier.observeScanStatus() } | ||
| scanningStatusTracker.notifyScanCompleted() | ||
|
|
||
| // Then | ||
| verify { mediaPlayer.start() } | ||
| job.cancel() | ||
|
|
||
| } | ||
|
|
||
| @Test | ||
| fun `playBeep should not be called when scan completes and audio is disabled`() = | ||
| runTest(testDispatcher) { | ||
| // Given | ||
| every { sharedPreferences.getBoolean(any(), any()) } returns false | ||
|
|
||
| // When | ||
| val job = launch { notifier.observeScanStatus() } | ||
| scanningStatusTracker.notifyScanCompleted() | ||
|
|
||
| // Then | ||
| verify(exactly = 0) { mediaPlayer.start() } | ||
| job.cancel() | ||
|
|
||
| } | ||
|
|
||
| @Test | ||
| fun `releaseMediaPlayer should release the media player`() = runTest(testDispatcher) { | ||
| //Given | ||
| every { sharedPreferences.getBoolean(any(), any()) } returns true | ||
|
|
||
| // When | ||
| val job = launch { notifier.observeScanStatus() } | ||
| scanningStatusTracker.notifyScanCompleted() | ||
| notifier.releaseMediaPlayer() | ||
|
|
||
| // Then | ||
| verify { mediaPlayer.start() } | ||
| verify { mediaPlayer.release() } | ||
| job.cancel() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.simprints.fingerprint.infra.scanner.capture | ||
|
|
||
| import kotlinx.coroutines.channels.BufferOverflow | ||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||
| import kotlinx.coroutines.flow.SharedFlow | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| class FingerprintScanningStatusTracker @Inject constructor() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit of a nitpick, but from the name and usage in the code above, I expected this to handle multiple status values. IMO, this class should either handle all the status changes (at least have an easy way to extend to do so) or have a less generic name.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @luhmirin-s in this story I will introduce the idle success and failure flows |
||
| private val _scanCompleted = MutableSharedFlow<Unit>( | ||
| replay = 0, | ||
| extraBufferCapacity = 1, | ||
| onBufferOverflow = BufferOverflow.DROP_OLDEST | ||
| ) | ||
| val scanCompleted: SharedFlow<Unit> get() = _scanCompleted | ||
|
|
||
| fun notifyScanCompleted() { | ||
| _scanCompleted.tryEmit(Unit) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.simprints.fingerprint.infra.scanner.capture | ||
|
|
||
| import com.google.common.truth.Truth | ||
| import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.test.UnconfinedTestDispatcher | ||
| import kotlinx.coroutines.test.runTest | ||
| import org.junit.Test | ||
|
|
||
| @OptIn(ExperimentalCoroutinesApi::class) | ||
| class FingerprintScanningStatusTrackerTest { | ||
| private val tracker = FingerprintScanningStatusTracker() | ||
| private val testDispatcher = UnconfinedTestDispatcher() | ||
|
|
||
| @Test | ||
| fun `test notifyScanCompleted emits Unit`() = runTest(testDispatcher) { | ||
| var emitted = false | ||
| val job = launch { | ||
| tracker.scanCompleted.collect { | ||
| emitted = true | ||
| } | ||
| } | ||
| tracker.notifyScanCompleted() | ||
| Truth.assertThat(emitted).isTrue() | ||
| job.cancel() | ||
| } | ||
|
|
||
| @Test | ||
| fun `test scanCompleted flow does not replay past emissions`() = runTest(testDispatcher) { | ||
| tracker.notifyScanCompleted() | ||
|
|
||
| var emitted = false | ||
| val job = launch { | ||
| tracker.scanCompleted.collect { | ||
| emitted = true | ||
| } | ||
| } | ||
| Truth.assertThat(emitted).isFalse() | ||
| job.cancel() | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need preferences UI in this module?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To access the PrefsManger class as it is part of the androidx prefs lib
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to decouple the actual setting from the UI?