Skip to content

Commit 2df5f83

Browse files
committed
Refactor SecureImageRepository
1 parent 72a09fc commit 2df5f83

File tree

17 files changed

+211
-249
lines changed

17 files changed

+211
-249
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.darkrockstudios.app.securecamera
22

33
import com.darkrockstudios.app.securecamera.auth.AuthorizationManager
4-
import com.darkrockstudios.app.securecamera.camera.SecureImageManager
4+
import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
55
import com.darkrockstudios.app.securecamera.camera.ThumbnailCache
66
import com.darkrockstudios.app.securecamera.gallery.GalleryViewModel
77
import com.darkrockstudios.app.securecamera.obfuscation.ObfuscatePhotoViewModel
@@ -17,7 +17,7 @@ import org.koin.core.module.dsl.viewModelOf
1717
import org.koin.dsl.module
1818

1919
val appModule = module {
20-
singleOf(::SecureImageManager)
20+
singleOf(::SecureImageRepository)
2121
single<AppPreferencesManager> { AppPreferencesManager(context = get()) }
2222
singleOf(::AuthorizationManager)
2323
singleOf(::LocationRepository)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.darkrockstudios.app.securecamera
22

33
import android.app.Application
4-
import com.darkrockstudios.app.securecamera.camera.SecureImageManager
4+
import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
55
import org.koin.android.ext.koin.androidContext
66
import org.koin.android.ext.koin.androidLogger
77
import org.koin.core.component.KoinComponent
@@ -10,7 +10,7 @@ import org.koin.core.context.startKoin
1010
import timber.log.Timber
1111

1212
class MainApplication : Application(), KoinComponent {
13-
private val imageManager by inject<SecureImageManager>()
13+
private val imageManager by inject<SecureImageRepository>()
1414

1515
override fun onCreate() {
1616
super.onCreate()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import androidx.compose.ui.text.style.TextAlign
2222
import androidx.compose.ui.unit.dp
2323
import androidx.navigation.NavController
2424
import com.darkrockstudios.app.securecamera.R
25-
import com.darkrockstudios.app.securecamera.camera.SecureImageManager
25+
import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
2626
import com.darkrockstudios.app.securecamera.gallery.vibrateDevice
2727
import com.darkrockstudios.app.securecamera.navigation.AppDestinations
2828
import com.darkrockstudios.app.securecamera.usecases.SecurityResetUseCase
@@ -44,7 +44,7 @@ fun PinVerificationContent(
4444
modifier: Modifier = Modifier
4545
) {
4646
val authManager = koinInject<AuthorizationManager>()
47-
val imageManager = koinInject<SecureImageManager>()
47+
val imageManager = koinInject<SecureImageRepository>()
4848
val securityResetUseCase = koinInject<SecurityResetUseCase>()
4949
val verifyPinUseCase = koinInject<VerifyPinUseCase>()
5050

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ fun CameraControls(
4747
var activeJobs by remember { mutableStateOf(mutableListOf<kotlinx.coroutines.Job>()) }
4848
val isLoading by remember { derivedStateOf { activeJobs.isNotEmpty() } }
4949
var isFlashing by rememberSaveable { mutableStateOf(false) }
50-
val imageSaver = koinInject<SecureImageManager>()
50+
val imageSaver = koinInject<SecureImageRepository>()
5151
val authManager = koinInject<AuthorizationManager>()
5252
val locationRepository = koinInject<LocationRepository>()
5353
val context = LocalContext.current
@@ -169,7 +169,7 @@ fun CameraControls(
169169
@OptIn(ExperimentalUuidApi::class)
170170
private suspend fun handleImageCapture(
171171
cameraController: CameraState,
172-
imageSaver: SecureImageManager,
172+
imageSaver: SecureImageRepository,
173173
context: Context,
174174
location: Location?,
175175
isFlashOn: Boolean,

app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/SecureImageManager.kt renamed to app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/SecureImageRepository.kt

Lines changed: 98 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import kotlinx.coroutines.sync.Mutex
2424
import kotlinx.coroutines.sync.withLock
2525
import java.io.ByteArrayOutputStream
2626
import java.io.File
27-
import java.io.FileOutputStream
2827
import java.text.SimpleDateFormat
2928
import java.util.*
3029
import kotlin.time.ExperimentalTime
@@ -34,7 +33,7 @@ private data class KeyParams(
3433
val outputSize: BinarySize = 32.bytes,
3534
)
3635

37-
class SecureImageManager(
36+
class SecureImageRepository(
3837
private val appContext: Context,
3938
private val preferencesManager: AppPreferencesManager,
4039
private val authorizationManager: AuthorizationManager,
@@ -148,6 +147,95 @@ class SecureImageManager(
148147
return secretDerivation.deriveSecret(keyInput.toByteArray()).toByteArray()
149148
}
150149

150+
/**
151+
* Compresses a bitmap to JPEG format with the specified quality
152+
*/
153+
private fun compressBitmapToJpeg(bitmap: Bitmap, quality: Int): ByteArray {
154+
return ByteArrayOutputStream().use { outputStream ->
155+
bitmap.compress(CompressFormat.JPEG, quality, outputStream)
156+
outputStream.toByteArray()
157+
}
158+
}
159+
160+
/**
161+
* Encrypts and saves image data to a file, then renames it to the target file
162+
*/
163+
private suspend fun encryptAndSaveImage(imageBytes: ByteArray, tempFile: File, targetFile: File) {
164+
tempFile.writeBytes(imageBytes)
165+
166+
val pin = authorizationManager.securityPin ?: throw IllegalStateException("No Security PIN")
167+
encryptToFile(
168+
plainPin = pin.plainPin,
169+
hashedPin = pin.hashedPin,
170+
plain = tempFile.readBytes(),
171+
targetFile = tempFile,
172+
)
173+
174+
tempFile.renameTo(targetFile)
175+
}
176+
177+
/**
178+
* Processes an image with metadata and prepares it for saving
179+
*/
180+
private fun processImageWithMetadata(
181+
bitmap: Bitmap,
182+
sourceJpgBytes: ByteArray,
183+
quality: Int
184+
): ByteArray {
185+
val newJpgBytes = compressBitmapToJpeg(bitmap, quality)
186+
var updatedBytes = newJpgBytes
187+
188+
val metadata = Kim.readMetadata(sourceJpgBytes)
189+
if (metadata != null) {
190+
// Apply all existing metadata to the new image
191+
metadata.convertToPhotoMetadata().let { photoMetadata ->
192+
if (photoMetadata.takenDate != null) {
193+
updatedBytes = Kim.update(bytes = updatedBytes, MetadataUpdate.TakenDate(photoMetadata.takenDate!!))
194+
}
195+
196+
if (photoMetadata.orientation != null) {
197+
updatedBytes =
198+
Kim.update(bytes = updatedBytes, MetadataUpdate.Orientation(photoMetadata.orientation!!))
199+
}
200+
201+
if (photoMetadata.gpsCoordinates != null) {
202+
updatedBytes =
203+
Kim.update(bytes = updatedBytes, MetadataUpdate.GpsCoordinates(photoMetadata.gpsCoordinates!!))
204+
}
205+
}
206+
}
207+
208+
return updatedBytes
209+
}
210+
211+
/**
212+
* Applies specific metadata to an image for the saveImage function
213+
*/
214+
private fun applySaveImageMetadata(
215+
imageBytes: ByteArray,
216+
latLng: GpsCoordinates?,
217+
applyRotation: Boolean,
218+
rotationDegrees: Int
219+
): ByteArray {
220+
val dateUpdate: MetadataUpdate = MetadataUpdate.TakenDate(System.currentTimeMillis())
221+
var updatedBytes = Kim.update(bytes = imageBytes, dateUpdate)
222+
223+
if (applyRotation) {
224+
updatedBytes = Kim.update(bytes = updatedBytes, MetadataUpdate.Orientation(TiffOrientation.STANDARD))
225+
} else {
226+
val tiffOrientation = calculateTiffOrientation(rotationDegrees)
227+
val orientationUpdate: MetadataUpdate = MetadataUpdate.Orientation(tiffOrientation)
228+
updatedBytes = Kim.update(bytes = updatedBytes, orientationUpdate)
229+
}
230+
231+
if (latLng != null) {
232+
val gpsUpdate: MetadataUpdate = MetadataUpdate.GpsCoordinates(latLng)
233+
updatedBytes = Kim.update(bytes = updatedBytes, gpsUpdate)
234+
}
235+
236+
return updatedBytes
237+
}
238+
151239
@OptIn(ExperimentalTime::class)
152240
suspend fun saveImage(
153241
image: CapturedImage,
@@ -172,53 +260,9 @@ class SecureImageManager(
172260
rawSensorBitmap = rawSensorBitmap.rotate(image.rotationDegrees)
173261
}
174262

175-
FileOutputStream(tempFile).use { outputStream ->
176-
rawSensorBitmap.compress(CompressFormat.JPEG, quality, outputStream)
177-
}
178-
179-
val dateUpdate: MetadataUpdate = MetadataUpdate.TakenDate(System.currentTimeMillis())
180-
var updatedBytes = Kim.update(bytes = tempFile.readBytes(), dateUpdate)
181-
182-
if(applyRotation) {
183-
updatedBytes = Kim.update(bytes = updatedBytes, MetadataUpdate.Orientation(TiffOrientation.STANDARD))
184-
} else {
185-
val tiffOrientation = calculateTiffOrientation(image.rotationDegrees)
186-
val orientationUpdate: MetadataUpdate = MetadataUpdate.Orientation(tiffOrientation)
187-
updatedBytes = Kim.update(bytes = updatedBytes, orientationUpdate)
188-
}
189-
190-
if (latLng != null) {
191-
val gpsUpdate: MetadataUpdate = MetadataUpdate.GpsCoordinates(latLng)
192-
updatedBytes = Kim.update(bytes = updatedBytes, gpsUpdate)
193-
}
194-
195-
tempFile.writeBytes(updatedBytes)
196-
197-
// val thumbnailBitmap = ThumbnailUtils.createImageThumbnail(photoFile, Size(640, 480), null)
198-
// val thumbnailBytes = thumbnailBitmap.let { bitmap ->
199-
// ByteArrayOutputStream().use { outputStream ->
200-
// bitmap.compress(CompressFormat.JPEG, quality, outputStream)
201-
// outputStream.toByteArray()
202-
// }
203-
// }
204-
//
205-
// photoFile.writeBytes(
206-
// Kim.updateThumbnail(
207-
// bytes = photoFile.readBytes(),
208-
// thumbnailBytes = thumbnailBytes
209-
// )
210-
// )
211-
212-
val pin = authorizationManager.securityPin ?: throw IllegalStateException("No Security PIN")
213-
214-
encryptToFile(
215-
plainPin = pin.plainPin,
216-
hashedPin = pin.hashedPin,
217-
plain = tempFile.readBytes(),
218-
targetFile = tempFile,
219-
)
220-
221-
tempFile.renameTo(photoFile)
263+
val jpgBytes = compressBitmapToJpeg(rawSensorBitmap, quality)
264+
val updatedBytes = applySaveImageMetadata(jpgBytes, latLng, applyRotation, image.rotationDegrees)
265+
encryptAndSaveImage(updatedBytes, tempFile, photoFile)
222266

223267
return photoFile
224268
}
@@ -229,49 +273,12 @@ class SecureImageManager(
229273
quality: Int = 90
230274
): PhotoDef {
231275
val jpgBytes = decryptJpg(photoDef)
232-
233-
val metadata = Kim.readMetadata(jpgBytes)
234-
235-
val newJpgBytes = ByteArrayOutputStream().use { outputStream ->
236-
bitmap.compress(CompressFormat.JPEG, quality, outputStream)
237-
outputStream.toByteArray()
238-
}
276+
val updatedBytes = processImageWithMetadata(bitmap, jpgBytes, quality)
239277

240278
val dir = getGalleryDirectory()
241279
val tempFile = File(dir, "${photoDef.photoName}.tmp")
242280

243-
var updatedBytes = newJpgBytes
244-
245-
if (metadata != null) {
246-
// Apply all existing metadata to the new image
247-
metadata.convertToPhotoMetadata().let { photoMetadata ->
248-
if (photoMetadata.takenDate != null) {
249-
updatedBytes = Kim.update(bytes = updatedBytes, MetadataUpdate.TakenDate(photoMetadata.takenDate!!))
250-
}
251-
252-
if (photoMetadata.orientation != null) {
253-
updatedBytes =
254-
Kim.update(bytes = updatedBytes, MetadataUpdate.Orientation(photoMetadata.orientation!!))
255-
}
256-
257-
if (photoMetadata.gpsCoordinates != null) {
258-
updatedBytes =
259-
Kim.update(bytes = updatedBytes, MetadataUpdate.GpsCoordinates(photoMetadata.gpsCoordinates!!))
260-
}
261-
}
262-
}
263-
264-
tempFile.writeBytes(updatedBytes)
265-
266-
val pin = authorizationManager.securityPin ?: throw IllegalStateException("No Security PIN")
267-
encryptToFile(
268-
plainPin = pin.plainPin,
269-
hashedPin = pin.hashedPin,
270-
plain = tempFile.readBytes(),
271-
targetFile = tempFile,
272-
)
273-
274-
tempFile.renameTo(photoDef.photoFile)
281+
encryptAndSaveImage(updatedBytes, tempFile, photoDef.photoFile)
275282

276283
thumbnailCache.evictThumbnail(photoDef)
277284
getThumbnail(photoDef).delete()
@@ -285,54 +292,14 @@ class SecureImageManager(
285292
quality: Int = 90
286293
): PhotoDef {
287294
val jpgBytes = decryptJpg(photoDef)
288-
289-
val metadata = Kim.readMetadata(jpgBytes)
290-
291-
val newJpgBytes = ByteArrayOutputStream().use { outputStream ->
292-
bitmap.compress(CompressFormat.JPEG, quality, outputStream)
293-
outputStream.toByteArray()
294-
}
295+
val updatedBytes = processImageWithMetadata(bitmap, jpgBytes, quality)
295296

296297
val dir = getGalleryDirectory()
297-
298-
// Create a new unique filename based on the current time
299-
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss_SS", Locale.US)
300-
val newImageName = "photo_" + dateFormat.format(Date()) + ".jpg"
298+
val newImageName = photoDef.photoName.substringBefore(".jpg") + "_cp.jpg"
301299
val newPhotoFile = File(dir, newImageName)
302300
val tempFile = File(dir, "$newImageName.tmp")
303301

304-
var updatedBytes = newJpgBytes
305-
306-
if (metadata != null) {
307-
// Apply all existing metadata to the new image
308-
metadata.convertToPhotoMetadata().let { photoMetadata ->
309-
if (photoMetadata.takenDate != null) {
310-
updatedBytes = Kim.update(bytes = updatedBytes, MetadataUpdate.TakenDate(photoMetadata.takenDate!!))
311-
}
312-
313-
if (photoMetadata.orientation != null) {
314-
updatedBytes =
315-
Kim.update(bytes = updatedBytes, MetadataUpdate.Orientation(photoMetadata.orientation!!))
316-
}
317-
318-
if (photoMetadata.gpsCoordinates != null) {
319-
updatedBytes =
320-
Kim.update(bytes = updatedBytes, MetadataUpdate.GpsCoordinates(photoMetadata.gpsCoordinates!!))
321-
}
322-
}
323-
}
324-
325-
tempFile.writeBytes(updatedBytes)
326-
327-
val pin = authorizationManager.securityPin ?: throw IllegalStateException("No Security PIN")
328-
encryptToFile(
329-
plainPin = pin.plainPin,
330-
hashedPin = pin.hashedPin,
331-
plain = tempFile.readBytes(),
332-
targetFile = tempFile,
333-
)
334-
335-
tempFile.renameTo(newPhotoFile)
302+
encryptAndSaveImage(updatedBytes, tempFile, newPhotoFile)
336303

337304
// Create a new PhotoDef for the new file
338305
val newPhotoDef = PhotoDef(

app/src/main/kotlin/com/darkrockstudios/app/securecamera/gallery/GalleryContent.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import androidx.navigation.NavController
2828
import com.darkrockstudios.app.securecamera.ConfirmDeletePhotoDialog
2929
import com.darkrockstudios.app.securecamera.R
3030
import com.darkrockstudios.app.securecamera.camera.PhotoDef
31-
import com.darkrockstudios.app.securecamera.camera.SecureImageManager
31+
import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
3232
import com.darkrockstudios.app.securecamera.navigation.AppDestinations
3333
import com.darkrockstudios.app.securecamera.ui.HandleUiEvents
3434
import kotlinx.coroutines.CoroutineDispatcher
@@ -124,7 +124,7 @@ private fun PhotoGrid(
124124
Dispatchers.IO.limitedParallelism(4) // Limit to 4 concurrent thumbnail loads
125125
}
126126

127-
val imageManager = koinInject<SecureImageManager>()
127+
val imageManager = koinInject<SecureImageRepository>()
128128
val scope = rememberCoroutineScope()
129129
LazyVerticalGrid(
130130
columns = GridCells.Adaptive(minSize = 128.dp),
@@ -151,7 +151,7 @@ private fun PhotoGrid(
151151
@Composable
152152
private fun PhotoItem(
153153
photo: PhotoDef,
154-
imageManager: SecureImageManager,
154+
imageManager: SecureImageRepository,
155155
scope: CoroutineScope,
156156
limitedDispatcher: CoroutineDispatcher,
157157
isSelected: Boolean = false,

app/src/main/kotlin/com/darkrockstudios/app/securecamera/gallery/GalleryViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import android.content.Context
44
import androidx.lifecycle.viewModelScope
55
import com.darkrockstudios.app.securecamera.BaseViewModel
66
import com.darkrockstudios.app.securecamera.camera.PhotoDef
7-
import com.darkrockstudios.app.securecamera.camera.SecureImageManager
7+
import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
88
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesManager
99
import com.darkrockstudios.app.securecamera.share.sharePhotosWithProvider
1010
import kotlinx.coroutines.Dispatchers
@@ -13,7 +13,7 @@ import kotlinx.coroutines.launch
1313
import kotlinx.coroutines.withContext
1414

1515
class GalleryViewModel(
16-
private val imageManager: SecureImageManager,
16+
private val imageManager: SecureImageRepository,
1717
private val preferencesManager: AppPreferencesManager
1818
) : BaseViewModel<GalleryUiState>() {
1919

0 commit comments

Comments
 (0)