diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt index 79c8af98..5d5f6c99 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt @@ -493,7 +493,7 @@ class GeckoWeb : BWebview { override fun onExternalResponse(session: GeckoSession, response: WebResponse) { Blog.LOGE("response >>> ${response.uri} ") if (response.uri.contains(".apk")) return - + val url = response.uri val filename = "${url.substringAfterLast("/")}_${System.currentTimeMillis()}.mp4" // 파일명 추출, 없으면 URL에서 가져옴 diff --git a/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt b/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt index e639c70b..1c378689 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt @@ -12,6 +12,8 @@ import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import java.io.File +import java.net.URLConnection +import java.util.regex.Pattern object CommonUtils { fun dpToPx(context: Context, dp: Float): Int { @@ -27,48 +29,90 @@ object CommonUtils { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP } - fun getFileTypeBySignature(file: File): String? { - val buffer = ByteArray(12) // 동영상 포맷 확인용으로 충분히 크게 버퍼 확장 - file.inputStream().use { it.read(buffer, 0, buffer.size) } - // GIF - if (buffer.sliceArray(0..5).contentEquals("GIF87a".toByteArray()) || - buffer.sliceArray(0..5).contentEquals("GIF89a".toByteArray())) { - return "gif" + + fun getFileTypeBySignature(file: File): String? { + val buffer = ByteArray(12) + val readBytes = file.inputStream().use { it.read(buffer, 0, buffer.size) } + + // 1. 시그니처 검사 (기존 방식) + if (readBytes >= 6) { + val gifSignature1 = "GIF87a".toByteArray(Charsets.US_ASCII) + val gifSignature2 = "GIF89a".toByteArray(Charsets.US_ASCII) + if (buffer.copyOfRange(0, 6).contentEquals(gifSignature1) + || buffer.copyOfRange(0, 6).contentEquals(gifSignature2) + ) { + return "gif" + } } - // JPEG - if (buffer[0] == 0xFF.toByte() && buffer[1] == 0xD8.toByte() && buffer[2] == 0xFF.toByte()) { + + if (readBytes >= 3 && + buffer[0] == 0xFF.toByte() && buffer[1] == 0xD8.toByte() && buffer[2] == 0xFF.toByte() + ) { return "jpg" } - // PNG - if (buffer.sliceArray(0..7).contentEquals(byteArrayOf(0x89.toByte(),'P'.toByte(),'N'.toByte(),'G'.toByte(),0x0D,0x0A,0x1A,0x0A))) { + + if (readBytes >= 8 && + buffer.copyOfRange(0, 8).contentEquals( + byteArrayOf( + 0x89.toByte(), 'P'.toByte(), 'N'.toByte(), 'G'.toByte(), + 0x0D, 0x0A, 0x1A, 0x0A + ) + ) + ) { return "png" } - // MP4 (ftyp... 브랜드 체크, 보통 4~8바이트 확인) - // "ftyp"는 4바이트 offset 4 ~ 7에 위치 - if (buffer.size >= 12 && buffer.sliceArray(4..7).contentEquals("ftyp".toByteArray())) { + if (readBytes >= 12 && + buffer.copyOfRange(4, 8).contentEquals("ftyp".toByteArray(Charsets.US_ASCII)) + ) { return "mp4" } - // AVI (큰 헤더 "RIFF....AVI ") - if (buffer.size >= 12 && - buffer.sliceArray(0..3).contentEquals("RIFF".toByteArray()) && - buffer.sliceArray(8..11).contentEquals("AVI ".toByteArray())) { + if (readBytes >= 12 && + buffer.copyOfRange(0, 4).contentEquals("RIFF".toByteArray(Charsets.US_ASCII)) && + buffer.copyOfRange(8, 12).contentEquals("AVI ".toByteArray(Charsets.US_ASCII)) + ) { return "avi" } - // MKV (Matroska) - EBML 헤더: 0x1A 0x45 0xDF 0xA3 - if (buffer.size >= 4 && - buffer[0] == 0x1A.toByte() && buffer[1] == 0x45.toByte() && - buffer[2] == 0xDF.toByte() && buffer[3] == 0xA3.toByte()) { + if (readBytes >= 4 && + buffer[0] == 0x1A.toByte() && + buffer[1] == 0x45.toByte() && + buffer[2] == 0xDF.toByte() && + buffer[3] == 0xA3.toByte() + ) { return "mkv" } + // 2. MIME 타입 검사 (Optional, 정확도 향상) + val mimeType = URLConnection.guessContentTypeFromStream(file.inputStream()) + if (mimeType != null) { + when { + mimeType.equals("image/gif", ignoreCase = true) -> return "gif" + mimeType.equals("image/jpeg", ignoreCase = true) -> return "jpg" + mimeType.equals("image/png", ignoreCase = true) -> return "png" + mimeType.startsWith("video/mp4", ignoreCase = true) -> return "mp4" + mimeType.startsWith("video/x-msvideo", ignoreCase = true) -> return "avi" + mimeType.startsWith("video/x-matroska", ignoreCase = true) -> return "mkv" + } + } + + // 3. 정규식 기반 시그니처 검사 (GIF 전용 예) + if (readBytes >= 6) { + val sigStr = String(buffer, 0, 6, Charsets.US_ASCII) + val gifPattern = Pattern.compile("GIF8[79]a") + if (gifPattern.matcher(sigStr).matches()) { + return "gif" + } + } + return null } + + fun downloadFileWithOkHttp(context: Context,refferer : Uri, fileUrl: String) { android.app.AlertDialog.Builder(context) .setTitle("파일 다운로드") @@ -95,15 +139,12 @@ object CommonUtils { ).show() } } else { - - // Content-Type에서 확장자 추출 val contentType = response.header("Content-Type") Blog.LOGE("downloadFileWithOkHttp contentType $contentType") var extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } - // 확장자 없으면 URL에서 추출하거나 기본값 "dat" 사용 if (extension.isNullOrBlank()) { extension = MimeTypeMap.getFileExtensionFromUrl(fileUrl) @@ -112,9 +153,6 @@ object CommonUtils { } } Blog.LOGE("downloadFileWithOkHttp extension $extension") -// if (extension.equals("bin")){ -//// extension = "jpg" -// } val fileName = "${ refferer.host?.replace( @@ -133,6 +171,7 @@ object CommonUtils { val resultFile = if (extension in listOf("bin", "dat")) { val realExt = getFileTypeBySignature(file) + Blog.LOGE("downloadFileWithOkHttp extension $realExt") when { realExt.isNullOrBlank() -> file realExt == extension -> file @@ -142,7 +181,7 @@ object CommonUtils { ",", "_" ) - }_${System.currentTimeMillis()}.$extension") + }_${System.currentTimeMillis()}.$realExt") if (file.renameTo(newFile)) newFile else file } }