WIP: Record logs in SQLite

This commit is contained in:
Philipp Heckel 2022-01-17 00:19:05 -05:00
parent 22eeb7c719
commit 0968a39420
26 changed files with 216 additions and 58 deletions

View file

@ -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')"
]
}
}

View file

@ -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())

View file

@ -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)
}

View file

@ -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

View file

@ -0,0 +1,81 @@
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
}
}
}
}

View file

@ -1,9 +1,9 @@
package io.heckel.ntfy.msg
import android.os.Build
import android.util.Log
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.util.*
import okhttp3.*
import okhttp3.RequestBody.Companion.toRequestBody

View file

@ -2,10 +2,10 @@ package io.heckel.ntfy.msg
import android.content.Context
import android.content.Intent
import android.util.Log
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.util.joinTagsMap
import io.heckel.ntfy.util.splitTags
import kotlinx.coroutines.Dispatchers

View file

@ -1,11 +1,11 @@
package io.heckel.ntfy.msg
import android.content.Context
import android.util.Log
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.workDataOf
import io.heckel.ntfy.log.Log
/**
* Download attachment in the background via WorkManager

View file

@ -25,7 +25,6 @@ import okhttp3.Response
import java.io.File
import java.util.concurrent.TimeUnit
class DownloadWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
private val client = OkHttpClient.Builder()
.callTimeout(15, TimeUnit.MINUTES) // Total timeout for entire request

View file

@ -6,20 +6,15 @@ import android.graphics.BitmapFactory
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.heckel.ntfy.R
import io.heckel.ntfy.data.*
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.ui.DetailActivity
import io.heckel.ntfy.ui.DetailAdapter
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class NotificationService(val context: Context) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

View file

@ -0,0 +1,8 @@
package io.heckel.ntfy.service
interface Connection {
fun start()
fun close()
fun since(): Long
fun matches(otherSubscriptionIds: Collection<Long>): Boolean
}

View file

@ -8,7 +8,6 @@ import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.os.SystemClock
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.heckel.ntfy.BuildConfig
@ -17,6 +16,7 @@ import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.ConnectionState
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationDispatcher
import io.heckel.ntfy.ui.MainActivity
@ -26,7 +26,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
/**
* The subscriber service manages the foreground service for instant delivery.
*
@ -55,14 +54,6 @@ import java.util.concurrent.ConcurrentHashMap
* - https://github.com/robertohuertasm/endless-service/blob/master/app/src/main/java/com/robertohuertas/endless/EndlessService.kt
* - https://gist.github.com/varunon9/f2beec0a743c96708eb0ef971a9ff9cd
*/
interface Connection {
fun start()
fun close()
fun since(): Long
fun matches(otherSubscriptionIds: Collection<Long>): Boolean
}
class SubscriberService : Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false

View file

