Skip to content

Commit 4db7d24

Browse files
committed
Encrypt videos in a foreground service
1 parent 837ffe8 commit 4db7d24

9 files changed

Lines changed: 744 additions & 152 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
1313
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
1414
<uses-permission android:name="android.permission.VIBRATE"/>
15+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
16+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
1517

1618
<!-- Prevent dependencies from adding these permissions -->
1719
<uses-permission android:name="android.permission.INTERNET" tools:node="remove"/>
@@ -74,9 +76,18 @@
7476
android:name=".auth.SessionService"
7577
android:exported="false"/>
7678

79+
<service
80+
android:name=".encryption.VideoEncryptionService"
81+
android:exported="false"
82+
android:foregroundServiceType="dataSync"/>
83+
7784
<receiver
7885
android:name=".import.ImportCancelReceiver"
7986
android:exported="false"/>
87+
88+
<receiver
89+
android:name=".encryption.VideoEncryptionCancelReceiver"
90+
android:exported="false"/>
8091
</application>
8192

8293
</manifest>

app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/CameraState.kt

Lines changed: 11 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ import androidx.compose.ui.unit.IntSize
1212
import androidx.core.content.ContextCompat
1313
import androidx.lifecycle.LifecycleOwner
1414
import androidx.lifecycle.coroutineScope
15+
import com.darkrockstudios.app.securecamera.encryption.VideoEncryptionService
1516
import com.darkrockstudios.app.securecamera.obfuscation.FacialDetection
1617
import com.darkrockstudios.app.securecamera.preferences.AppSettingsDataSource
17-
import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
1818
import com.darkrockstudios.app.securecamera.security.streaming.SecvFileFormat
19-
import com.darkrockstudios.app.securecamera.security.streaming.VideoEncryptionHelper
2019
import kotlinx.coroutines.*
21-
import kotlinx.coroutines.flow.collectLatest
2220
import org.koin.core.component.KoinComponent
2321
import org.koin.core.component.inject
2422
import timber.log.Timber
@@ -47,10 +45,8 @@ class CameraState internal constructor(
4745
private val clock: Clock by inject()
4846
private val facialDetection: FacialDetection by inject()
4947
private val preferences: AppSettingsDataSource by inject()
50-
private val encryptionScheme: EncryptionScheme by inject()
5148

5249
private var faceAnalyzer: FacialDetectionAnalyzer? = null
53-
private var videoEncryptionHelper: VideoEncryptionHelper? = null
5450
private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
5551
private val analysisExecutor: ExecutorService = Executors.newSingleThreadExecutor()
5652
private val analysisScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@@ -98,66 +94,13 @@ class CameraState internal constructor(
9894
var recordingDurationMs by mutableLongStateOf(0L)
9995
private set
10096

101-
var isEncryptingVideo by mutableStateOf(false)
102-
private set
103-
104-
var encryptionProgress by mutableFloatStateOf(0f)
105-
private set
106-
10797
private var activeRecording: Recording? = null
10898
private var videoCapture: VideoCapture<Recorder>? = null
10999
private var pendingTempFile: File? = null
110100
private var pendingOutputFile: File? = null
111101

112102
init {
113103
observePreferences()
114-
initializeEncryptionHelper()
115-
}
116-
117-
private fun initializeEncryptionHelper() {
118-
val streamingScheme = encryptionScheme.getStreamingCapability()
119-
if (streamingScheme != null) {
120-
videoEncryptionHelper = VideoEncryptionHelper(streamingScheme)
121-
observeEncryptionProgress()
122-
} else {
123-
Timber.w("Streaming encryption not available")
124-
}
125-
}
126-
127-
private fun observeEncryptionProgress() {
128-
lifecycleOwner.lifecycle.coroutineScope.launch {
129-
videoEncryptionHelper?.encryptionProgress?.collectLatest { progress ->
130-
when (progress) {
131-
is VideoEncryptionHelper.EncryptionProgress.Idle -> {
132-
isEncryptingVideo = false
133-
encryptionProgress = 0f
134-
}
135-
136-
is VideoEncryptionHelper.EncryptionProgress.Starting -> {
137-
isEncryptingVideo = true
138-
encryptionProgress = 0f
139-
}
140-
141-
is VideoEncryptionHelper.EncryptionProgress.InProgress -> {
142-
isEncryptingVideo = true
143-
encryptionProgress = progress.progress
144-
}
145-
146-
is VideoEncryptionHelper.EncryptionProgress.Completed -> {
147-
isEncryptingVideo = false
148-
encryptionProgress = 0f
149-
videoEncryptionHelper?.resetProgress()
150-
}
151-
152-
is VideoEncryptionHelper.EncryptionProgress.Error -> {
153-
isEncryptingVideo = false
154-
encryptionProgress = 0f
155-
Timber.e("Encryption error: ${progress.message}")
156-
videoEncryptionHelper?.resetProgress()
157-
}
158-
}
159-
}
160-
}
161104
}
162105

163106
private fun observePreferences() {
@@ -323,8 +266,8 @@ class CameraState internal constructor(
323266
return null
324267
}
325268

326-
if (isRecording || isEncryptingVideo) {
327-
Timber.w("Recording or encryption already in progress")
269+
if (isRecording) {
270+
Timber.w("Recording already in progress")
328271
return null
329272
}
330273

@@ -375,11 +318,9 @@ class CameraState internal constructor(
375318
pendingTempFile = null
376319
pendingOutputFile = null
377320
} else {
378-
Timber.i("Recording complete, starting encryption...")
379-
// Start encryption in background
380-
lifecycleOwner.lifecycle.coroutineScope.launch {
381-
encryptRecordedVideo(tempFile, outputFile)
382-
}
321+
Timber.i("Recording complete, enqueueing encryption...")
322+
// Enqueue encryption via ForegroundService
323+
enqueueVideoEncryption(context, tempFile, outputFile)
383324
}
384325
}
385326
}
@@ -389,28 +330,12 @@ class CameraState internal constructor(
389330
}
390331

391332
/**
392-
* Encrypts the recorded video from temp file to final encrypted file.
333+
* Enqueues the recorded video for encryption via the ForegroundService.
334+
* The service handles encryption in the background with progress notifications.
393335
*/
394-
private suspend fun encryptRecordedVideo(tempFile: File, outputFile: File) {
395-
val helper = videoEncryptionHelper
396-
if (helper == null) {
397-
Timber.e("Video encryption helper not available")
398-
// Fall back to keeping the unencrypted file (rename to .mp4)
399-
val fallbackFile = File(outputFile.parent, outputFile.nameWithoutExtension + ".mp4")
400-
tempFile.renameTo(fallbackFile)
401-
return
402-
}
403-
404-
val success = helper.encryptVideoFile(tempFile, outputFile)
405-
if (success) {
406-
Timber.i("Video encrypted successfully: ${outputFile.absolutePath}")
407-
} else {
408-
Timber.e("Video encryption failed")
409-
// Keep the temp file as fallback (rename to .mp4)
410-
val fallbackFile = File(outputFile.parent, outputFile.nameWithoutExtension + ".mp4")
411-
tempFile.renameTo(fallbackFile)
412-
}
413-
336+
private fun enqueueVideoEncryption(context: Context, tempFile: File, outputFile: File) {
337+
Timber.i("Enqueueing video encryption: ${tempFile.name} -> ${outputFile.name}")
338+
VideoEncryptionService.enqueueEncryption(context, tempFile, outputFile)
414339
pendingTempFile = null
415340
pendingOutputFile = null
416341
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.darkrockstudios.app.securecamera.encryption
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import timber.log.Timber
7+
8+
/**
9+
* BroadcastReceiver for handling cancel actions from the encryption notification.
10+
*/
11+
class VideoEncryptionCancelReceiver : BroadcastReceiver() {
12+
13+
companion object {
14+
const val ACTION_CANCEL_ENCRYPTION = "com.darkrockstudios.app.securecamera.action.CANCEL_ENCRYPTION"
15+
const val ACTION_CANCEL_ALL = "com.darkrockstudios.app.securecamera.action.CANCEL_ALL_ENCRYPTION"
16+
}
17+
18+
override fun onReceive(context: Context, intent: Intent) {
19+
when (intent.action) {
20+
ACTION_CANCEL_ENCRYPTION -> {
21+
Timber.d("Cancel current encryption job requested")
22+
VideoEncryptionService.cancelCurrentJob(context)
23+
}
24+
25+
ACTION_CANCEL_ALL -> {
26+
Timber.d("Cancel all encryption jobs requested")
27+
VideoEncryptionService.cancelAllJobs(context)
28+
}
29+
}
30+
}
31+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.darkrockstudios.app.securecamera.encryption
2+
3+
import java.io.File
4+
5+
/**
6+
* Represents a video encryption job to be processed by the VideoEncryptionService.
7+
*/
8+
data class VideoEncryptionJob(
9+
val jobId: String,
10+
val tempFile: File,
11+
val outputFile: File,
12+
val createdAt: Long,
13+
val status: JobStatus = JobStatus.Pending
14+
)
15+
16+
/**
17+
* Represents the current status of an encryption job.
18+
*/
19+
sealed class JobStatus {
20+
data object Pending : JobStatus()
21+
data class InProgress(val progress: Float) : JobStatus()
22+
data object Completed : JobStatus()
23+
data class Error(val message: String) : JobStatus()
24+
data object Cancelled : JobStatus()
25+
}

0 commit comments

Comments
 (0)