Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-24 22:18:50 +03:00
parent a774347c70
commit a71e17561f
4 changed files with 56 additions and 10 deletions

View file

@ -4,18 +4,15 @@ import android.content.Context
import com.tangem.datasource.BuildConfig import com.tangem.datasource.BuildConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.joda.time.DateTime import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormatterBuilder import org.joda.time.format.DateTimeFormatterBuilder
import timber.log.Timber import timber.log.Timber
import java.io.BufferedWriter import java.io.*
import java.io.File import java.util.zip.ZipEntry
import java.io.FileWriter import java.util.zip.ZipOutputStream
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -30,7 +27,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class AppLogsStore @Inject constructor( class AppLogsStore @Inject constructor(
@ApplicationContext private val applicationContext: Context, @ApplicationContext private val applicationContext: Context,
dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) { ) {
private val scope = CoroutineScope( private val scope = CoroutineScope(
@ -38,8 +35,10 @@ class AppLogsStore @Inject constructor(
CoroutineExceptionHandler { _, error -> Timber.e("AppLogsStore.scope is failed $error") }, CoroutineExceptionHandler { _, error -> Timber.e("AppLogsStore.scope is failed $error") },
) )
private val mutex = Mutex() private val mutex = Mutex()
private val zipMutex = Mutex()
private val file = File(applicationContext.filesDir, NEW_LOG_FILE_NAME) private val file = File(applicationContext.filesDir, PERMITTED_FILE_NAME)
private val fileZip = File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP)
private val formatter = DateTimeFormatterBuilder() private val formatter = DateTimeFormatterBuilder()
.appendDayOfMonth(2) .appendDayOfMonth(2)
@ -58,6 +57,16 @@ class AppLogsStore @Inject constructor(
/** Get log file */ /** Get log file */
fun getFile(): File? = if (file.exists()) file else null fun getFile(): File? = if (file.exists()) file else null
suspend fun getZipFile(): File? {
return zipMutex.withLock {
if (file.exists()) {
zip(listOf(file), fileZip)
} else {
null
}
}
}
/** Save log [message] */ /** Save log [message] */
fun saveLogMessage(tag: String, message: String) { fun saveLogMessage(tag: String, message: String) {
// Temporally logs are not saved in prod environment // Temporally logs are not saved in prod environment
@ -133,8 +142,41 @@ class AppLogsStore @Inject constructor(
} }
} }
@Suppress("NestedBlockDepth")
private suspend fun zip(filesToCompress: List<File>, outputZipFile: File): File? {
return withContext(dispatchers.io) {
if (outputZipFile.exists() && !outputZipFile.delete()) {
return@withContext null
}
val buffer = ByteArray(BUFFER_SIZE)
FileOutputStream(outputZipFile).use { fos ->
ZipOutputStream(fos).use { zos ->
filesToCompress.forEach { file ->
FileInputStream(file).use { inStream ->
val ze = ZipEntry(file.name)
zos.putNextEntry(ze)
var len: Int
while (inStream.read(buffer).also { len = it } > 0) {
zos.write(buffer, 0, len)
}
}
}
zos.finish() // Ensures the zip output is finalized
}
}
outputZipFile
}
}
private companion object { private companion object {
const val BUFFER_SIZE = 1024
const val LOG_FILE_NAME = "logs.txt" const val LOG_FILE_NAME = "logs.txt"
const val NEW_LOG_FILE_NAME = "app_logs.txt" const val NEW_LOG_FILE_NAME = "app_logs.txt"
// the only name that we allow to send as email to company addresses
const val PERMITTED_FILE_NAME = "log.txt"
const val PERMITTED_FILE_NAME_ZIP = "log.zip"
} }
} }

View file

@ -94,6 +94,8 @@ internal class DefaultFeedbackRepository(
override fun getLogFile(): File? = appLogsStore.getFile() override fun getLogFile(): File? = appLogsStore.getFile()
override suspend fun getZipLogFile(): File? = appLogsStore.getZipFile()
override fun sendEmail(feedbackEmail: FeedbackEmail) { override fun sendEmail(feedbackEmail: FeedbackEmail) {
emailSender.send( emailSender.send(
EmailSender.Email( EmailSender.Email(

View file

@ -31,7 +31,7 @@ class SendFeedbackEmailUseCase(
subject = emailSubjectResolver.resolve(type), subject = emailSubjectResolver.resolve(type),
message = createMessage(type), message = createMessage(type),
// Temporally user data is not sent // Temporally user data is not sent
file = null, // feedbackRepository.getLogFile(), file = null, // feedbackRepository.getZipLogFile(),
) )
feedbackRepository.sendEmail(email) feedbackRepository.sendEmail(email)

View file

@ -27,5 +27,7 @@ interface FeedbackRepository {
fun getLogFile(): File? fun getLogFile(): File?
suspend fun getZipLogFile(): File?
fun sendEmail(feedbackEmail: FeedbackEmail) fun sendEmail(feedbackEmail: FeedbackEmail)
} }