Skip to content

Commit 67e1c45

Browse files
committed
Allow rotation withour recreation
- Fix thumbnail generation
1 parent dcf7c8e commit 67e1c45

3 files changed

Lines changed: 65 additions & 8 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<activity
3636
android:name=".MainActivity"
3737
android:exported="true"
38+
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
3839
android:theme="@style/Theme.SecureCamera">
3940
<intent-filter>
4041
<action android:name="android.intent.action.MAIN"/>

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

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -471,12 +471,25 @@ class SecureImageRepository(
471471
var tempFile: File? = null
472472

473473
return try {
474+
if (!video.videoFile.exists()) {
475+
Timber.e("Video file does not exist: ${video.videoFile.absolutePath}")
476+
return null
477+
}
478+
479+
Timber.d("Video file size: ${video.videoFile.length()} bytes")
480+
474481
decryptor = streamingScheme.createStreamingDecryptor(video.videoFile)
475482

476483
// Create a temporary file with decrypted video content
477-
// We need enough data for MediaMetadataRetriever to extract a frame
478-
// Typically the first few MB are sufficient
479-
val bytesToRead = minOf(decryptor.totalSize, THUMBNAIL_EXTRACTION_BYTES)
484+
// MP4 files often have moov atom at the end, so we need enough data
485+
// For smaller videos, read the whole file to ensure we get the moov atom
486+
val bytesToRead = if (decryptor.totalSize <= SMALL_VIDEO_THRESHOLD) {
487+
Timber.d("Small video, reading entire file for thumbnail")
488+
decryptor.totalSize
489+
} else {
490+
Timber.d("Large video, reading first $THUMBNAIL_EXTRACTION_BYTES bytes")
491+
minOf(decryptor.totalSize, THUMBNAIL_EXTRACTION_BYTES)
492+
}
480493
val buffer = ByteArray(bytesToRead.toInt())
481494
val bytesRead = decryptor.read(0, buffer, 0, buffer.size)
482495

@@ -508,11 +521,30 @@ class SecureImageRepository(
508521
return try {
509522
MediaMetadataRetriever().use { retriever ->
510523
retriever.setDataSource(videoFile.absolutePath)
511-
// Get frame at 1 second (or first frame if video is shorter)
512-
retriever.getFrameAtTime(1_000_000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
524+
525+
// Try frame at 1 second first
526+
var frame = retriever.getFrameAtTime(1_000_000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
527+
528+
// If that fails, try frame at 0 (first frame)
529+
if (frame == null) {
530+
Timber.d("Frame at 1s not found, trying first frame")
531+
frame = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
532+
}
533+
534+
// Last resort: try any frame
535+
if (frame == null) {
536+
Timber.d("Specific frames not found, trying any frame")
537+
frame = retriever.frameAtTime
538+
}
539+
540+
if (frame == null) {
541+
Timber.w("Could not extract any frame from video: ${videoFile.name}")
542+
}
543+
544+
frame
513545
}
514546
} catch (e: Exception) {
515-
Timber.e(e, "Failed to extract video frame")
547+
Timber.e(e, "Failed to extract video frame from: ${videoFile.name}")
516548
null
517549
}
518550
}
@@ -741,8 +773,11 @@ class SecureImageRepository(
741773
const val THUMBNAILS_DIR = ".thumbnails"
742774
const val MAX_DECOY_PHOTOS = 10
743775

744-
// Amount of video data to decrypt for thumbnail extraction (5MB should be enough for moov atom)
745-
private const val THUMBNAIL_EXTRACTION_BYTES = 5L * 1024 * 1024
776+
// Amount of video data to decrypt for thumbnail extraction
777+
// MP4 files may have moov atom at the end, so we need to read more data
778+
// For videos under 50MB, we read the whole file; otherwise we read 20MB
779+
private const val THUMBNAIL_EXTRACTION_BYTES = 20L * 1024 * 1024
780+
private const val SMALL_VIDEO_THRESHOLD = 50L * 1024 * 1024
746781

747782
internal fun generateCopyName(dir: File, originalName: String): String {
748783
val base = originalName.substringBeforeLast(".")

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ private fun MediaGridItem(
189189
) {
190190
val isEncrypting = encryptionProgress != null
191191
var thumbnailBitmap by remember(mediaItem.mediaName) { mutableStateOf<ImageBitmap?>(null) }
192+
var thumbnailFailed by remember(mediaItem.mediaName) { mutableStateOf(false) }
192193
val isDecoy = remember(mediaItem) {
193194
(mediaItem as? PhotoDef)?.let { imageManager.isDecoyPhoto(it) } ?: false
194195
}
@@ -204,6 +205,7 @@ private fun MediaGridItem(
204205
if (thumbnailBitmap == null && !isEncrypting) {
205206
scope.launch(limitedDispatcher) {
206207
thumbnailBitmap = imageManager.readMediaThumbnail(mediaItem)?.asImageBitmap()
208+
thumbnailFailed = (thumbnailBitmap == null)
207209
}
208210
}
209211
}
@@ -274,6 +276,25 @@ private fun MediaGridItem(
274276
.alpha(imageAlpha)
275277
)
276278
}
279+
// Show placeholder for failed thumbnails
280+
thumbnailFailed -> {
281+
Box(
282+
modifier = Modifier
283+
.fillMaxSize()
284+
.background(MaterialTheme.colorScheme.surfaceVariant),
285+
contentAlignment = Alignment.Center
286+
) {
287+
Icon(
288+
imageVector = if (mediaItem.mediaType == MediaType.VIDEO)
289+
Icons.Filled.PlayArrow
290+
else
291+
Icons.Filled.Warning,
292+
contentDescription = null,
293+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
294+
modifier = Modifier.size(48.dp)
295+
)
296+
}
297+
}
277298
// Show loading spinner while thumbnail loads
278299
else -> {
279300
Box(modifier = Modifier.fillMaxSize()) {

0 commit comments

Comments
 (0)