@ -2,10 +2,10 @@ package io.heckel.ntfy.service
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.work.*
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.log.Log
/**
* This class only manages the SubscriberService, i.e. it starts or stops it.

View file

@ -4,11 +4,11 @@ import android.app.AlarmManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import io.heckel.ntfy.data.ConnectionState
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.msg.NotificationParser
import io.heckel.ntfy.util.topicUrl
import io.heckel.ntfy.util.topicUrlWs

View file

@ -6,7 +6,6 @@ import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.widget.*
import androidx.fragment.app.DialogFragment
@ -15,12 +14,10 @@ import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Database
import io.heckel.ntfy.data.Repository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AddFragment : DialogFragment() {
private lateinit var repository: Repository
private lateinit var subscribeListener: SubscribeListener

View file

@ -6,7 +6,6 @@ import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.text.Html
import android.util.Log
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
@ -24,6 +23,7 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.service.SubscriberServiceManager

View file

@ -8,7 +8,6 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -21,6 +20,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.stfalcon.imageviewer.StfalconImageViewer
import io.heckel.ntfy.R
import io.heckel.ntfy.data.*
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.util.*
import kotlinx.coroutines.Dispatchers

View file

@ -1,19 +1,18 @@
package io.heckel.ntfy.ui
import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
@ -22,15 +21,19 @@ import androidx.work.*
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.work.PollWorker
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.*
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationDispatcher
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.fadeStatusBarColor
import io.heckel.ntfy.util.formatDateShort
import kotlinx.coroutines.*
import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.work.PollWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.random.Random
@ -60,6 +63,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.init(this)
Log.setRecord(true)
Log.d(TAG, "Create $this")
// Dependencies that depend on Context

View file

@ -4,7 +4,6 @@ import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.widget.RadioButton
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope

View file

@ -1,12 +1,14 @@
package io.heckel.ntfy.ui
import android.Manifest
import android.content.*
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
@ -16,9 +18,8 @@ import androidx.preference.*
import androidx.preference.Preference.OnPreferenceClickListener
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Database
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.util.formatBytes
import io.heckel.ntfy.util.formatDateShort

View file

@ -2,11 +2,10 @@ package io.heckel.ntfy.up
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.preference.PreferenceManager
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.randomString
import io.heckel.ntfy.util.topicUrlUp
@ -25,6 +24,7 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
if (context == null || intent == null) {
return
}
Log.init(context) // Init in all entrypoints
when (intent.action) {
ACTION_REGISTER -> register(context, intent)
ACTION_UNREGISTER -> unregister(context, intent)

View file

@ -6,17 +6,14 @@ package io.heckel.ntfy.up
*/
const val ACTION_NEW_ENDPOINT = "org.unifiedpush.android.connector.NEW_ENDPOINT"
const val ACTION_REGISTRATION_FAILED = "org.unifiedpush.android.connector.REGISTRATION_FAILED"
const val ACTION_REGISTRATION_REFUSED = "org.unifiedpush.android.connector.REGISTRATION_REFUSED"
const val ACTION_UNREGISTERED = "org.unifiedpush.android.connector.UNREGISTERED"
const val ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE"
const val ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER"
const val ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER"
const val ACTION_MESSAGE_ACK = "org.unifiedpush.android.distributor.MESSAGE_ACK"
const val EXTRA_APPLICATION = "application"
const val EXTRA_TOKEN = "token"
const val EXTRA_ENDPOINT = "endpoint"
const val EXTRA_MESSAGE = "message"
const val EXTRA_MESSAGE_ID = "id"

View file

@ -2,7 +2,7 @@ package io.heckel.ntfy.up
import android.content.Context
import android.content.Intent
import android.util.Log
import io.heckel.ntfy.log.Log
/**
* This is the UnifiedPush distributor, an amalgamation of messages to be sent as part of the spec.

View file

@ -1,17 +1,14 @@
package io.heckel.ntfy.work
import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.data.Database
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.firebase.FirebaseService
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.BroadcastService
import io.heckel.ntfy.msg.NotificationDispatcher
import io.heckel.ntfy.msg.NotificationService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.random.Random
@ -21,7 +18,12 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
// Every time the worker is changed, the periodic work has to be REPLACEd.
// This is facilitated in the MainActivity using the VERSION below.
init {
Log.init(ctx) // Init in all entrypoints
}
override suspend fun doWork(): Result {
return withContext(Dispatchers.IO) {
Log.d(TAG, "Polling for new notifications")
val database = Database.getInstance(applicationContext)

View file

@ -1,7 +1,7 @@
package io.heckel.ntfy.firebase
import android.util.Log
import com.google.firebase.messaging.FirebaseMessaging
import io.heckel.ntfy.log.Log
class FirebaseMessenger {
fun subscribe(topic: String) {

View file

@ -1,7 +1,6 @@
package io.heckel.ntfy.firebase
import android.content.Intent
import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.heckel.ntfy.R
@ -9,6 +8,7 @@ import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Attachment
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.PROGRESS_NONE
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.msg.*
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.util.toPriority
@ -23,6 +23,10 @@ class FirebaseService : FirebaseMessagingService() {
private val job = SupervisorJob()
private val messenger = FirebaseMessenger()
init {
Log.init(this) // Init in all entrypoints
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
// We only process data messages
if (remoteMessage.data.isEmpty()) {