Auto Open stabilization

This commit is contained in:
DarkKnight2122
2026-03-31 17:25:27 +05:30
parent 8a9258b318
commit 1fdaf6d55d
6 changed files with 695 additions and 684 deletions

View File

@@ -573,8 +573,12 @@ class FeaturesRootSection : Routes.Route() {
} }
DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> { DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> {
val isMessageListProperty = property.key.name.endsWith("_messages") val isMessageListProperty = property.key.name.endsWith("_messages")
val isSleepWindowProperty = property.key.name.contains("sleep_window")
if (isMessageListProperty) { if (isMessageListProperty) {
alertDialogs.MessageListPropertyDialog(property) { showDialog = false } alertDialogs.MessageListPropertyDialog(property) { showDialog = false }
} else if (isSleepWindowProperty) {
alertDialogs.AutoOpenScheduleDialog(property as PropertyPair<String>) { showDialog = false }
} else { } else {
alertDialogs.KeyboardInputDialog(property) { showDialog = false } alertDialogs.KeyboardInputDialog(property) { showDialog = false }
} }

View File

@@ -11,6 +11,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -27,6 +28,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -80,6 +82,146 @@ import me.eternal.purrfectsnap.ui.util.Dialog as StandardDialog
class AlertDialogs( class AlertDialogs(
private val translation: LocaleWrapper, private val translation: LocaleWrapper,
){ ){
@Composable
fun MessageListPropertyDialog(property: PropertyPair<*>, onDismiss: () -> Unit = {}) {
val currentValue = property.value.getNullable()?.toString() ?: "[]"
val propertyName = translation[property.key.propertyName()]
MessageListManagerDialog(
title = propertyName ?: "",
messageListJson = currentValue,
onSave = { newValue: String ->
property.value.setAny(newValue)
},
onDismiss = onDismiss
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AutoOpenScheduleDialog(
property: PropertyPair<String>,
onDismiss: () -> Unit
) {
val windowParts = (property.value.get() as String).split("-")
val startTime = windowParts.getOrNull(0)?.split(":") ?: listOf("23", "00")
val endTime = windowParts.getOrNull(1)?.split(":") ?: listOf("07", "00")
var isEditingEnd by remember { mutableStateOf(false) }
val startState = rememberTimePickerState(
initialHour = startTime.getOrNull(0)?.toIntOrNull() ?: 23,
initialMinute = startTime.getOrNull(1)?.toIntOrNull() ?: 0,
is24Hour = true
)
val endState = rememberTimePickerState(
initialHour = endTime.getOrNull(0)?.toIntOrNull() ?: 7,
initialMinute = endTime.getOrNull(1)?.toIntOrNull() ?: 0,
is24Hour = true
)
DefaultDialogCard {
Column(
modifier = Modifier.padding(18.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = translation["auto_open_snaps.auto_open_schedule.title"] ?: "Auto Open Scheduler",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.ExtraBold,
color = Color.White
)
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White.copy(alpha = 0.05f))
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
val activeColor = PurrfectPalette.glowPrimary.copy(alpha = 0.25f)
val inactiveColor = Color.Transparent
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(12.dp))
.background(if (!isEditingEnd) activeColor else inactiveColor)
.clickable { isEditingEnd = false }
.padding(vertical = 10.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "${translation["auto_open_snaps.auto_open_schedule.start"] ?: "Start"}: ${String.format("%02d:%02d", startState.hour, startState.minute)}",
color = if (!isEditingEnd) Color.White else Color.White.copy(alpha = 0.6f),
fontWeight = if (!isEditingEnd) FontWeight.Bold else FontWeight.Normal
)
}
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(12.dp))
.background(if (isEditingEnd) activeColor else inactiveColor)
.clickable { isEditingEnd = true }
.padding(vertical = 10.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "${translation["auto_open_snaps.auto_open_schedule.end"] ?: "End"}: ${String.format("%02d:%02d", endState.hour, endState.minute)}",
color = if (isEditingEnd) Color.White else Color.White.copy(alpha = 0.6f),
fontWeight = if (isEditingEnd) FontWeight.Bold else FontWeight.Normal
)
}
}
TimePicker(
state = if (isEditingEnd) endState else startState,
colors = TimePickerDefaults.colors(
clockDialColor = Color.White.copy(alpha = 0.05f),
clockDialSelectedContentColor = Color.White,
clockDialUnselectedContentColor = Color.White.copy(alpha = 0.7f),
selectorColor = PurrfectPalette.glowPrimary,
periodSelectorBorderColor = PurrfectPalette.glowPrimary,
periodSelectorSelectedContainerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.2f),
periodSelectorUnselectedContainerColor = Color.Transparent,
periodSelectorSelectedContentColor = Color.White,
periodSelectorUnselectedContentColor = Color.White.copy(alpha = 0.7f),
timeSelectorSelectedContainerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.2f),
timeSelectorUnselectedContainerColor = Color.White.copy(alpha = 0.05f),
timeSelectorSelectedContentColor = Color.White,
timeSelectorUnselectedContentColor = Color.White.copy(alpha = 0.7f)
)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End)
) {
TextButton(onClick = onDismiss) {
Text(text = translation["button.negative"], color = Color.White)
}
Button(
onClick = {
val startStr = String.format("%02d:%02d", startState.hour, startState.minute)
val endStr = String.format("%02d:%02d", endState.hour, endState.minute)
property.value.setAny("$startStr-$endStr")
onDismiss()
},
colors = ButtonDefaults.buttonColors(
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.35f),
contentColor = Color.White
)
) {
Text(text = translation["button.positive"])
}
}
}
}
}
@Composable @Composable
fun DefaultDialogCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { fun DefaultDialogCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@@ -1260,23 +1402,7 @@ class AlertDialogs(
} }
} }
} }
}
}
@Composable
fun MessageListPropertyDialog(property: PropertyPair<*>, onDismiss: () -> Unit = {}) {
val currentValue = property.value.getNullable()?.toString() ?: "[]"
val propertyName = translation[property.key.propertyName()]
MessageListManagerDialog(
title = propertyName,
messageListJson = currentValue,
onSave = { newValue ->
property.value.setAny(newValue)
},
onDismiss = onDismiss
)
}
@Composable @Composable
fun MessageListManagerDialog( fun MessageListManagerDialog(
@@ -1313,7 +1439,6 @@ class AlertDialogs(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
// Message list
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -1329,7 +1454,7 @@ class AlertDialogs(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = translation["auto_reply_messages.dialog.no_messages"], text = translation["bulk_messaging_action.no_messages_found"] ?: "No messages",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
@@ -1371,24 +1496,14 @@ class AlertDialogs(
showAddDialog = true showAddDialog = true
} }
) { ) {
Icon( Icon(Icons.Default.Edit, contentDescription = translation["common.edit"] ?: "Edit", tint = MaterialTheme.colorScheme.primary)
Icons.Default.Edit,
contentDescription = "Edit",
tint = MaterialTheme.colorScheme.primary
)
} }
IconButton( IconButton(
onClick = { onClick = {
messageList = messageList.toMutableList().apply { messageList = messageList.toMutableList().apply { removeAt(index) }
removeAt(index)
}
} }
) { ) {
Icon( Icon(Icons.Default.Delete, contentDescription = translation["common.delete"] ?: "Delete", tint = MaterialTheme.colorScheme.error)
Icons.Default.Delete,
contentDescription = "Delete",
tint = MaterialTheme.colorScheme.error
)
} }
} }
} }
@@ -1398,7 +1513,6 @@ class AlertDialogs(
} }
} }
// Add button
Button( Button(
onClick = { onClick = {
editingIndex = -1 editingIndex = -1
@@ -1408,20 +1522,13 @@ class AlertDialogs(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp), .padding(8.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
containerColor = MaterialTheme.colorScheme.primary
)
) { ) {
Icon( Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(text = translation["auto_reply_messages.dialog.add_message"]) Text(text = translation["common.add"] ?: "Add Message")
} }
// Dialog buttons
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -1430,17 +1537,14 @@ class AlertDialogs(
) { ) {
Button( Button(
onClick = { onDismiss() }, onClick = { onDismiss() },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Text(text = translation["button.cancel"]) Text(text = translation["button.cancel"])
} }
Button( Button(
onClick = { onClick = {
val gson = com.google.gson.Gson() val gson = com.google.gson.Gson()
val jsonString = gson.toJson(messageList) onSave(gson.toJson(messageList))
onSave(jsonString)
onDismiss() onDismiss()
} }
) { ) {
@@ -1450,7 +1554,6 @@ class AlertDialogs(
} }
} }
// Add/Edit message dialog
if (showAddDialog) { if (showAddDialog) {
StandardDialog( StandardDialog(
onDismissRequest = { showAddDialog = false }, onDismissRequest = { showAddDialog = false },
@@ -1460,7 +1563,7 @@ class AlertDialogs(
) { ) {
DefaultDialogCard { DefaultDialogCard {
Text( Text(
text = if (editingIndex == -1) translation["auto_reply_messages.dialog.add_message"] else translation["auto_reply_messages.dialog.edit_message"], text = if (editingIndex == -1) translation["common.add"] ?: "Add Message" else translation["common.edit"] ?: "Edit Message",
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier modifier = Modifier
@@ -1472,13 +1575,13 @@ class AlertDialogs(
TextField( TextField(
value = editingText, value = editingText,
onValueChange = { editingText = it }, onValueChange = { editingText = it },
label = { Text(translation["auto_reply_messages.dialog.message_label"]) }, label = { Text(translation["common.message"] ?: "Message") },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp), .padding(8.dp),
minLines = 2, minLines = 2,
maxLines = 4, maxLines = 4,
placeholder = { Text(translation["auto_reply_messages.dialog.message_placeholder"]) } placeholder = { Text(translation["common.type_message"] ?: "Type message...") }
) )
Row( Row(
@@ -1489,32 +1592,22 @@ class AlertDialogs(
) { ) {
Button( Button(
onClick = { showAddDialog = false }, onClick = { showAddDialog = false },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Text(text = translation["button.cancel"]) Text(text = translation["button.cancel"])
} }
Button( Button(
onClick = { onClick = {
if (editingText.isNotBlank()) { if (editingText.isNotBlank()) {
if (editingIndex == -1) { messageList = messageList.toMutableList().apply {
// Add new message if (editingIndex == -1) add(editingText) else set(editingIndex, editingText)
messageList = messageList.toMutableList().apply {
add(editingText)
}
} else {
// Edit existing message
messageList = messageList.toMutableList().apply {
set(editingIndex, editingText)
}
} }
} }
showAddDialog = false showAddDialog = false
}, },
enabled = editingText.isNotBlank() enabled = editingText.isNotBlank()
) { ) {
Text(text = if (editingIndex == -1) translation["auto_reply_messages.dialog.add_message"] else translation["button.save"]) Text(text = if (editingIndex == -1) translation["common.add"] ?: "Add" else translation["button.save"])
} }
} }
} }
@@ -1522,3 +1615,4 @@ class AlertDialogs(
} }
} }
} }

View File

@@ -1,4 +1,4 @@
{ {
"setup": { "setup": {
"activity": { "activity": {
"wrong_apk_title": "Wrong APK installed", "wrong_apk_title": "Wrong APK installed",
@@ -1661,10 +1661,26 @@
"name": "Auto Open Compact Notification", "name": "Auto Open Compact Notification",
"description": "Use a smaller, single-line notification for status updates" "description": "Use a smaller, single-line notification for status updates"
}, },
"show_progress_bar": {
"name": "Show Progress Bar",
"description": "Display a visual progress bar in the status notification"
},
"show_lifetime_stats": {
"name": "Show Lifetime Statistics",
"description": "Include the total number of snaps opened since installation in the notification"
},
"show_queue_preview": {
"name": "Show Queue Preview",
"description": "Show a list of the most recent snaps waiting in the queue (Expanded only)"
},
"only_on_wifi": { "name": "Auto Open only on Wi-Fi", "description": "Only process queue when connected to a Wi-Fi network to save mobile data" }, "only_on_wifi": { "name": "Auto Open only on Wi-Fi", "description": "Only process queue when connected to a Wi-Fi network to save mobile data" },
"only_when_idle": { "only_when_idle": {
"name": "Auto Open only when Idle", "name": "Auto Open Schedule",
"description": "Only process queue when the device is not in active use" "description": "Configure a specific time window where the engine will throttle its speed."
},
"sleep_window": {
"name": "Auto Open Scheduler",
"description": "Define the start and end times for scheduled throttled processing."
}, },
"pause_during_gaming": { "name": "Pause Auto Open During Gaming", "description": "Automatically slow down processing when a resource intensive app or a game is in the foreground" }, "pause_during_gaming": { "name": "Pause Auto Open During Gaming", "description": "Automatically slow down processing when a resource intensive app or a game is in the foreground" },
"safe_processing": { "name": "Auto Open with stealth pace", "description": "Adds variable delays and natural breaks to remain undetected. Turn off for maximum speed." } "safe_processing": { "name": "Auto Open with stealth pace", "description": "Adds variable delays and natural breaks to remain undetected. Turn off for maximum speed." }
@@ -2165,7 +2181,7 @@
} }
} }
}, },
"network_optimization": { "name": "Network Optimization", "description": "Optimizes network socket buffers for higher throughput" }, "better_transcript": { "network_optimization": { "name": "Improved Network Connectivity", "description": "Optimizes network socket buffers for maximum stability and high-speed upload/download performance" }, "better_transcript": {
"name": "Better Transcript", "name": "Better Transcript",
"description": "Improves the voice note transcript", "description": "Improves the voice note transcript",
"properties": { "properties": {
@@ -3257,6 +3273,11 @@
"forced_logout_toast": "Removed account due to forced logout" "forced_logout_toast": "Removed account due to forced logout"
}, },
"auto_open_snaps": { "title": "Auto Open Snaps", "processed_count": "Opened", "queue_size": "Queue", "action_reset": "Reset Statistics", "priority_title": "Auto Open Snaps (Priority)", "auto_open_snaps": { "title": "Auto Open Snaps", "processed_count": "Opened", "queue_size": "Queue", "action_reset": "Reset Statistics", "priority_title": "Auto Open Snaps (Priority)",
"auto_open_schedule": {
"title": "Auto Open Scheduler",
"start": "Start",
"end": "End"
},
"error_title": "Auto Open Snaps (Errors)", "error_title": "Auto Open Snaps (Errors)",
"channel_description": "Notifications for auto-opening snaps queue status", "channel_description": "Notifications for auto-opening snaps queue status",
"priority_channel_description": "High priority notifications for auto-opening snaps", "priority_channel_description": "High priority notifications for auto-opening snaps",
@@ -3277,6 +3298,16 @@
"status_paused": "Paused", "status_paused": "Paused",
"status_monitoring": "Monitoring", "status_monitoring": "Monitoring",
"status_active": "Active", "status_active": "Active",
"status_failed": "Failed to open {sender}",
"status_retrying": "Retrying in background...",
"processing_speed_full": "Full Speed",
"processing_speed": "Processing Speed",
"speed_throttled": "Throttled",
"estimated_time": "Estimated Time",
"notification_statistics": "STATISTICS",
"notification_total_opened": "Lifetime Opened",
"notification_queue_preview": "QUEUE PREVIEW",
"notification_no_snaps_queue": "Monitoring snaps in background...",
"queue_cleared": "Queue cleared and statistics reset", "queue_cleared": "Queue cleared and statistics reset",
"queue_cleared_title": "Queue cleared", "queue_cleared_title": "Queue cleared",
"queue_cleared_reset": "Queue Cleared & Reset", "queue_cleared_reset": "Queue Cleared & Reset",
@@ -3686,17 +3717,25 @@
"include_saved_locations_description": "Export your saved location coordinates", "include_saved_locations_description": "Export your saved location coordinates",
"common": { "common": {
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save",
"edit": "Edit",
"delete": "Delete",
"close": "Close", "close": "Close",
"add": "Add", "add": "Add",
"ok": "OK", "ok": "OK",
"quit": "Quit", "quit": "Quit",
"done": "Done", "done": "Done",
"back": "Back", "back": "Back",
"message": "Message",
"type_message": "Type message...",
"unknown": "Unknown", "unknown": "Unknown",
"unknown_error": "Unknown error", "unknown_error": "Unknown error",
"not_available": "N/A", "not_available": "N/A",
"added": "Added", "added": "Added",
"no_friends_found": "No friends found", "no_friends_found": "No friends found",
"no_messages": "No messages",
"message": "Message",
"type_message": "Type message...",
"exporting_memories": "Exporting memories... ({failed} failed)" "exporting_memories": "Exporting memories... ({failed} failed)"
}, },
"clear_friend_feed": "Clear Friend Feed", "clear_friend_feed": "Clear Friend Feed",

View File

@@ -176,12 +176,19 @@ class MessagingTweaks : ConfigContainer() {
inputCheck = { it.toIntOrNull()?.coerceAtLeast(1000) != null } inputCheck = { it.toIntOrNull()?.coerceAtLeast(1000) != null }
} }
val compactNotification = boolean("compact_notification", false) val compactNotification = boolean("compact_notification", false)
val showProgressBar = boolean("show_progress_bar", true)
val showLifetimeStats = boolean("show_lifetime_stats", false)
val showQueuePreview = boolean("show_queue_preview", true)
// Resource Intelligence: Smart triggers for battery and data safety // Resource Intelligence: Smart triggers for battery and data safety
val onlyOnWifi = boolean("only_on_wifi", false) val onlyOnWifi = boolean("only_on_wifi", false)
val onlyWhenIdle = boolean("only_when_idle", false)
val pauseDuringGaming = boolean("pause_during_gaming", false) val pauseDuringGaming = boolean("pause_during_gaming", false)
val safeProcessing = boolean("safe_processing", true) val safeProcessing = boolean("safe_processing", true)
val onlyWhenIdle = boolean("only_when_idle", false)
val sleepWindow = string("sleep_window", defaultValue = "23:00-07:00") {
addFlags(ConfigFlag.NO_DISABLE_KEY)
inputCheck = { it.matches(Regex("^([01]\\d|2[0-3]):([0-5]\\d)-([01]\\d|2[0-3]):([0-5]\\d)$")) }
}
} }
class AutoDeleteSentMessagesConfig : ConfigContainer(hasGlobalState = true) { class AutoDeleteSentMessagesConfig : ConfigContainer(hasGlobalState = true) {
@@ -324,4 +331,3 @@ class MessagingTweaks : ConfigContainer() {
val instantTranslation = container("instant_translation", InstantTranslationConfig()) { requireRestart() } val instantTranslation = container("instant_translation", InstantTranslationConfig()) { requireRestart() }
} }

View File

@@ -21,6 +21,8 @@ enum class FileType(
JPG("jpg", "image/jpg",false, true, false), JPG("jpg", "image/jpg",false, true, false),
ZIP("zip", "application/zip", false, false, false), ZIP("zip", "application/zip", false, false, false),
WEBP("webp", "image/webp", false, true, false), WEBP("webp", "image/webp", false, true, false),
HEIC("heic", "image/heic", false, true, false),
HEIF("heif", "image/heif", false, true, false),
MPD("mpd", "text/xml", false, false, false), MPD("mpd", "text/xml", false, false, false),
UNKNOWN("dat", "application/octet-stream", false, false, false); UNKNOWN("dat", "application/octet-stream", false, false, false);
@@ -64,6 +66,11 @@ enum class FileType(
} }
val majorBrand = String(array, 8, 4, Charsets.US_ASCII).trim('\u0000').lowercase() val majorBrand = String(array, 8, 4, Charsets.US_ASCII).trim('\u0000').lowercase()
// Explicitly exclude known IMAGE-only brands to prevent false positives (HEIC/HEIF)
val imageBrands = setOf("heic", "heix", "hevc", "hevx", "mif1", "msf1")
if (majorBrand in imageBrands) return false
return majorBrand in setOf( return majorBrand in setOf(
"mp41", "mp41",
"mp42", "mp42",
@@ -76,14 +83,13 @@ enum class FileType(
"avc1", "avc1",
"dash", "dash",
"cmfc", "cmfc",
"mif1",
"msnv", "msnv",
"3gp4", "3gp4",
"3gp5", "3gp5",
"3gp6", "3gp6",
"3g2a", "3g2a",
"3g2b" "3g2b"
) || majorBrand.isNotEmpty() // FALLBACK: If it has the ftyp box, it's a video ) || majorBrand.isNotEmpty() // FALLBACK: If it has the ftyp box and isn't a known image brand, it's a video
} }
fun fromFile(file: File): FileType { fun fromFile(file: File): FileType {
@@ -98,8 +104,24 @@ enum class FileType(
val headerBytes = ByteArray(16) val headerBytes = ByteArray(16)
System.arraycopy(array, 0, headerBytes, 0, 16) System.arraycopy(array, 0, headerBytes, 0, 16)
val hex = bytesToHex(headerBytes) val hex = bytesToHex(headerBytes)
return fileSignatures.entries.firstOrNull { hex.startsWith(it.key) }?.value
?: if (looksLikeIsoBmffVideo(headerBytes)) MP4 else UNKNOWN // 1. Check strict signatures
fileSignatures.entries.firstOrNull { hex.startsWith(it.key) }?.value?.let { return it }
// 2. Check ISO BMFF container type
val majorBrand = if (headerBytes.size >= 12 &&
headerBytes[4] == 'f'.code.toByte() && headerBytes[5] == 't'.code.toByte() &&
headerBytes[6] == 'y'.code.toByte() && headerBytes[7] == 'p'.code.toByte()) {
String(headerBytes, 8, 4, Charsets.US_ASCII).trim('\u0000').lowercase()
} else null
if (majorBrand != null) {
if (majorBrand in setOf("heic", "heix")) return HEIC
if (majorBrand in setOf("mif1", "msf1")) return HEIF
if (looksLikeIsoBmffVideo(headerBytes)) return MP4
}
return UNKNOWN
} }
fun fromInputStream(inputStream: InputStream): FileType { fun fromInputStream(inputStream: InputStream): FileType {