@@ -6,40 +6,29 @@ import to.bitkit.BuildConfig
66import to.bitkit.data.ChatwootHttpClient
77import to.bitkit.di.BgDispatcher
88import to.bitkit.env.Env
9+ import to.bitkit.ext.getEnumValueOf
910import to.bitkit.models.ChatwootMessage
1011import to.bitkit.utils.Logger
11- import to.bitkit.viewmodels.LogFile
1212import java.io.BufferedReader
13+ import java.io.ByteArrayOutputStream
1314import java.io.File
15+ import java.io.FileInputStream
1416import java.io.FileReader
1517import java.util.Base64
18+ import java.util.zip.ZipEntry
19+ import java.util.zip.ZipOutputStream
1620import javax.inject.Inject
1721import javax.inject.Singleton
1822
1923@Singleton
2024class LogsRepo @Inject constructor(
2125 @BgDispatcher private val bgDispatcher : CoroutineDispatcher ,
2226 private val chatwootHttpClient : ChatwootHttpClient
23-
2427) {
25-
2628 suspend fun postQuestion (email : String , message : String ): Result <Unit > = withContext(bgDispatcher) {
2729 return @withContext try {
28-
29- val lastLog = getLogs().getOrNull()?.lastOrNull()
30- val logFile = lastLog?.file
31-
32- var logsBase64 = " "
33- var logsFileName = " "
34-
35- if (logFile != null && logFile.exists()) {
36- val fileContent = logFile.readBytes()
37- logsBase64 = Base64 .getEncoder().encodeToString(fileContent)
38-
39- logsFileName = logFile.name.substringBeforeLast(" ." )
40- } else {
41- Logger .warn(" No log file found" , context = TAG )
42- }
30+ val logsBase64 = zipLogs().getOrDefault(" " )
31+ val logsFileName = " bitkit_logs_${System .currentTimeMillis()} .zip"
4332
4433 chatwootHttpClient.postQuestion(
4534 message = ChatwootMessage (
@@ -48,7 +37,7 @@ class LogsRepo @Inject constructor(
4837 platform = " ${Env .PLATFORM } ${Env .androidSDKVersion} " ,
4938 version = " ${BuildConfig .VERSION_NAME } ${BuildConfig .VERSION_CODE } " ,
5039 logs = logsBase64,
51- logsFileName = logsFileName
40+ logsFileName = logsFileName,
5241 )
5342 )
5443 Result .success(Unit )
@@ -58,6 +47,7 @@ class LogsRepo @Inject constructor(
5847 }
5948 }
6049
50+ /* * * Lists log files sorted by newest first */
6151 suspend fun getLogs (): Result <List <LogFile >> = withContext(bgDispatcher) {
6252 try {
6353 val logDir = File (Env .logDir)
@@ -73,13 +63,14 @@ class LogsRepo @Inject constructor(
7363
7464 val serviceName = components.firstOrNull()
7565 ?.let { c -> c.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } }
76- ? : " Unknown"
66+ ? : LogSource . Unknown .name
7767 val timestamp = if (components.size >= 3 ) components[components.size - 2 ] else " "
7868 val displayName = " $serviceName Log: $timestamp "
7969
8070 LogFile (
8171 displayName = displayName,
8272 file = file,
73+ source = getEnumValueOf<LogSource >(serviceName).getOrDefault(LogSource .Unknown ),
8374 )
8475 }
8576 ?.sortedByDescending { it.file.lastModified() }
@@ -112,7 +103,75 @@ class LogsRepo @Inject constructor(
112103 }
113104 }
114105
106+ /* * Zips up the most recent logs and returns base64 of zip file */
107+ suspend fun zipLogs (
108+ limit : Int = 20,
109+ includeAllSources : Boolean = false
110+ ): Result <String > = withContext(bgDispatcher) {
111+ return @withContext try {
112+ val logsResult = getLogs()
113+ if (logsResult.isFailure) {
114+ return @withContext Result .failure(logsResult.exceptionOrNull() ? : Exception (" Failed to get logs" ))
115+ }
116+
117+ val allLogs = logsResult.getOrNull()?.filter { it.source != LogSource .Unknown } ? : emptyList()
118+ val logsToZip = if (includeAllSources) {
119+ allLogs.take(limit)
120+ } else {
121+ // Group by source and take most recent from each
122+ allLogs.groupBy { it.source }
123+ .values
124+ .flatMap { logs ->
125+ val sourcesCount = LogSource .entries.filter { it != LogSource .Unknown }.size
126+ logs.take(limit / sourcesCount.coerceAtLeast(1 ))
127+ }
128+ .take(limit)
129+ }
130+
131+ if (logsToZip.isEmpty()) {
132+ return @withContext Result .failure(Exception (" No log files found" ))
133+ }
134+
135+ val base64String = createZipBase64(logsToZip)
136+ Result .success(base64String)
137+ } catch (e: Exception ) {
138+ Logger .error(" Failed to zip logs" , e, context = TAG )
139+ Result .failure(e)
140+ }
141+ }
142+
143+ private fun createZipBase64 (logFiles : List <LogFile >): String {
144+ val zipBytes = ByteArrayOutputStream ().use { byteArrayOut ->
145+ ZipOutputStream (byteArrayOut).use { zipOut ->
146+ logFiles.forEach { logFile ->
147+ if (logFile.file.exists()) {
148+ val zipEntry = ZipEntry (" ${logFile.source.name.lowercase()} /${logFile.fileName} " )
149+ zipOut.putNextEntry(zipEntry)
150+
151+ FileInputStream (logFile.file).use { fileIn ->
152+ fileIn.copyTo(zipOut)
153+ }
154+ zipOut.closeEntry()
155+ }
156+ }
157+ }
158+ byteArrayOut.toByteArray()
159+ }
160+
161+ return Base64 .getEncoder().encodeToString(zipBytes)
162+ }
163+
115164 private companion object {
116165 const val TAG = " SupportRepo"
117166 }
118167}
168+
169+ data class LogFile (
170+ val displayName : String ,
171+ val file : File ,
172+ val source : LogSource ,
173+ ) {
174+ val fileName: String get() = file.name
175+ }
176+
177+ enum class LogSource { Ldk , Bitkit , Unknown }
0 commit comments