ntfy-android/app/src/main/java/io/heckel/ntfy/log/Log.kt
2022-01-18 22:07:49 -05:00

184 lines
6.7 KiB
Kotlin

package io.heckel.ntfy.log
import android.content.Context
import android.os.Build
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.db.Database
import io.heckel.ntfy.db.LogDao
import io.heckel.ntfy.db.LogEntry
import io.heckel.ntfy.util.isIgnoringBatteryOptimizations
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class Log(private val logsDao: LogDao) {
private val record: AtomicBoolean = AtomicBoolean(false)
private val count: AtomicInteger = AtomicInteger(0)
private val scrubNum: AtomicInteger = AtomicInteger(-1)
private val scrubTerms = Collections.synchronizedMap(mutableMapOf<String, String>())
private fun log(level: Int, tag: String, message: String, exception: Throwable?) {
if (!record.get()) return
GlobalScope.launch(Dispatchers.IO) { // FIXME This does not guarantee the log order
logsDao.insert(LogEntry(System.currentTimeMillis(), tag, level, message, exception?.stackTraceToString()))
val current = count.incrementAndGet()
if (current >= PRUNE_EVERY) {
logsDao.prune(ENTRIES_MAX)
count.set(0) // I know there is a race here, but this is good enough
}
}
}
fun getFormatted(): String {
return prependDeviceInfo(formatEntries(scrubEntries(logsDao.getAll())))
}
private fun prependDeviceInfo(s: String): String {
return """
This is a log of the ntfy Android app. The log shows up to 5,000 lines.
Server URLs (aside from ntfy.sh) and topics have been replaced with fruits 🍌🥝🍋🥥🥑🍊🍎🍑.
Device info:
--
ntfy: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR})
OS: ${System.getProperty("os.version")}
Android: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
Model: ${Build.DEVICE}
Product: ${Build.PRODUCT}
---
""".trimIndent() + "\n\n$s"
}
fun addScrubTerm(term: String, type: TermType = TermType.Term) {
if (scrubTerms[term] != null || IGNORE_TERMS.contains(term)) {
return
}
val replaceTermIndex = scrubNum.incrementAndGet()
val replaceTerm = REPLACE_TERMS.getOrNull(replaceTermIndex) ?: "fruit${replaceTermIndex}"
scrubTerms[term] = when (type) {
TermType.Domain -> "$replaceTerm.example.com"
else -> replaceTerm
}
}
private fun scrubEntries(entries: List<LogEntry>): List<LogEntry> {
return entries
.map { e ->
e.copy(
message = scrub(e.message)!!,
exception = scrub(e.exception)
)
}
}
private fun scrub(line: String?): String? {
var newLine = line ?: return null
scrubTerms.forEach { (scrubTerm, replaceTerm) ->
newLine = newLine.replace(scrubTerm, replaceTerm)
}
return newLine
}
private fun formatEntries(entries: List<LogEntry>): String {
return entries.joinToString(separator = "\n") { e ->
val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(Date(e.timestamp))
val level = when (e.level) {
android.util.Log.DEBUG -> "D"
android.util.Log.INFO -> "I"
android.util.Log.WARN -> "W"
android.util.Log.ERROR -> "E"
else -> "?"
}
val tag = e.tag.format("%-23s")
val prefix = "${e.timestamp} $date $level $tag"
val message = if (e.exception != null) {
"${e.message}\nException:\n${e.exception}"
} else {
e.message
}
"$prefix $message"
}
}
private fun deleteAll() {
return logsDao.deleteAll()
}
enum class TermType {
Domain, Term
}
companion object {
private const val TAG = "NtfyLog"
private const val PRUNE_EVERY = 100
private const val ENTRIES_MAX = 5000
private val IGNORE_TERMS = listOf("ntfy.sh")
private val REPLACE_TERMS = listOf(
"banana", "kiwi", "lemon", "coconut", "avocado", "orange", "apple", "peach"
)
private var instance: Log? = null
fun d(tag: String, message: String, exception: Throwable? = null) {
if (exception == null) android.util.Log.d(tag, message) else android.util.Log.d(tag, message, exception)
getInstance()?.log(android.util.Log.DEBUG, tag, message, exception)
}
fun i(tag: String, message: String, exception: Throwable? = null) {
if (exception == null) android.util.Log.i(tag, message) else android.util.Log.i(tag, message, exception)
getInstance()?.log(android.util.Log.INFO, tag, message, exception)
}
fun w(tag: String, message: String, exception: Throwable? = null) {
if (exception == null) android.util.Log.w(tag, message) else android.util.Log.w(tag, message, exception)
getInstance()?.log(android.util.Log.WARN, tag, message, exception)
}
fun e(tag: String, message: String, exception: Throwable? = null) {
if (exception == null) android.util.Log.e(tag, message) else android.util.Log.e(tag, message, exception)
getInstance()?.log(android.util.Log.ERROR, tag, message, exception)
}
fun setRecord(enable: Boolean) {
if (!enable) d(TAG, "Disabled log recording")
getInstance()?.record?.set(enable)
if (enable) d(TAG, "Enabled log recording")
}
fun getRecord(): Boolean {
return getInstance()?.record?.get() ?: false
}
fun getFormatted(): String {
return getInstance()?.getFormatted() ?: "(no logs)"
}
fun deleteAll() {
getInstance()?.deleteAll()
d(TAG, "Log was truncated")
}
fun addScrubTerm(term: String, type: TermType = TermType.Term) {
getInstance()?.addScrubTerm(term, type)
}
fun init(context: Context) {
return synchronized(Log::class) {
if (instance == null) {
val database = Database.getInstance(context.applicationContext)
instance = Log(database.logDao())
}
}
}
private fun getInstance(): Log? {
return synchronized(Log::class) {
instance
}
}
}
}