Skip to content

Commit 4d59810

Browse files
committed
Deal with stranded plain-text video files
This could be due to a crash
1 parent 4db7d24 commit 4d59810

File tree

5 files changed

+95
-23
lines changed

5 files changed

+95
-23
lines changed

app/src/main/kotlin/com/darkrockstudios/app/securecamera/MainActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.lifecycle.repeatOnLifecycle
1616
import androidx.navigation3.runtime.NavKey
1717
import androidx.navigation3.runtime.rememberNavBackStack
1818
import com.darkrockstudios.app.securecamera.auth.AuthorizationRepository
19+
import com.darkrockstudios.app.securecamera.encryption.VideoEncryptionService
1920
import com.darkrockstudios.app.securecamera.navigation.*
2021
import com.darkrockstudios.app.securecamera.navigation.Camera
2122
import com.darkrockstudios.app.securecamera.preferences.AppSettingsDataSource
@@ -68,6 +69,9 @@ class MainActivity : ComponentActivity() {
6869
Camera
6970
}
7071
if (authorizationRepository.checkSessionValidity()) {
72+
// Session is valid - recover any stranded temp video files immediately
73+
// This ensures unencrypted videos are encrypted ASAP
74+
VideoEncryptionService.recoverStrandedFiles(this)
7175
targetKey
7276
} else {
7377
PinVerification(targetKey)

app/src/main/kotlin/com/darkrockstudios/app/securecamera/auth/PinVerificationViewModel.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
55
import androidx.navigation3.runtime.NavKey
66
import com.darkrockstudios.app.securecamera.BaseViewModel
77
import com.darkrockstudios.app.securecamera.R
8+
import com.darkrockstudios.app.securecamera.encryption.VideoEncryptionService
89
import com.darkrockstudios.app.securecamera.gallery.vibrateDevice
910
import com.darkrockstudios.app.securecamera.navigation.Introduction
1011
import com.darkrockstudios.app.securecamera.usecases.InvalidateSessionUseCase
@@ -110,6 +111,10 @@ class PinVerificationViewModel(
110111
val isValid = verifyPinUseCase.verifyPin(pin)
111112

112113
if (isValid) {
114+
// Recover any stranded temp video files immediately after auth
115+
// This ensures unencrypted videos are encrypted ASAP
116+
VideoEncryptionService.recoverStrandedFiles(appContext)
117+
113118
withContext(Dispatchers.Main) {
114119
_uiState.update {
115120
it.copy(

app/src/main/kotlin/com/darkrockstudios/app/securecamera/encryption/VideoEncryptionService.kt

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.core.app.NotificationCompat
1111
import com.darkrockstudios.app.securecamera.MainActivity
1212
import com.darkrockstudios.app.securecamera.R
1313
import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
14+
import com.darkrockstudios.app.securecamera.security.streaming.SecvFileFormat
1415
import com.darkrockstudios.app.securecamera.security.streaming.VideoEncryptionHelper
1516
import kotlinx.coroutines.*
1617
import kotlinx.coroutines.flow.MutableStateFlow
@@ -121,6 +122,71 @@ class VideoEncryptionService : Service() {
121122
}
122123
context.startService(intent)
123124
}
125+
126+
/**
127+
* Recover stranded temp files that were left behind due to a crash.
128+
* This should be called after user authentication to ensure any unencrypted
129+
* video files are immediately encrypted.
130+
*
131+
* Handles:
132+
* - temp_*.mp4 files (unencrypted recordings)
133+
* - *.secv.encrypting files (partial encryptions - deleted)
134+
*/
135+
fun recoverStrandedFiles(context: Context) {
136+
val videosDir = File(context.filesDir, "videos")
137+
if (!videosDir.exists()) {
138+
Timber.d("Videos directory doesn't exist, nothing to recover")
139+
return
140+
}
141+
142+
// Find and delete any partial .encrypting files
143+
val encryptingFiles = videosDir.listFiles { file ->
144+
file.name.endsWith(VideoEncryptionHelper.ENCRYPTING_SUFFIX)
145+
} ?: emptyArray()
146+
147+
encryptingFiles.forEach { file ->
148+
Timber.w("Deleting partial encryption file: ${file.name}")
149+
file.delete()
150+
}
151+
152+
// Find stranded temp files
153+
val tempFiles = videosDir.listFiles { file ->
154+
file.name.startsWith("temp_") && file.name.endsWith(".mp4")
155+
} ?: emptyArray()
156+
157+
if (tempFiles.isEmpty()) {
158+
Timber.d("No stranded temp files found")
159+
return
160+
}
161+
162+
Timber.w("Found ${tempFiles.size} stranded temp file(s), recovering...")
163+
164+
// Get set of files currently being processed
165+
val currentlyProcessing = _encryptionState.value.keys
166+
167+
tempFiles.forEach { tempFile ->
168+
// Derive the expected output file name
169+
val outputName = tempFile.name
170+
.replace("temp_", "video_")
171+
.replace(".mp4", ".${SecvFileFormat.FILE_EXTENSION}")
172+
val outputFile = File(videosDir, outputName)
173+
174+
// Skip if already being processed
175+
if (currentlyProcessing.contains(outputName)) {
176+
Timber.d("Skipping ${tempFile.name} - already being processed")
177+
return@forEach
178+
}
179+
180+
// Delete any existing partial output (shouldn't exist if .encrypting is used, but safety check)
181+
if (outputFile.exists()) {
182+
Timber.w("Deleting existing partial output: ${outputFile.name}")
183+
outputFile.delete()
184+
}
185+
186+
Timber.i("Recovering stranded video: ${tempFile.name} -> ${outputFile.name}")
187+
enqueueEncryption(context, tempFile, outputFile)
188+
}
189+
}
124190
}
125191

126192
override fun onCreate() {
@@ -299,7 +365,6 @@ class VideoEncryptionService : Service() {
299365
if (job.tempFile.exists()) {
300366
job.tempFile.delete()
301367
}
302-
showCompletionNotification()
303368
}
304369

305370
private fun handleJobCancelled(job: VideoEncryptionJob) {
@@ -436,20 +501,6 @@ class VideoEncryptionService : Service() {
436501
}
437502
}
438503

439-
private fun showCompletionNotification() {
440-
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
441-
.setSmallIcon(android.R.drawable.ic_menu_save)
442-
.setContentTitle(getString(R.string.encryption_complete_title))
443-
.setContentText(getString(R.string.encryption_complete_content))
444-
.setPriority(NotificationCompat.PRIORITY_LOW)
445-
.setAutoCancel(true)
446-
.setContentIntent(createContentPendingIntent())
447-
448-
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
449-
// Use a different notification ID for completion so it doesn't interfere with ongoing
450-
notificationManager.notify(NOTIFICATION_ID + 100, builder.build())
451-
}
452-
453504
private fun showErrorNotification(message: String) {
454505
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
455506
.setSmallIcon(android.R.drawable.ic_dialog_alert)

app/src/main/kotlin/com/darkrockstudios/app/securecamera/security/streaming/VideoEncryptionHelper.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class VideoEncryptionHelper(
3535
chunkSize: Int = SecvFileFormat.DEFAULT_CHUNK_SIZE,
3636
isCancelled: () -> Boolean = { false }
3737
): Boolean = withContext(Dispatchers.IO) {
38+
val encryptingFile = File(outputFile.absolutePath + ENCRYPTING_SUFFIX)
39+
3840
try {
3941
_encryptionProgress.value = EncryptionProgress.Starting
4042

@@ -50,7 +52,7 @@ class VideoEncryptionHelper(
5052
_encryptionProgress.value = EncryptionProgress.InProgress(0f)
5153

5254
// Create the streaming encryptor
53-
val encryptor = streamingScheme.createStreamingEncryptor(outputFile, chunkSize)
55+
val encryptor = streamingScheme.createStreamingEncryptor(encryptingFile, chunkSize)
5456

5557
try {
5658
// Read the temp file in chunks and write to encryptor
@@ -82,13 +84,21 @@ class VideoEncryptionHelper(
8284
encryptor.close()
8385
}
8486

85-
// Verify the output file was created
86-
if (!outputFile.exists() || outputFile.length() == 0L) {
87+
// Verify the encrypting file was created
88+
if (!encryptingFile.exists() || encryptingFile.length() == 0L) {
8789
Timber.e("Encrypted output file is empty or missing")
8890
_encryptionProgress.value = EncryptionProgress.Error("Encryption failed")
8991
return@withContext false
9092
}
9193

94+
// Rename .encrypting to final .secv
95+
if (!encryptingFile.renameTo(outputFile)) {
96+
Timber.e("Failed to rename encrypting file to final output")
97+
_encryptionProgress.value = EncryptionProgress.Error("Failed to finalize encryption")
98+
encryptingFile.delete()
99+
return@withContext false
100+
}
101+
92102
tempFile.delete()
93103

94104
_encryptionProgress.value = EncryptionProgress.Completed
@@ -99,15 +109,19 @@ class VideoEncryptionHelper(
99109
Timber.e(e, "Failed to encrypt video file")
100110
_encryptionProgress.value = EncryptionProgress.Error(e.message ?: "Unknown error")
101111

102-
// Clean up partial output file on error
103-
if (outputFile.exists()) {
104-
outputFile.delete()
112+
// Clean up partial encrypting file on error
113+
if (encryptingFile.exists()) {
114+
encryptingFile.delete()
105115
}
106116

107117
return@withContext false
108118
}
109119
}
110120

121+
companion object {
122+
const val ENCRYPTING_SUFFIX = ".encrypting"
123+
}
124+
111125
fun resetProgress() {
112126
_encryptionProgress.value = EncryptionProgress.Idle
113127
}

app/src/main/res/values/strings.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,6 @@
111111
<string name="encryption_notification_preparing">Preparing…</string>
112112
<string name="encryption_notification_progress">Encrypting: %1$d%%</string>
113113
<string name="encryption_notification_queue">%1$d of %2$d - %3$d%%</string>
114-
<string name="encryption_complete_title">Encryption Complete</string>
115-
<string name="encryption_complete_content">Video saved securely</string>
116114
<string name="encryption_error_title">Encryption Failed</string>
117115
<string name="encryption_cancelled_title">Encryption Cancelled</string>
118116
<string name="encryption_action_cancel">Cancel</string>

0 commit comments

Comments
 (0)