Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0968a394 authored by Philipp Heckel's avatar Philipp Heckel
Browse files

WIP: Record logs in SQLite

parent 22eeb7c7
Loading
Loading
Loading
Loading
+52 −2
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@
  "formatVersion": 1,
  "database": {
    "version": 6,
    "identityHash": "fc725df9153ee7088ae8024428b7f2cf",
    "identityHash": "09ecfdb757b0f7643ad010fca9a0ed43",
    "entities": [
      {
        "tableName": "Subscription",
@@ -195,12 +195,62 @@
        },
        "indices": [],
        "foreignKeys": []
      },
      {
        "tableName": "Logs",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `tag` TEXT NOT NULL, `level` INTEGER NOT NULL, `message` TEXT NOT NULL, `exception` TEXT)",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "timestamp",
            "columnName": "timestamp",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "tag",
            "columnName": "tag",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "level",
            "columnName": "level",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "message",
            "columnName": "message",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "exception",
            "columnName": "exception",
            "affinity": "TEXT",
            "notNull": false
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [],
        "foreignKeys": []
      }
    ],
    "views": [],
    "setupQueries": [
      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fc725df9153ee7088ae8024428b7f2cf')"
      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '09ecfdb757b0f7643ad010fca9a0ed43')"
    ]
  }
}
 No newline at end of file
+5 −1
Original line number Diff line number Diff line
@@ -4,9 +4,13 @@ import android.app.Application
import android.content.Context
import io.heckel.ntfy.data.Database
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.log.Log

class Application : Application() {
    private val database by lazy { Database.getInstance(this) }
    private val database by lazy {
        Log.init(this) // What a hack, but this is super early and used everywhere
        Database.getInstance(this)
    }
    val repository by lazy {
        val sharedPrefs = applicationContext.getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE)
        Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao())
+25 −1
Original line number Diff line number Diff line
@@ -77,10 +77,24 @@ const val PROGRESS_FAILED = -3
const val PROGRESS_DELETED = -4
const val PROGRESS_DONE = 100

@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 6)
@Entity
data class Logs(
    @PrimaryKey(autoGenerate = true) val id: Long, // Internal ID, only used in Repository and activities
    @ColumnInfo(name = "timestamp") val timestamp: Long,
    @ColumnInfo(name = "tag") val tag: String,
    @ColumnInfo(name = "level") val level: Int,
    @ColumnInfo(name = "message") val message: String,
    @ColumnInfo(name = "exception") val exception: String?
) {
    constructor(timestamp: Long, tag: String, level: Int, message: String, exception: String?) :
            this(0, timestamp, tag, level, message, exception)
}

@androidx.room.Database(entities = [Subscription::class, Notification::class, Logs::class], version = 6)
abstract class Database : RoomDatabase() {
    abstract fun subscriptionDao(): SubscriptionDao
    abstract fun notificationDao(): NotificationDao
    abstract fun logsDao(): LogsDao

    companion object {
        @Volatile
@@ -261,3 +275,13 @@ interface NotificationDao {
    @Query("DELETE FROM notification WHERE subscriptionId = :subscriptionId")
    fun removeAll(subscriptionId: Long)
}


@Dao
interface LogsDao {
    @Insert
    suspend fun insert(entry: Logs)

    @Query("DELETE FROM logs WHERE id NOT IN (SELECT id FROM logs ORDER BY id DESC LIMIT :keepCount)")
    suspend fun prune(keepCount: Int)
}
+1 −1
Original line number Diff line number Diff line
@@ -4,9 +4,9 @@ import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.lifecycle.*
import io.heckel.ntfy.log.Log
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong

+81 −0
Original line number Diff line number Diff line
package io.heckel.ntfy.log

import android.content.Context
import io.heckel.ntfy.data.Database
import io.heckel.ntfy.data.Logs
import io.heckel.ntfy.data.LogsDao
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger

class Log(private val logsDao: LogsDao) {
    private var record: AtomicBoolean = AtomicBoolean(false)
    private var count: AtomicInteger = AtomicInteger(0)

    private fun log(level: Int, tag: String, message: String, exception: Throwable?) {
        if (!record.get()) return
        GlobalScope.launch(Dispatchers.IO) {
            logsDao.insert(Logs(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
            }
        }
    }

    companion object {
        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 init(context: Context): Log {
            return synchronized(Log::class) {
                if (instance == null) {
                    val database = Database.getInstance(context.applicationContext)
                    instance = Log(database.logsDao())
                }
                instance!!
            }
        }

        private const val TAG = "NtfyLog"
        private const val PRUNE_EVERY = 100
        private const val ENTRIES_MAX = 10000
        private var instance: Log? = null

        private fun getInstance(): Log? {
            return synchronized(Log::class) {
                instance
            }
        }
    }
}
Loading