11package com.dooboolab.audiorecorderplayer
22
33import android.Manifest
4+ import android.content.Context
5+ import android.content.SharedPreferences
46import android.content.pm.PackageManager
57import android.media.MediaPlayer
68import android.media.MediaRecorder
@@ -12,13 +14,15 @@ import android.os.SystemClock
1214import android.util.Log
1315import androidx.core.app.ActivityCompat
1416import com.facebook.react.bridge.*
17+ import com.facebook.react.bridge.LifecycleEventListener
1518import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
1619import com.facebook.react.modules.core.PermissionListener
20+ import java.io.File
1721import java.io.IOException
1822import java.util.*
1923import kotlin.math.log10
2024
21- class RNAudioRecorderPlayerModule (private val reactContext : ReactApplicationContext ) : ReactContextBaseJavaModule(reactContext), PermissionListener {
25+ class RNAudioRecorderPlayerModule (private val reactContext : ReactApplicationContext ) : ReactContextBaseJavaModule(reactContext), PermissionListener, LifecycleEventListener {
2226 private var audioFileURL = " "
2327 private var subsDurationMillis = 500
2428 private var _meteringEnabled = false
@@ -29,7 +33,63 @@ class RNAudioRecorderPlayerModule(private val reactContext: ReactApplicationCont
2933 private var mTimer: Timer ? = null
3034 private var pausedRecordTime = 0L
3135 private var totalPausedRecordTime = 0L
36+ private var recordStartTime = 0L
37+ private var accumulatedRecordTime = 0L
38+ private var isRecordingActive = false
39+ private val audioSegmentURLs = mutableListOf<String >()
3240 var recordHandler: Handler ? = Handler (Looper .getMainLooper())
41+
42+ // SharedPreferences keys for session recovery
43+ private val PREFS_NAME = " RNAudioRecorderPrefs"
44+ private val KEY_SEGMENT_URLS = " segmentUrls"
45+ private val KEY_ACCUMULATED_TIME = " accumulatedTime"
46+ private val KEY_IS_RECORDING = " isRecording"
47+ private val KEY_IS_PAUSED = " isPaused"
48+ private val KEY_START_TIME = " startTime"
49+
50+ private fun getSharedPreferences (): SharedPreferences {
51+ return reactContext.getSharedPreferences(PREFS_NAME , Context .MODE_PRIVATE )
52+ }
53+
54+ init {
55+ // Register lifecycle listener to handle app termination
56+ reactContext.addLifecycleEventListener(this )
57+ }
58+
59+ private fun saveRecoveryState () {
60+ val prefs = getSharedPreferences()
61+ val editor = prefs.edit()
62+
63+ // Save segment URLs as comma-separated string
64+ editor.putString(KEY_SEGMENT_URLS , audioSegmentURLs.joinToString(" ," ))
65+
66+ // Save timing information
67+ editor.putLong(KEY_ACCUMULATED_TIME , accumulatedRecordTime)
68+
69+ // Save recording state
70+ editor.putBoolean(KEY_IS_RECORDING , mediaRecorder != null )
71+ editor.putBoolean(KEY_IS_PAUSED , ! isRecordingActive && mediaRecorder != null )
72+
73+ // Save start time if recording is active
74+ if (isRecordingActive && recordStartTime > 0 ) {
75+ editor.putLong(KEY_START_TIME , recordStartTime)
76+ } else {
77+ editor.remove(KEY_START_TIME )
78+ }
79+
80+ editor.commit() // Use commit() instead of apply() for immediate saving
81+ }
82+
83+ private fun clearRecoveryState () {
84+ val prefs = getSharedPreferences()
85+ val editor = prefs.edit()
86+ editor.remove(KEY_SEGMENT_URLS )
87+ editor.remove(KEY_ACCUMULATED_TIME )
88+ editor.remove(KEY_IS_RECORDING )
89+ editor.remove(KEY_IS_PAUSED )
90+ editor.remove(KEY_START_TIME )
91+ editor.apply ()
92+ }
3393 override fun getName (): String {
3494 return tag
3595 }
@@ -92,6 +152,12 @@ class RNAudioRecorderPlayerModule(private val reactContext: ReactApplicationCont
92152 MediaRecorder ()
93153 }
94154
155+ // Clear any previous recovery state when starting new recording
156+ audioSegmentURLs.clear()
157+ audioSegmentURLs.add(audioFileURL)
158+ accumulatedRecordTime = 0L
159+ clearRecoveryState()
160+
95161 try {
96162 if (audioSet == null ) {
97163 newMediaRecorder.setAudioSource(MediaRecorder .AudioSource .MIC )
@@ -122,6 +188,8 @@ class RNAudioRecorderPlayerModule(private val reactContext: ReactApplicationCont
122188
123189 mediaRecorder = newMediaRecorder
124190
191+ recordStartTime = SystemClock .elapsedRealtime()
192+ isRecordingActive = true
125193 val systemTime = SystemClock .elapsedRealtime()
126194 recorderRunnable = object : Runnable {
127195 override fun run () {
@@ -165,6 +233,8 @@ class RNAudioRecorderPlayerModule(private val reactContext: ReactApplicationCont
165233 try {
166234 mediaRecorder!! .resume()
167235 totalPausedRecordTime + = SystemClock .elapsedRealtime() - pausedRecordTime;
236+ isRecordingActive = true
237+ recordStartTime = SystemClock .elapsedRealtime()
168238 recorderRunnable?.let { recordHandler!! .postDelayed(it, subsDurationMillis.toLong()) }
169239 promise.resolve(" Recorder resumed." )
170240 } catch (e: Exception ) {
@@ -183,6 +253,14 @@ class RNAudioRecorderPlayerModule(private val reactContext: ReactApplicationCont
183253 try {
184254 mediaRecorder!! .pause()
185255 pausedRecordTime = SystemClock .elapsedRealtime();
256+
257+ // Update timing before pausing
258+ if (isRecordingActive && recordStartTime > 0 ) {
259+ accumulatedRecordTime + = SystemClock .elapsedRealtime() - recordStartTime
260+ recordStartTime = 0
261+ isRecordingActive = false
262+ }
263+
186264 recorderRunnable?.let { recordHandler!! .removeCallbacks(it) };
187265 promise.resolve(" Recorder paused." )
188266 } catch (e: Exception ) {
@@ -214,6 +292,13 @@ class RNAudioRecorderPlayerModule(private val reactContext: ReactApplicationCont
214292 }
215293
216294 try {
295+ // Update timing one final time before stopping
296+ if (isRecordingActive && recordStartTime > 0 ) {
297+ accumulatedRecordTime + = SystemClock .elapsedRealtime() - recordStartTime
298+ recordStartTime = 0
299+ isRecordingActive = false
300+ }
301+
217302 mediaRecorder!! .stop()
218303 mediaRecorder!! .release()
219304 mediaRecorder = null
@@ -232,6 +317,10 @@ class RNAudioRecorderPlayerModule(private val reactContext: ReactApplicationCont
232317 return
233318 }
234319
320+ // Clear recovery state after successful stop
321+ clearRecoveryState()
322+ audioSegmentURLs.clear()
323+
235324 Log .d(tag, " Recording completed successfully: ${audioFile.length()} bytes" )
236325 promise.resolve(" file:///$audioFileURL " )
237326 } catch (stopException: RuntimeException ) {
@@ -448,7 +537,145 @@ class RNAudioRecorderPlayerModule(private val reactContext: ReactApplicationCont
448537 subsDurationMillis = (sec * 1000 ).toInt()
449538 promise.resolve(" setSubscriptionDuration: $subsDurationMillis " )
450539 }
540+
541+ @ReactMethod
542+ fun hasRecoverableSession (promise : Promise ) {
543+ val prefs = getSharedPreferences()
544+ val segmentUrlsString = prefs.getString(KEY_SEGMENT_URLS , " " )
545+
546+ if (! segmentUrlsString.isNullOrEmpty()) {
547+ val segmentUrls = segmentUrlsString.split(" ," ).filter { it.isNotEmpty() }
548+
549+ // Verify at least one segment file still exists
550+ val validSegments = segmentUrls.filter { path ->
551+ File (path).exists()
552+ }
553+
554+ if (validSegments.isNotEmpty()) {
555+ val result = Arguments .createMap()
556+ result.putBoolean(" hasSession" , true )
557+ result.putInt(" segmentCount" , validSegments.size)
558+ result.putDouble(" accumulatedTime" , prefs.getLong(KEY_ACCUMULATED_TIME , 0L ).toDouble())
559+ result.putBoolean(" wasRecording" , prefs.getBoolean(KEY_IS_RECORDING , false ))
560+ result.putBoolean(" wasPaused" , prefs.getBoolean(KEY_IS_PAUSED , false ))
561+ promise.resolve(result)
562+ } else {
563+ // No valid segments found, clear recovery state
564+ clearRecoveryState()
565+ val result = Arguments .createMap()
566+ result.putBoolean(" hasSession" , false )
567+ promise.resolve(result)
568+ }
569+ } else {
570+ val result = Arguments .createMap()
571+ result.putBoolean(" hasSession" , false )
572+ promise.resolve(result)
573+ }
574+ }
575+
576+ @ReactMethod
577+ fun recoverSession (promise : Promise ) {
578+ val prefs = getSharedPreferences()
579+ val segmentUrlsString = prefs.getString(KEY_SEGMENT_URLS , " " )
580+
581+ if (segmentUrlsString.isNullOrEmpty()) {
582+ promise.reject(" NO_SESSION" , " No recoverable session found" )
583+ return
584+ }
585+
586+ val segmentUrls = segmentUrlsString.split(" ," ).filter { it.isNotEmpty() }
587+
588+ // Verify files exist
589+ val validSegments = segmentUrls.filter { path ->
590+ File (path).exists()
591+ }
592+
593+ if (validSegments.isEmpty()) {
594+ clearRecoveryState()
595+ promise.reject(" NO_VALID_SEGMENTS" , " No valid audio segments found" )
596+ return
597+ }
598+
599+ // Restore segment URLs
600+ audioSegmentURLs.clear()
601+ audioSegmentURLs.addAll(validSegments)
602+
603+ // Restore timing information
604+ accumulatedRecordTime = prefs.getLong(KEY_ACCUMULATED_TIME , 0L )
605+
606+ // Restore recording state flags
607+ val wasRecording = prefs.getBoolean(KEY_IS_RECORDING , false )
608+ val wasPaused = prefs.getBoolean(KEY_IS_PAUSED , false )
609+
610+ // Return comma-separated segment paths and state info
611+ val result = Arguments .createMap()
612+ result.putString(" segments" , validSegments.map { " file:///$it " }.joinToString(" ," ))
613+ result.putDouble(" accumulatedTime" , accumulatedRecordTime.toDouble())
614+ result.putBoolean(" wasRecording" , wasRecording)
615+ result.putBoolean(" wasPaused" , wasPaused)
616+
617+ // Clear the recovery state after successful recovery
618+ clearRecoveryState()
619+
620+ promise.resolve(result)
621+ }
622+
623+ @ReactMethod
624+ fun clearRecoverySession (promise : Promise ) {
625+ clearRecoveryState()
626+ promise.resolve(" Recovery session cleared" )
627+ }
451628
629+ // Lifecycle event handlers for saving state on termination
630+ override fun onHostResume () {
631+ // App resumed from background
632+ // We don't need to do anything here since recording continues in background
633+ }
634+
635+ override fun onHostPause () {
636+ // App is going to background or being terminated
637+ // Save state quickly before potential termination
638+ if (mediaRecorder != null ) {
639+ // Update timing if recording
640+ if (isRecordingActive && recordStartTime > 0 ) {
641+ val currentTime = SystemClock .elapsedRealtime()
642+ accumulatedRecordTime + = currentTime - recordStartTime
643+ // Don't reset recordStartTime here in case it's just backgrounding
644+ }
645+
646+ // Add current audio file to segments if not already added
647+ if (audioFileURL.isNotEmpty() && ! audioSegmentURLs.contains(audioFileURL)) {
648+ audioSegmentURLs.add(audioFileURL)
649+ }
650+
651+ saveRecoveryState()
652+ }
653+ }
654+
655+ override fun onHostDestroy () {
656+ // App is being destroyed
657+ // Final save before termination
658+ if (mediaRecorder != null ) {
659+ // Final timing update
660+ if (isRecordingActive && recordStartTime > 0 ) {
661+ val currentTime = SystemClock .elapsedRealtime()
662+ accumulatedRecordTime + = currentTime - recordStartTime
663+ recordStartTime = 0
664+ isRecordingActive = false
665+ }
666+
667+ // Ensure current segment is saved
668+ if (audioFileURL.isNotEmpty() && ! audioSegmentURLs.contains(audioFileURL)) {
669+ audioSegmentURLs.add(audioFileURL)
670+ }
671+
672+ saveRecoveryState()
673+ }
674+
675+ // Unregister lifecycle listener
676+ reactContext.removeLifecycleEventListener(this )
677+ }
678+
452679 override fun onRequestPermissionsResult (requestCode : Int , permissions : Array <String >, grantResults : IntArray ): Boolean {
453680 var requestRecordAudioPermission: Int = 200
454681
0 commit comments