From ef86e12902b95ca8e6c080835c9f6c43f74f7baf Mon Sep 17 00:00:00 2001 From: melad Date: Tue, 24 Sep 2024 07:12:07 +0300 Subject: [PATCH 1/3] [MS-720] Add Audio Notification for Finger Removal After Scanning --- feature/dashboard/src/main/res/values/ids.xml | 1 + .../src/main/res/xml/preference_general.xml | 6 ++ fingerprint/capture/build.gradle.kts | 1 + .../screen/FingerprintCaptureFragment.kt | 63 ++++++++++++++++--- .../FingerprintCaptureWrapperFactory.kt | 8 ++- .../capture/FingerprintCaptureWrapperV1.kt | 2 + .../capture/FingerprintCaptureWrapperV2.kt | 5 ++ .../FingerprintScanningStatusTracker.kt | 17 +++++ gradle/wrapper/gradle-wrapper.properties | 2 +- .../resources/src/main/res/values/strings.xml | 3 +- 10 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTracker.kt diff --git a/feature/dashboard/src/main/res/values/ids.xml b/feature/dashboard/src/main/res/values/ids.xml index 6ab8e37fb3..cca9a92677 100644 --- a/feature/dashboard/src/main/res/values/ids.xml +++ b/feature/dashboard/src/main/res/values/ids.xml @@ -11,6 +11,7 @@ select_fingers_preference_key sync_info_preference_key preference_update_config_key + preference_enable_audio_on_scan_complete_key app_details_key app_details_preference_key diff --git a/feature/dashboard/src/main/res/xml/preference_general.xml b/feature/dashboard/src/main/res/xml/preference_general.xml index e4d4e6c5d1..2c0880841c 100644 --- a/feature/dashboard/src/main/res/xml/preference_general.xml +++ b/feature/dashboard/src/main/res/xml/preference_general.xml @@ -23,6 +23,12 @@ android:key="@string/preference_update_config_key" android:summary="@string/dashboard_preference_summary_update_config" android:title="@string/dashboard_preference_update_config_title" /> + + vm.updateSelectedFinger(position) }, - isAbleToSelectNewFinger = { vm.stateLiveData.value?.currentCaptureState()?.isCommunicating() != true } + isAbleToSelectNewFinger = { + vm.stateLiveData.value?.currentCaptureState()?.isCommunicating() != true + } ) } @@ -185,12 +213,21 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri vm.stateLiveData.observe(viewLifecycleOwner) { state -> if (state != null) { // Update pager - fingerViewPagerManager.setCurrentPageAndFingerStates(state.fingerStates, state.currentFingerIndex) + fingerViewPagerManager.setCurrentPageAndFingerStates( + state.fingerStates, + state.currentFingerIndex + ) // Update button with(state.currentCaptureState()) { - binding.fingerprintScanButton.text = getString(buttonTextId(state.isAskingRescan)) - binding.fingerprintScanButton.setBackgroundColor(resources.getColor(buttonBackgroundColour(), null)) + binding.fingerprintScanButton.text = + getString(buttonTextId(state.isAskingRescan)) + binding.fingerprintScanButton.setBackgroundColor( + resources.getColor( + buttonBackgroundColour(), + null + ) + ) } updateConfirmDialog(state) @@ -198,7 +235,9 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri } } - vm.vibrate.observe(viewLifecycleOwner, LiveDataEventObserver { Vibrate.vibrate(requireContext()) }) + vm.vibrate.observe( + viewLifecycleOwner, + LiveDataEventObserver { Vibrate.vibrate(requireContext()) }) vm.noFingersScannedToast.observe(viewLifecycleOwner, LiveDataEventObserver { requireContext().showToast(IDR.string.fingerprint_capture_no_fingers_scanned) @@ -218,9 +257,11 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri }.toArgs() ) }) - vm.finishWithFingerprints.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { fingerprints -> - findNavController().finishWithResult(this, fingerprints) - }) + vm.finishWithFingerprints.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { fingerprints -> + findNavController().finishWithResult(this, fingerprints) + }) } private fun launchConnection() { @@ -290,4 +331,8 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri confirmDialog?.dismiss() super.onDestroyView() } + + companion object { + private const val AUDIO_PREFERENCE_KEY = "preference_enable_audio_on_scan_complete_key" + } } diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperFactory.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperFactory.kt index 4ad5710a8d..8f3457cfa1 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperFactory.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperFactory.kt @@ -13,6 +13,7 @@ import com.simprints.fingerprint.infra.scanner.v2.scanner.Scanner as ScannerV2 class FingerprintCaptureWrapperFactory @Inject constructor( @DispatcherIO private val ioDispatcher: CoroutineDispatcher, private val scannerUiHelper: ScannerUiHelper, + private val scanningStatusTracker: FingerprintScanningStatusTracker ) { private var _captureWrapper: FingerprintCaptureWrapper? = null @@ -20,10 +21,13 @@ class FingerprintCaptureWrapperFactory @Inject constructor( get() = _captureWrapper ?: throw NullScannerException() fun createV1(scannerV1: ScannerV1) { - _captureWrapper = FingerprintCaptureWrapperV1(scannerV1, ioDispatcher) + _captureWrapper = + FingerprintCaptureWrapperV1(scannerV1, ioDispatcher, scanningStatusTracker) } fun createV2(scannerV2: ScannerV2) { - _captureWrapper = FingerprintCaptureWrapperV2(scannerV2, scannerUiHelper, ioDispatcher) + _captureWrapper = FingerprintCaptureWrapperV2( + scannerV2, scannerUiHelper, ioDispatcher, scanningStatusTracker + ) } } diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1.kt index d5bf051dba..53a116e502 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1.kt @@ -23,6 +23,7 @@ import kotlin.coroutines.resumeWithException internal class FingerprintCaptureWrapperV1( private val scannerV1: Scanner, private val ioDispatcher: CoroutineDispatcher, + private val scanningStatusTracker: FingerprintScanningStatusTracker ) : FingerprintCaptureWrapper { override suspend fun acquireFingerprintImage(): AcquireFingerprintImageResponse { throw UnavailableVero2FeatureException(UnavailableVero2Feature.IMAGE_ACQUISITION) @@ -71,6 +72,7 @@ internal class FingerprintCaptureWrapperV1( scannerV1.imageQuality ) ) + scanningStatusTracker.notifyScanCompleted() }, failure = { if (it == SCANNER_ERROR.TIMEOUT) diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2.kt index 4fe099d743..5d5a88cff2 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2.kt @@ -24,6 +24,7 @@ internal class FingerprintCaptureWrapperV2( private val scannerV2: Scanner, private val scannerUiHelper: ScannerUiHelper, private val ioDispatcher: CoroutineDispatcher, + private val scanningStatusTracker: FingerprintScanningStatusTracker, ) : FingerprintCaptureWrapper { override suspend fun acquireImageDistortionMatrixConfiguration(): ByteArray = @@ -54,6 +55,7 @@ internal class FingerprintCaptureWrapperV2( } // Capture fingerprint and ensure it's OK scannerV2.captureFingerprint().ensureCaptureResultOkOrError().await() + scanningStatusTracker.notifyScanCompleted() // Transfer the unprocessed image from the scanner acquireUnprocessedImage().switchIfEmpty(Single.error(NoFingerDetectedException("Failed to acquire unprocessed image data"))) .wrapErrorsFromScanner().await() @@ -83,6 +85,9 @@ internal class FingerprintCaptureWrapperV2( scannerV2 .captureFingerprint(captureDpi) .ensureCaptureResultOkOrError() + .andThen(Completable.fromAction { + scanningStatusTracker.notifyScanCompleted() + }) .andThen(scannerV2.getImageQualityScore()) .switchIfEmpty(Single.error(NoFingerDetectedException("Failed to acquire image quality score"))) .setLedStateBasedOnQualityScoreOrInterpretAsNoFingerDetected(qualityThreshold) diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTracker.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTracker.kt new file mode 100644 index 0000000000..5511a92ce9 --- /dev/null +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTracker.kt @@ -0,0 +1,17 @@ +package com.simprints.fingerprint.infra.scanner.capture + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.simprints.core.livedata.LiveDataEvent +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FingerprintScanningStatusTracker @Inject constructor() { + private val _scanCompleted = MutableLiveData() + val scanCompleted: LiveData get() = _scanCompleted + + fun notifyScanCompleted() { + _scanCompleted.postValue(LiveDataEvent()) + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a6f6c00077..904fec0521 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Apr 17 08:02:27 EET 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/infra/resources/src/main/res/values/strings.xml b/infra/resources/src/main/res/values/strings.xml index 46929283ca..2399e1a760 100644 --- a/infra/resources/src/main/res/values/strings.xml +++ b/infra/resources/src/main/res/values/strings.xml @@ -371,9 +371,10 @@ View the fingers that will be scanned Further details on sync Refresh device and project configuration + Enable or disable audio alerts when a fingerprint scan is completed, notifying the user to remove their hand from the scanner. Copied to clipboard Configuration update started - + Audio on Fingerprint Scan Completion Sync Information From a80d3b6fcb1602d1b496a8d4d4316ee2f067b7ac Mon Sep 17 00:00:00 2001 From: melad Date: Tue, 24 Sep 2024 07:33:15 +0300 Subject: [PATCH 2/3] [MS-720] Add FingerprintScanningStatusTrackerTest --- .../screen/FingerprintCaptureFragment.kt | 35 +++++------------- fingerprint/capture/src/main/res/raw/beep.mp3 | Bin 0 -> 15932 bytes .../FingerprintCaptureWrapperFactoryTest.kt | 4 +- .../FingerprintCaptureWrapperV1Test.kt | 4 +- .../FingerprintCaptureWrapperV2Test.kt | 5 ++- .../FingerprintScanningStatusTrackerTest.kt | 34 +++++++++++++++++ 6 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 fingerprint/capture/src/main/res/raw/beep.mp3 create mode 100644 fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTrackerTest.kt diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt index 730cbd3551..7fcc9fd4b8 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt @@ -131,7 +131,6 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri .getDefaultSharedPreferences(requireContext()) .getBoolean(AUDIO_PREFERENCE_KEY, true) - private fun initUI() { initToolbar(args.params.flowType) initMissingFingerButton() @@ -164,8 +163,7 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri R.id.action_fingerprintCaptureFragment_to_graphExitForm, exitFormConfiguration { titleRes = com.simprints.infra.resources.R.string.exit_form_title_fingerprinting - backButtonRes = - com.simprints.infra.resources.R.string.exit_form_continue_fingerprints_button + backButtonRes = com.simprints.infra.resources.R.string.exit_form_continue_fingerprints_button visibleOptions = scannerOptions() }.toArgs() ) @@ -186,9 +184,7 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri binding.fingerprintViewPager, binding.fingerprintIndicator, onFingerSelected = { position -> vm.updateSelectedFinger(position) }, - isAbleToSelectNewFinger = { - vm.stateLiveData.value?.currentCaptureState()?.isCommunicating() != true - } + isAbleToSelectNewFinger = { vm.stateLiveData.value?.currentCaptureState()?.isCommunicating() != true } ) } @@ -213,21 +209,12 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri vm.stateLiveData.observe(viewLifecycleOwner) { state -> if (state != null) { // Update pager - fingerViewPagerManager.setCurrentPageAndFingerStates( - state.fingerStates, - state.currentFingerIndex - ) + fingerViewPagerManager.setCurrentPageAndFingerStates(state.fingerStates, state.currentFingerIndex) // Update button with(state.currentCaptureState()) { - binding.fingerprintScanButton.text = - getString(buttonTextId(state.isAskingRescan)) - binding.fingerprintScanButton.setBackgroundColor( - resources.getColor( - buttonBackgroundColour(), - null - ) - ) + binding.fingerprintScanButton.text = getString(buttonTextId(state.isAskingRescan)) + binding.fingerprintScanButton.setBackgroundColor(resources.getColor(buttonBackgroundColour(), null)) } updateConfirmDialog(state) @@ -235,9 +222,7 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri } } - vm.vibrate.observe( - viewLifecycleOwner, - LiveDataEventObserver { Vibrate.vibrate(requireContext()) }) + vm.vibrate.observe(viewLifecycleOwner, LiveDataEventObserver { Vibrate.vibrate(requireContext()) }) vm.noFingersScannedToast.observe(viewLifecycleOwner, LiveDataEventObserver { requireContext().showToast(IDR.string.fingerprint_capture_no_fingers_scanned) @@ -257,11 +242,9 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri }.toArgs() ) }) - vm.finishWithFingerprints.observe( - viewLifecycleOwner, - LiveDataEventWithContentObserver { fingerprints -> - findNavController().finishWithResult(this, fingerprints) - }) + vm.finishWithFingerprints.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { fingerprints -> + findNavController().finishWithResult(this, fingerprints) + }) } private fun launchConnection() { diff --git a/fingerprint/capture/src/main/res/raw/beep.mp3 b/fingerprint/capture/src/main/res/raw/beep.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..fdcaa16b7d3ae2975054f4001f323f9e2b392691 GIT binary patch literal 15932 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 = mockk(relaxed = true) + + @Before + fun setup() { + fingerprintScanningStatusTracker = FingerprintScanningStatusTracker() + fingerprintScanningStatusTracker.scanCompleted.observeForever(scanCompletedObserver) + } + + @Test + fun `notifyScanCompleted posts LiveDataEvent to scanCompleted`() { + // Act + fingerprintScanningStatusTracker.notifyScanCompleted() + + // Assert + verify { scanCompletedObserver.onChanged(any()) } + } +} From 08cd780dd714ecd161b95e75db4aaf6406f99079 Mon Sep 17 00:00:00 2001 From: melad Date: Mon, 30 Sep 2024 13:21:54 +0300 Subject: [PATCH 3/3] [MS-720] Use shared flow to emit scan complete events --- .../src/main/res/xml/preference_general.xml | 2 +- .../screen/FingerprintCaptureFragment.kt | 33 ++---- .../FingerprintScanCompletionAudioNotifier.kt | 43 ++++++++ ...gerprintScanCompletionAudioNotifierTest.kt | 100 ++++++++++++++++++ .../FingerprintScanningStatusTracker.kt | 16 +-- .../FingerprintScanningStatusTrackerTest.kt | 52 +++++---- 6 files changed, 191 insertions(+), 55 deletions(-) create mode 100644 fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintScanCompletionAudioNotifier.kt create mode 100644 fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintScanCompletionAudioNotifierTest.kt diff --git a/feature/dashboard/src/main/res/xml/preference_general.xml b/feature/dashboard/src/main/res/xml/preference_general.xml index 2c0880841c..20906d3c0c 100644 --- a/feature/dashboard/src/main/res/xml/preference_general.xml +++ b/feature/dashboard/src/main/res/xml/preference_general.xml @@ -25,7 +25,7 @@ android:title="@string/dashboard_preference_update_config_title" /> diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt index 7fcc9fd4b8..7d6b605763 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt @@ -1,7 +1,6 @@ package com.simprints.fingerprint.capture.screen import android.graphics.Paint -import android.media.MediaPlayer import android.os.Bundle import android.view.View import androidx.activity.addCallback @@ -9,9 +8,9 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import androidx.preference.PreferenceManager import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.response.AppErrorReason import com.simprints.core.livedata.LiveDataEventObserver @@ -38,7 +37,6 @@ import com.simprints.fingerprint.capture.views.fingerviewpager.FingerViewPagerMa import com.simprints.fingerprint.capture.views.tryagainsplash.TryAnotherFingerSplashDialogFragment import com.simprints.fingerprint.connect.FingerprintConnectContract import com.simprints.fingerprint.connect.FingerprintConnectResult -import com.simprints.fingerprint.infra.scanner.capture.FingerprintScanningStatusTracker import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload.AlertScreenEventType import com.simprints.infra.logging.LoggingConstants.CrashReportTag.FINGER_CAPTURE import com.simprints.infra.logging.Simber @@ -49,6 +47,7 @@ import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.system.Vibrate import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import java.io.Serializable import javax.inject.Inject import com.simprints.infra.resources.R as IDR @@ -65,7 +64,7 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri private var hasSplashScreenBeenTriggered: Boolean = false @Inject - lateinit var scanningStatusTracker: FingerprintScanningStatusTracker + lateinit var fingerprintScanCompletionAudioNotifier: FingerprintScanCompletionAudioNotifier override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -110,27 +109,12 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri args.params.fingerprintsToCapture, args.params.fingerprintSDK, ) - - scanningStatusTracker.scanCompleted.observe(viewLifecycleOwner, LiveDataEventObserver { - if (isAudioEnabled()) - playBeep() - }) - initUI() - } - - private fun playBeep() { - val mediaPlayer = MediaPlayer.create(context, R.raw.beep) - mediaPlayer.start() - mediaPlayer.setOnCompletionListener { - it.release() + lifecycleScope.launch { + fingerprintScanCompletionAudioNotifier.observeScanStatus() } + initUI() } - private fun isAudioEnabled() = - PreferenceManager - .getDefaultSharedPreferences(requireContext()) - .getBoolean(AUDIO_PREFERENCE_KEY, true) - private fun initUI() { initToolbar(args.params.flowType) initMissingFingerButton() @@ -312,10 +296,7 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri override fun onDestroyView() { confirmDialog?.dismiss() + fingerprintScanCompletionAudioNotifier.releaseMediaPlayer() super.onDestroyView() } - - companion object { - private const val AUDIO_PREFERENCE_KEY = "preference_enable_audio_on_scan_complete_key" - } } diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintScanCompletionAudioNotifier.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintScanCompletionAudioNotifier.kt new file mode 100644 index 0000000000..d17018f408 --- /dev/null +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintScanCompletionAudioNotifier.kt @@ -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" + } +} diff --git a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintScanCompletionAudioNotifierTest.kt b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintScanCompletionAudioNotifierTest.kt new file mode 100644 index 0000000000..3d5e929072 --- /dev/null +++ b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintScanCompletionAudioNotifierTest.kt @@ -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() + } +} diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTracker.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTracker.kt index 5511a92ce9..e840d5541d 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTracker.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTracker.kt @@ -1,17 +1,21 @@ package com.simprints.fingerprint.infra.scanner.capture -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.simprints.core.livedata.LiveDataEvent +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() { - private val _scanCompleted = MutableLiveData() - val scanCompleted: LiveData get() = _scanCompleted + private val _scanCompleted = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val scanCompleted: SharedFlow get() = _scanCompleted fun notifyScanCompleted() { - _scanCompleted.postValue(LiveDataEvent()) + _scanCompleted.tryEmit(Unit) } } diff --git a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTrackerTest.kt b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTrackerTest.kt index 87bf814dd0..f539dda0f9 100644 --- a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTrackerTest.kt +++ b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintScanningStatusTrackerTest.kt @@ -1,34 +1,42 @@ package com.simprints.fingerprint.infra.scanner.capture -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer -import com.simprints.core.livedata.LiveDataEvent -import io.mockk.mockk -import io.mockk.verify -import org.junit.Before -import org.junit.Rule +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() - @get:Rule - val rule = InstantTaskExecutorRule() - - private lateinit var fingerprintScanningStatusTracker: FingerprintScanningStatusTracker - private val scanCompletedObserver: Observer = mockk(relaxed = true) - - @Before - fun setup() { - fingerprintScanningStatusTracker = FingerprintScanningStatusTracker() - fingerprintScanningStatusTracker.scanCompleted.observeForever(scanCompletedObserver) + @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 `notifyScanCompleted posts LiveDataEvent to scanCompleted`() { - // Act - fingerprintScanningStatusTracker.notifyScanCompleted() + fun `test scanCompleted flow does not replay past emissions`() = runTest(testDispatcher) { + tracker.notifyScanCompleted() - // Assert - verify { scanCompletedObserver.onChanged(any()) } + var emitted = false + val job = launch { + tracker.scanCompleted.collect { + emitted = true + } + } + Truth.assertThat(emitted).isFalse() + job.cancel() } + }