Skip to content

Instantly share code, notes, and snippets.

@marius-m
Created November 11, 2022 14:44
Show Gist options
  • Save marius-m/d78fbc57cf22bb01a7baa93168f384f0 to your computer and use it in GitHub Desktop.
Save marius-m/d78fbc57cf22bb01a7baa93168f384f0 to your computer and use it in GitHub Desktop.
Logging export mechanism (With zip funct)
import android.content.Context
import android.os.Environment
import androidx.lifecycle.LifecycleCoroutineScope
import com.spotos.core.LogUtils.withLogInstance
import com.spotos.core.LogUtils.withLogKlass
import com.spotos.core.Tags
import com.spotos.data.SentryInteractor
import com.spotos.data.ViewProvider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class LogsExportDefault(
private val context: Context,
private val viewProvider: ViewProvider<LogsExport.View>,
private val sentryInteractor: SentryInteractor,
private val lifecycleCoroutineScope: LifecycleCoroutineScope,
private val dispatcherIo: CoroutineDispatcher = Dispatchers.IO,
private val dispatcherMain: CoroutineDispatcher = Dispatchers.Main,
) : LogsExport {
override fun exportLogs(logName: String) {
lifecycleCoroutineScope.launch {
Timber.tag(Tags.DEBUG).d(
"exportLogs.init(thread: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
)
viewProvider.get()?.showProgress()
withContext(dispatcherIo) {
try {
val dirInternal = internalDir(context, logName)
val dirExternal = externalDirWithCleanUp(context, logName)
Timber.tag(Tags.DEBUG).d(
"exportLogs.copy(thread: %s, target: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
dirExternal.absolutePath,
)
dirInternal.copyRecursively(target = dirExternal)
withContext(dispatcherMain) {
viewProvider.get()?.exportSuccessMessage(dirExternal.absolutePath)
}
} catch (e: Exception) {
withContext(dispatcherMain) {
Timber.tag(Tags.DEBUG).w(
e,
"exportLogs.error(thread: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
)
viewProvider.get()?.exportErrorMessage(e.message ?: "Unknown")
}
} finally {
withContext(dispatcherMain) {
Timber.tag(Tags.DEBUG).d(
"exportLogs.finally(thread: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
)
viewProvider.get()?.hideProgress()
}
}
}
}
}
override fun exportLogsAsZip(logName: String) {
lifecycleCoroutineScope.launch {
Timber.tag(Tags.DEBUG).d(
"exportLogsAsZip.init(thread: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
)
viewProvider.get()?.showProgress()
withContext(dispatcherIo) {
try {
val dirInternal = internalDir(context, logName)
val dirExternal = externalDirWithCleanUp(context, logName)
Timber.tag(Tags.DEBUG).d(
"exportLogsAsZip.copyInit(thread: %s, target: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
dirExternal.absolutePath,
)
val fileLogsAsZip = zipDir(dirInput = dirInternal, logName = logName)
val fileExternalTarget = File(dirExternal, "${logName}.zip")
fileLogsAsZip
.copyTo(target = fileExternalTarget, overwrite = true)
Timber.tag(Tags.DEBUG).d(
"exportLogsAsZip.copyResult(thread: %s, target: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
fileExternalTarget,
)
dirCleanZips(dirInput = dirInternal)
withContext(dispatcherMain) {
viewProvider.get()?.exportSuccessMessage(dirExternal.absolutePath)
}
} catch (e: Exception) {
withContext(dispatcherMain) {
Timber.tag(Tags.DEBUG).w(
e,
"exportLogsAsZip.error(thread: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
)
viewProvider.get()?.exportErrorMessage(e.message ?: "Unknown")
}
} finally {
withContext(dispatcherMain) {
Timber.tag(Tags.DEBUG).d(
"exportLogsAsZip.finally(thread: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
)
viewProvider.get()?.hideProgress()
}
}
}
}
}
override fun reportAsZip(logName: String) {
lifecycleCoroutineScope.launch {
Timber.tag(Tags.DEBUG).d(
"reportAsZip.init(thread: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
)
viewProvider.get()?.showProgress()
withContext(dispatcherIo) {
try {
val dirInternal = internalDir(context, logName)
val dirExternal = externalDirWithCleanUp(context, logName)
Timber.tag(Tags.DEBUG).d(
"reportAsZip.copyInit(thread: %s, target: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
dirExternal.absolutePath,
)
dirCleanZips(dirInput = dirInternal)
val fileLogsAsZip = zipDir(dirInput = dirInternal, logName = logName)
sentryInteractor.reportWithLogs("Manual report", fileLogsAsZip)
Timber.tag(Tags.DEBUG).d(
"reportAsZip.copyResult(thread: %s, target: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
fileLogsAsZip,
)
withContext(dispatcherMain) {
viewProvider.get()?.reportSuccessMessage(dirExternal.absolutePath)
}
} catch (e: Exception) {
withContext(dispatcherMain) {
Timber.tag(Tags.DEBUG).w(
e,
"reportAsZip.error(thread: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
)
viewProvider.get()?.reportErrorMessage(e.message ?: "Unknown")
}
} finally {
withContext(dispatcherMain) {
Timber.tag(Tags.DEBUG).d(
"reportAsZip.finally(thread: %s)".withLogInstance(this@LogsExportDefault),
Thread.currentThread(),
)
viewProvider.get()?.hideProgress()
}
}
}
}
}
companion object {
private fun internalDir(
context: Context,
logName: String,
): File {
return File(context.filesDir, logName)
}
@Throws(IllegalStateException::class)
private fun externalDirWithCleanUp(
context: Context,
logName: String,
): File {
val externalDirRoot = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
checkNotNull(externalDirRoot) { "External directory is null" }
val externalDirLogs = File(externalDirRoot, logName)
if (externalDirLogs.exists()) {
externalDirLogs.deleteRecursively()
}
if (!externalDirLogs.exists()) {
val mkdirResult = externalDirLogs.mkdirs()
check(mkdirResult) { "Nowhere to save logs!" }
}
return externalDirLogs
}
fun lastLogAsFile(
context: Context,
logName: String,
): File {
val dirInternal = internalDir(context, logName)
return File(dirInternal, "$logName.log")
}
/**
* Zips output directory and its contents
* Source: https://stackoverflow.com/questions/52215496/how-to-zip-folders-subfolders-with-files-in-it-in-kotlin-using-zipoutputstream
* @return zipped file
*/
private fun zipDir(
dirInput: File,
logName: String,
): File {
Timber.tag(Tags.DEBUG).d(
"zipDir.init(dirInput: %s, logName: %s)"
.withLogKlass(LogsExportDefault::class),
dirInput.absolutePath,
logName,
)
val fileZipOutput = File.createTempFile(logName, ".zip", dirInput)
ZipOutputStream(BufferedOutputStream(FileOutputStream(fileZipOutput))).use { zos ->
dirInput.walkTopDown().forEach { file ->
val zipFileName = file.absolutePath
.removePrefix(dirInput.absolutePath).removePrefix("/")
val zipFileNameSuffix = if (file.isDirectory) "/" else ""
val entry = ZipEntry( "$zipFileName$zipFileNameSuffix")
when {
file.extension.endsWith("zip") -> {
Timber.tag(Tags.DEBUG).d(
"zipDir.zip.ignoreZip(file: %s)"
.withLogKlass(LogsExportDefault::class),
file.absolutePath,
)
}
file.isFile -> {
Timber.tag(Tags.DEBUG).d(
"zipDir.zip.appendEntryFile(file: %s)"
.withLogKlass(LogsExportDefault::class),
file.absolutePath,
)
zos.putNextEntry(entry)
file.inputStream().use {
it.copyTo(zos)
}
}
file.isDirectory -> {
Timber.tag(Tags.DEBUG).d(
"zipDir.zip.appendEntryDir(file: %s)"
.withLogKlass(LogsExportDefault::class),
file.absolutePath,
)
zos.putNextEntry(entry)
}
}
}
}
Timber.tag(Tags.DEBUG).d(
"zipDir.finish(fileZipOutput: %s)"
.withLogKlass(LogsExportDefault::class),
fileZipOutput.absolutePath,
)
return fileZipOutput
}
/**
* Removes *.zip files from directory
*/
private fun dirCleanZips(
dirInput: File,
) {
Timber.tag(Tags.DEBUG).d(
"dirCleanZips.init(dirInput: %s)"
.withLogKlass(LogsExportDefault::class),
dirInput.absolutePath,
)
val filesMarkedDelete = dirInput.walkTopDown().mapNotNull { file ->
when {
file.extension.endsWith("zip") -> {
Timber.tag(Tags.DEBUG).d(
"dirCleanZips.markRemove(file: %s)"
.withLogKlass(LogsExportDefault::class),
file.absolutePath,
)
file
}
file.isFile -> {
Timber.tag(Tags.DEBUG).d(
"dirCleanZips.ignoreFile(file: %s)"
.withLogKlass(LogsExportDefault::class),
file.absolutePath,
)
null
}
file.isDirectory -> {
Timber.tag(Tags.DEBUG).d(
"dirCleanZips.ignoreDir(file: %s)"
.withLogKlass(LogsExportDefault::class),
file.absolutePath,
)
null
}
else -> null
}
}
filesMarkedDelete.forEach { file ->
Timber.tag(Tags.DEBUG).d(
"dirCleanZips.remove(file: %s)"
.withLogKlass(LogsExportDefault::class),
file.absolutePath,
)
file.delete()
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment