Copy logs to clipboard; scrub terms; add schema migration
This commit is contained in:
parent
5fb3ae0536
commit
7152469172
15 changed files with 562 additions and 100 deletions
|
@ -12,8 +12,8 @@ android {
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
|
|
||||||
versionCode 16
|
versionCode 17
|
||||||
versionName "1.6.0"
|
versionName "1.7.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ dependencies {
|
||||||
implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion"
|
implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion"
|
||||||
implementation "androidx.activity:activity-ktx:$rootProject.activityVersion"
|
implementation "androidx.activity:activity-ktx:$rootProject.activityVersion"
|
||||||
implementation "androidx.fragment:fragment-ktx:$rootProject.fragmentVersion"
|
implementation "androidx.fragment:fragment-ktx:$rootProject.fragmentVersion"
|
||||||
implementation 'com.google.code.gson:gson:2.8.8'
|
implementation 'com.google.code.gson:gson:2.8.9'
|
||||||
|
|
||||||
// WorkManager
|
// WorkManager
|
||||||
implementation "androidx.work:work-runtime-ktx:2.6.0"
|
implementation "androidx.work:work-runtime-ktx:2.6.0"
|
||||||
|
@ -76,7 +76,7 @@ dependencies {
|
||||||
kapt "androidx.room:room-compiler:$roomVersion"
|
kapt "androidx.room:room-compiler:$roomVersion"
|
||||||
|
|
||||||
// OkHttp (HTTP library)
|
// OkHttp (HTTP library)
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.9.2"
|
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
|
||||||
|
|
||||||
// Firebase, sigh ... (only Google Play)
|
// Firebase, sigh ... (only Google Play)
|
||||||
playImplementation 'com.google.firebase:firebase-messaging:22.0.0'
|
playImplementation 'com.google.firebase:firebase-messaging:22.0.0'
|
||||||
|
|
256
app/schemas/io.heckel.ntfy.data.Database/7.json
Normal file
256
app/schemas/io.heckel.ntfy.data.Database/7.json
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 7,
|
||||||
|
"identityHash": "ecb1b85b2ae822dc62b2843620368477",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Subscription",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `topic` TEXT NOT NULL, `instant` INTEGER NOT NULL, `mutedUntil` INTEGER NOT NULL, `upAppId` TEXT, `upConnectorToken` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "baseUrl",
|
||||||
|
"columnName": "baseUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "topic",
|
||||||
|
"columnName": "topic",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "instant",
|
||||||
|
"columnName": "instant",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mutedUntil",
|
||||||
|
"columnName": "mutedUntil",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "upAppId",
|
||||||
|
"columnName": "upAppId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "upConnectorToken",
|
||||||
|
"columnName": "upConnectorToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Subscription_baseUrl_topic",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"baseUrl",
|
||||||
|
"topic"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_Subscription_upConnectorToken",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"upConnectorToken"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_upConnectorToken` ON `${TABLE_NAME}` (`upConnectorToken`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Notification",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscriptionId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "message",
|
||||||
|
"columnName": "message",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationId",
|
||||||
|
"columnName": "notificationId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "priority",
|
||||||
|
"columnName": "priority",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "click",
|
||||||
|
"columnName": "click",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deleted",
|
||||||
|
"columnName": "deleted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachment.name",
|
||||||
|
"columnName": "attachment_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachment.type",
|
||||||
|
"columnName": "attachment_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachment.size",
|
||||||
|
"columnName": "attachment_size",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachment.expires",
|
||||||
|
"columnName": "attachment_expires",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachment.url",
|
||||||
|
"columnName": "attachment_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachment.contentUri",
|
||||||
|
"columnName": "attachment_contentUri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachment.progress",
|
||||||
|
"columnName": "attachment_progress",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"subscriptionId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Log",
|
||||||
|
"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, 'ecb1b85b2ae822dc62b2843620368477')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,10 @@ class Application : Application() {
|
||||||
}
|
}
|
||||||
val repository by lazy {
|
val repository by lazy {
|
||||||
val sharedPrefs = applicationContext.getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
val sharedPrefs = applicationContext.getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||||
Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao())
|
val repository = Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao())
|
||||||
|
if (repository.getRecordLogs()) {
|
||||||
|
Log.setRecord(true)
|
||||||
|
}
|
||||||
|
repository
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,8 +77,8 @@ const val PROGRESS_FAILED = -3
|
||||||
const val PROGRESS_DELETED = -4
|
const val PROGRESS_DELETED = -4
|
||||||
const val PROGRESS_DONE = 100
|
const val PROGRESS_DONE = 100
|
||||||
|
|
||||||
@Entity
|
@Entity(tableName = "Log")
|
||||||
data class Logs(
|
data class LogEntry(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Long, // Internal ID, only used in Repository and activities
|
@PrimaryKey(autoGenerate = true) val id: Long, // Internal ID, only used in Repository and activities
|
||||||
@ColumnInfo(name = "timestamp") val timestamp: Long,
|
@ColumnInfo(name = "timestamp") val timestamp: Long,
|
||||||
@ColumnInfo(name = "tag") val tag: String,
|
@ColumnInfo(name = "tag") val tag: String,
|
||||||
|
@ -90,11 +90,11 @@ data class Logs(
|
||||||
this(0, timestamp, tag, level, message, exception)
|
this(0, timestamp, tag, level, message, exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
@androidx.room.Database(entities = [Subscription::class, Notification::class, Logs::class], version = 6)
|
@androidx.room.Database(entities = [Subscription::class, Notification::class, LogEntry::class], version = 7)
|
||||||
abstract class Database : RoomDatabase() {
|
abstract class Database : RoomDatabase() {
|
||||||
abstract fun subscriptionDao(): SubscriptionDao
|
abstract fun subscriptionDao(): SubscriptionDao
|
||||||
abstract fun notificationDao(): NotificationDao
|
abstract fun notificationDao(): NotificationDao
|
||||||
abstract fun logsDao(): LogsDao
|
abstract fun logDao(): LogDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile
|
@Volatile
|
||||||
|
@ -109,6 +109,7 @@ abstract class Database : RoomDatabase() {
|
||||||
.addMigrations(MIGRATION_3_4)
|
.addMigrations(MIGRATION_3_4)
|
||||||
.addMigrations(MIGRATION_4_5)
|
.addMigrations(MIGRATION_4_5)
|
||||||
.addMigrations(MIGRATION_5_6)
|
.addMigrations(MIGRATION_5_6)
|
||||||
|
.addMigrations(MIGRATION_6_7)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
this.instance = instance
|
this.instance = instance
|
||||||
|
@ -166,6 +167,12 @@ abstract class Database : RoomDatabase() {
|
||||||
db.execSQL("ALTER TABLE Notification ADD COLUMN attachment_progress INT") // Room limitation: Has to be nullable for @Embedded
|
db.execSQL("ALTER TABLE Notification ADD COLUMN attachment_progress INT") // Room limitation: Has to be nullable for @Embedded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val MIGRATION_6_7 = object : Migration(6, 7) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL("CREATE TABLE Log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, timestamp INT NOT NULL, tag TEXT NOT NULL, level INT NOT NULL, message TEXT NOT NULL, exception TEXT)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,12 +283,17 @@ interface NotificationDao {
|
||||||
fun removeAll(subscriptionId: Long)
|
fun removeAll(subscriptionId: Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface LogsDao {
|
interface LogDao {
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insert(entry: Logs)
|
suspend fun insert(entry: LogEntry)
|
||||||
|
|
||||||
@Query("DELETE FROM logs WHERE id NOT IN (SELECT id FROM logs ORDER BY id DESC LIMIT :keepCount)")
|
@Query("DELETE FROM log WHERE id NOT IN (SELECT id FROM log ORDER BY id DESC LIMIT :keepCount)")
|
||||||
suspend fun prune(keepCount: Int)
|
suspend fun prune(keepCount: Int)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM log ORDER BY timestamp ASC, id ASC")
|
||||||
|
fun getAll(): List<LogEntry>
|
||||||
|
|
||||||
|
@Query("DELETE FROM log")
|
||||||
|
fun deleteAll()
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,6 +215,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRecordLogs(): Boolean {
|
||||||
|
return sharedPrefs.getBoolean(SHARED_PREFS_RECORD_LOGS_ENABLED, false) // Disabled by default
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRecordLogsEnabled(enabled: Boolean) {
|
||||||
|
sharedPrefs.edit()
|
||||||
|
.putBoolean(SHARED_PREFS_RECORD_LOGS_ENABLED, enabled)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
fun getUnifiedPushEnabled(): Boolean {
|
fun getUnifiedPushEnabled(): Boolean {
|
||||||
return sharedPrefs.getBoolean(SHARED_PREFS_UNIFIED_PUSH_ENABLED, true) // Enabled by default
|
return sharedPrefs.getBoolean(SHARED_PREFS_UNIFIED_PUSH_ENABLED, true) // Enabled by default
|
||||||
}
|
}
|
||||||
|
@ -339,6 +349,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
|
||||||
const val SHARED_PREFS_WAKELOCK_ENABLED = "WakelockEnabled"
|
const val SHARED_PREFS_WAKELOCK_ENABLED = "WakelockEnabled"
|
||||||
const val SHARED_PREFS_CONNECTION_PROTOCOL = "ConnectionProtocol"
|
const val SHARED_PREFS_CONNECTION_PROTOCOL = "ConnectionProtocol"
|
||||||
const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
|
const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
|
||||||
|
const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
|
||||||
const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled"
|
const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled"
|
||||||
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL"
|
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL"
|
||||||
|
|
||||||
|
|
|
@ -2,22 +2,25 @@ package io.heckel.ntfy.log
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import io.heckel.ntfy.data.Database
|
import io.heckel.ntfy.data.Database
|
||||||
import io.heckel.ntfy.data.Logs
|
import io.heckel.ntfy.data.LogDao
|
||||||
import io.heckel.ntfy.data.LogsDao
|
import io.heckel.ntfy.data.LogEntry
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class Log(private val logsDao: LogsDao) {
|
class Log(private val logsDao: LogDao) {
|
||||||
private var record: AtomicBoolean = AtomicBoolean(false)
|
private val record: AtomicBoolean = AtomicBoolean(false)
|
||||||
private var count: AtomicInteger = AtomicInteger(0)
|
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?) {
|
private fun log(level: Int, tag: String, message: String, exception: Throwable?) {
|
||||||
if (!record.get()) return
|
if (!record.get()) return
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) { // FIXME This does not guarantee the log order
|
||||||
logsDao.insert(Logs(System.currentTimeMillis(), tag, level, message, exception?.stackTraceToString()))
|
logsDao.insert(LogEntry(System.currentTimeMillis(), tag, level, message, exception?.stackTraceToString()))
|
||||||
val current = count.incrementAndGet()
|
val current = count.incrementAndGet()
|
||||||
if (current >= PRUNE_EVERY) {
|
if (current >= PRUNE_EVERY) {
|
||||||
logsDao.prune(ENTRIES_MAX)
|
logsDao.prune(ENTRIES_MAX)
|
||||||
|
@ -26,7 +29,55 @@ class Log(private val logsDao: LogsDao) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAll(): Collection<LogEntry> {
|
||||||
|
return logsDao
|
||||||
|
.getAll()
|
||||||
|
.map { e ->
|
||||||
|
e.copy(
|
||||||
|
message = scrub(e.message)!!,
|
||||||
|
exception = scrub(e.exception)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteAll() {
|
||||||
|
return logsDao.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
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) ?: "scrubbed${replaceTermIndex}"
|
||||||
|
scrubTerms[term] = when (type) {
|
||||||
|
TermType.Domain -> "$replaceTerm.example.com"
|
||||||
|
else -> replaceTerm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrub(line: String?): String? {
|
||||||
|
var newLine = line ?: return null
|
||||||
|
scrubTerms.forEach { (scrubTerm, replaceTerm) ->
|
||||||
|
newLine = newLine.replace(scrubTerm, replaceTerm)
|
||||||
|
}
|
||||||
|
return newLine
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TermType {
|
||||||
|
Domain, Term
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
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(
|
||||||
|
"potato", "banana", "coconut", "kiwi", "avocado", "orange", "apple", "lemon", "olive", "peach"
|
||||||
|
)
|
||||||
|
private var instance: Log? = null
|
||||||
|
|
||||||
fun d(tag: String, message: String, exception: Throwable? = 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)
|
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)
|
getInstance()?.log(android.util.Log.DEBUG, tag, message, exception)
|
||||||
|
@ -57,20 +108,27 @@ class Log(private val logsDao: LogsDao) {
|
||||||
return getInstance()?.record?.get() ?: false
|
return getInstance()?.record?.get() ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAll(): Collection<LogEntry> {
|
||||||
|
return getInstance()?.getAll().orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAll() {
|
||||||
|
getInstance()?.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addScrubTerm(term: String, type: TermType = TermType.Term) {
|
||||||
|
getInstance()?.addScrubTerm(term, type)
|
||||||
|
}
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
return synchronized(Log::class) {
|
return synchronized(Log::class) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
val database = Database.getInstance(context.applicationContext)
|
val database = Database.getInstance(context.applicationContext)
|
||||||
instance = Log(database.logsDao())
|
instance = Log(database.logDao())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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? {
|
private fun getInstance(): Log? {
|
||||||
return synchronized(Log::class) {
|
return synchronized(Log::class) {
|
||||||
instance
|
instance
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.os.Environment
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
@ -18,6 +17,7 @@ import io.heckel.ntfy.BuildConfig
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.data.*
|
import io.heckel.ntfy.data.*
|
||||||
|
import io.heckel.ntfy.log.Log
|
||||||
import io.heckel.ntfy.util.queryFilename
|
import io.heckel.ntfy.util.queryFilename
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package io.heckel.ntfy.msg
|
package io.heckel.ntfy.msg
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import io.heckel.ntfy.data.Notification
|
import io.heckel.ntfy.data.Notification
|
||||||
import io.heckel.ntfy.data.Repository
|
import io.heckel.ntfy.data.Repository
|
||||||
import io.heckel.ntfy.data.Subscription
|
import io.heckel.ntfy.data.Subscription
|
||||||
|
import io.heckel.ntfy.log.Log
|
||||||
import io.heckel.ntfy.up.Distributor
|
import io.heckel.ntfy.up.Distributor
|
||||||
import io.heckel.ntfy.util.safeLet
|
import io.heckel.ntfy.util.safeLet
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package io.heckel.ntfy.service
|
package io.heckel.ntfy.service
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import io.heckel.ntfy.data.ConnectionState
|
import io.heckel.ntfy.data.ConnectionState
|
||||||
import io.heckel.ntfy.data.Notification
|
import io.heckel.ntfy.data.Notification
|
||||||
import io.heckel.ntfy.data.Repository
|
import io.heckel.ntfy.data.Repository
|
||||||
import io.heckel.ntfy.data.Subscription
|
import io.heckel.ntfy.data.Subscription
|
||||||
|
import io.heckel.ntfy.log.Log
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.util.topicUrl
|
import io.heckel.ntfy.util.topicUrl
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
@ -47,7 +47,7 @@ class JsonConnection(
|
||||||
notificationListener(subscription, notificationWithSubscriptionId)
|
notificationListener(subscription, notificationWithSubscriptionId)
|
||||||
}
|
}
|
||||||
val failed = AtomicBoolean(false)
|
val failed = AtomicBoolean(false)
|
||||||
val fail = { e: Exception ->
|
val fail = { _: Exception ->
|
||||||
failed.set(true)
|
failed.set(true)
|
||||||
if (isActive && serviceActive()) { // Avoid UI update races if we're restarting a connection
|
if (isActive && serviceActive()) { // Avoid UI update races if we're restarting a connection
|
||||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
||||||
|
|
|
@ -82,6 +82,8 @@ class SubscriberService : Service() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
Log.init(this) // Init logs in all entry points
|
||||||
Log.d(TAG, "Subscriber service has been created")
|
Log.d(TAG, "Subscriber service has been created")
|
||||||
|
|
||||||
val title = getString(R.string.channel_subscriber_notification_title)
|
val title = getString(R.string.channel_subscriber_notification_title)
|
||||||
|
|
|
@ -29,6 +29,7 @@ import io.heckel.ntfy.service.SubscriberService
|
||||||
import io.heckel.ntfy.service.SubscriberServiceManager
|
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||||
import io.heckel.ntfy.util.fadeStatusBarColor
|
import io.heckel.ntfy.util.fadeStatusBarColor
|
||||||
import io.heckel.ntfy.util.formatDateShort
|
import io.heckel.ntfy.util.formatDateShort
|
||||||
|
import io.heckel.ntfy.util.shortUrl
|
||||||
import io.heckel.ntfy.util.topicShortUrl
|
import io.heckel.ntfy.util.topicShortUrl
|
||||||
import io.heckel.ntfy.work.PollWorker
|
import io.heckel.ntfy.work.PollWorker
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -63,9 +64,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
Log.init(this)
|
Log.init(this) // Init logs in all entry points
|
||||||
Log.setRecord(true)
|
|
||||||
|
|
||||||
Log.d(TAG, "Create $this")
|
Log.d(TAG, "Create $this")
|
||||||
|
|
||||||
// Dependencies that depend on Context
|
// Dependencies that depend on Context
|
||||||
|
@ -98,6 +97,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
|
|
||||||
viewModel.list().observe(this) {
|
viewModel.list().observe(this) {
|
||||||
it?.let { subscriptions ->
|
it?.let { subscriptions ->
|
||||||
|
// Update main list
|
||||||
adapter.submitList(subscriptions as MutableList<Subscription>)
|
adapter.submitList(subscriptions as MutableList<Subscription>)
|
||||||
if (it.isEmpty()) {
|
if (it.isEmpty()) {
|
||||||
mainListContainer.visibility = View.GONE
|
mainListContainer.visibility = View.GONE
|
||||||
|
@ -106,6 +106,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
mainListContainer.visibility = View.VISIBLE
|
mainListContainer.visibility = View.VISIBLE
|
||||||
noEntries.visibility = View.GONE
|
noEntries.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add scrub terms to log (in case it gets exported)
|
||||||
|
subscriptions.forEach { s ->
|
||||||
|
Log.addScrubTerm(shortUrl(s.baseUrl), Log.TermType.Domain)
|
||||||
|
Log.addScrubTerm(s.topic)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package io.heckel.ntfy.ui
|
package io.heckel.ntfy.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -14,6 +15,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.*
|
import androidx.preference.*
|
||||||
import androidx.preference.Preference.OnPreferenceClickListener
|
import androidx.preference.Preference.OnPreferenceClickListener
|
||||||
import io.heckel.ntfy.BuildConfig
|
import io.heckel.ntfy.BuildConfig
|
||||||
|
@ -24,6 +26,10 @@ import io.heckel.ntfy.service.SubscriberService
|
||||||
import io.heckel.ntfy.util.formatBytes
|
import io.heckel.ntfy.util.formatBytes
|
||||||
import io.heckel.ntfy.util.formatDateShort
|
import io.heckel.ntfy.util.formatDateShort
|
||||||
import io.heckel.ntfy.util.toPriorityString
|
import io.heckel.ntfy.util.toPriorityString
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
private lateinit var fragment: SettingsFragment
|
private lateinit var fragment: SettingsFragment
|
||||||
|
@ -154,68 +160,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection protocol
|
|
||||||
val connectionProtocolPrefId = context?.getString(R.string.settings_advanced_connection_protocol_key) ?: return
|
|
||||||
val connectionProtocol: ListPreference? = findPreference(connectionProtocolPrefId)
|
|
||||||
connectionProtocol?.value = repository.getConnectionProtocol()
|
|
||||||
connectionProtocol?.preferenceDataStore = object : PreferenceDataStore() {
|
|
||||||
override fun putString(key: String?, value: String?) {
|
|
||||||
val proto = value ?: repository.getConnectionProtocol()
|
|
||||||
repository.setConnectionProtocol(proto)
|
|
||||||
restartService()
|
|
||||||
}
|
|
||||||
override fun getString(key: String?, defValue: String?): String {
|
|
||||||
return repository.getConnectionProtocol()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
connectionProtocol?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
|
|
||||||
when (pref.value) {
|
|
||||||
Repository.CONNECTION_PROTOCOL_WS -> getString(R.string.settings_advanced_connection_protocol_summary_ws)
|
|
||||||
else -> getString(R.string.settings_advanced_connection_protocol_summary_jsonhttp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permanent wakelock enabled
|
|
||||||
val wakelockEnabledPrefId = context?.getString(R.string.settings_advanced_wakelock_key) ?: return
|
|
||||||
val wakelockEnabled: SwitchPreference? = findPreference(wakelockEnabledPrefId)
|
|
||||||
wakelockEnabled?.isChecked = repository.getWakelockEnabled()
|
|
||||||
wakelockEnabled?.preferenceDataStore = object : PreferenceDataStore() {
|
|
||||||
override fun putBoolean(key: String?, value: Boolean) {
|
|
||||||
repository.setWakelockEnabled(value)
|
|
||||||
restartService()
|
|
||||||
}
|
|
||||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
|
||||||
return repository.getWakelockEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wakelockEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
|
|
||||||
if (pref.isChecked) {
|
|
||||||
getString(R.string.settings_advanced_wakelock_summary_enabled)
|
|
||||||
} else {
|
|
||||||
getString(R.string.settings_advanced_wakelock_summary_disabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast enabled
|
|
||||||
val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return
|
|
||||||
val broadcastEnabled: SwitchPreference? = findPreference(broadcastEnabledPrefId)
|
|
||||||
broadcastEnabled?.isChecked = repository.getBroadcastEnabled()
|
|
||||||
broadcastEnabled?.preferenceDataStore = object : PreferenceDataStore() {
|
|
||||||
override fun putBoolean(key: String?, value: Boolean) {
|
|
||||||
repository.setBroadcastEnabled(value)
|
|
||||||
}
|
|
||||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
|
||||||
return repository.getBroadcastEnabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
broadcastEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
|
|
||||||
if (pref.isChecked) {
|
|
||||||
getString(R.string.settings_advanced_broadcast_summary_enabled)
|
|
||||||
} else {
|
|
||||||
getString(R.string.settings_advanced_broadcast_summary_disabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnifiedPush enabled
|
// UnifiedPush enabled
|
||||||
val upEnabledPrefId = context?.getString(R.string.settings_unified_push_enabled_key) ?: return
|
val upEnabledPrefId = context?.getString(R.string.settings_unified_push_enabled_key) ?: return
|
||||||
val upEnabled: SwitchPreference? = findPreference(upEnabledPrefId)
|
val upEnabled: SwitchPreference? = findPreference(upEnabledPrefId)
|
||||||
|
@ -258,6 +202,123 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broadcast enabled
|
||||||
|
val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return
|
||||||
|
val broadcastEnabled: SwitchPreference? = findPreference(broadcastEnabledPrefId)
|
||||||
|
broadcastEnabled?.isChecked = repository.getBroadcastEnabled()
|
||||||
|
broadcastEnabled?.preferenceDataStore = object : PreferenceDataStore() {
|
||||||
|
override fun putBoolean(key: String?, value: Boolean) {
|
||||||
|
repository.setBroadcastEnabled(value)
|
||||||
|
}
|
||||||
|
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||||
|
return repository.getBroadcastEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
broadcastEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
|
||||||
|
if (pref.isChecked) {
|
||||||
|
getString(R.string.settings_advanced_broadcast_summary_enabled)
|
||||||
|
} else {
|
||||||
|
getString(R.string.settings_advanced_broadcast_summary_disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy logs
|
||||||
|
val copyLogsPrefId = context?.getString(R.string.settings_advanced_copy_logs_key) ?: return
|
||||||
|
val copyLogs: Preference? = findPreference(copyLogsPrefId)
|
||||||
|
copyLogs?.isVisible = Log.getRecord()
|
||||||
|
copyLogs?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
|
||||||
|
copyLogs?.onPreferenceClickListener = OnPreferenceClickListener {
|
||||||
|
copyLogsToClipboard()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record logs
|
||||||
|
val recordLogsPrefId = context?.getString(R.string.settings_advanced_record_logs_key) ?: return
|
||||||
|
val recordLogsEnabled: SwitchPreference? = findPreference(recordLogsPrefId)
|
||||||
|
recordLogsEnabled?.isChecked = Log.getRecord()
|
||||||
|
recordLogsEnabled?.preferenceDataStore = object : PreferenceDataStore() {
|
||||||
|
override fun putBoolean(key: String?, value: Boolean) {
|
||||||
|
repository.setRecordLogsEnabled(value)
|
||||||
|
Log.setRecord(value)
|
||||||
|
copyLogs?.isVisible = value
|
||||||
|
}
|
||||||
|
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||||
|
return Log.getRecord()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recordLogsEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
|
||||||
|
if (pref.isChecked) {
|
||||||
|
getString(R.string.settings_advanced_record_logs_summary_enabled)
|
||||||
|
} else {
|
||||||
|
getString(R.string.settings_advanced_record_logs_summary_disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recordLogsEnabled?.setOnPreferenceChangeListener { _, v ->
|
||||||
|
val newValue = v as Boolean
|
||||||
|
if (!newValue) {
|
||||||
|
val dialog = AlertDialog.Builder(activity)
|
||||||
|
.setMessage(R.string.settings_advanced_record_logs_delete_dialog_message)
|
||||||
|
.setPositiveButton(R.string.settings_advanced_record_logs_delete_dialog_button_delete) { _, _ ->
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
Log.deleteAll()
|
||||||
|
} }
|
||||||
|
.setNegativeButton(R.string.settings_advanced_record_logs_delete_dialog_button_keep) { _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
dialog
|
||||||
|
.setOnShowListener {
|
||||||
|
dialog
|
||||||
|
.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
|
.setTextColor(ContextCompat.getColor(requireContext(), R.color.primaryDangerButtonColor))
|
||||||
|
}
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection protocol
|
||||||
|
val connectionProtocolPrefId = context?.getString(R.string.settings_advanced_connection_protocol_key) ?: return
|
||||||
|
val connectionProtocol: ListPreference? = findPreference(connectionProtocolPrefId)
|
||||||
|
connectionProtocol?.value = repository.getConnectionProtocol()
|
||||||
|
connectionProtocol?.preferenceDataStore = object : PreferenceDataStore() {
|
||||||
|
override fun putString(key: String?, value: String?) {
|
||||||
|
val proto = value ?: repository.getConnectionProtocol()
|
||||||
|
repository.setConnectionProtocol(proto)
|
||||||
|
restartService()
|
||||||
|
}
|
||||||
|
override fun getString(key: String?, defValue: String?): String {
|
||||||
|
return repository.getConnectionProtocol()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectionProtocol?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
|
||||||
|
when (pref.value) {
|
||||||
|
Repository.CONNECTION_PROTOCOL_WS -> getString(R.string.settings_advanced_connection_protocol_summary_ws)
|
||||||
|
else -> getString(R.string.settings_advanced_connection_protocol_summary_jsonhttp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permanent wakelock enabled
|
||||||
|
val wakelockEnabledPrefId = context?.getString(R.string.settings_advanced_wakelock_key) ?: return
|
||||||
|
val wakelockEnabled: SwitchPreference? = findPreference(wakelockEnabledPrefId)
|
||||||
|
wakelockEnabled?.isChecked = repository.getWakelockEnabled()
|
||||||
|
wakelockEnabled?.preferenceDataStore = object : PreferenceDataStore() {
|
||||||
|
override fun putBoolean(key: String?, value: Boolean) {
|
||||||
|
repository.setWakelockEnabled(value)
|
||||||
|
restartService()
|
||||||
|
}
|
||||||
|
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||||
|
return repository.getWakelockEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wakelockEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
|
||||||
|
if (pref.isChecked) {
|
||||||
|
getString(R.string.settings_advanced_wakelock_summary_enabled)
|
||||||
|
} else {
|
||||||
|
getString(R.string.settings_advanced_wakelock_summary_disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
val versionPrefId = context?.getString(R.string.settings_about_version_key) ?: return
|
val versionPrefId = context?.getString(R.string.settings_about_version_key) ?: return
|
||||||
val versionPref: Preference? = findPreference(versionPrefId)
|
val versionPref: Preference? = findPreference(versionPrefId)
|
||||||
|
@ -266,7 +327,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
versionPref?.onPreferenceClickListener = OnPreferenceClickListener {
|
versionPref?.onPreferenceClickListener = OnPreferenceClickListener {
|
||||||
val context = context ?: return@OnPreferenceClickListener false
|
val context = context ?: return@OnPreferenceClickListener false
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("app version", version)
|
val clip = ClipData.newPlainText("ntfy version", version)
|
||||||
clipboard.setPrimaryClip(clip)
|
clipboard.setPrimaryClip(clip)
|
||||||
Toast
|
Toast
|
||||||
.makeText(context, getString(R.string.settings_about_version_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
.makeText(context, getString(R.string.settings_about_version_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
||||||
|
@ -290,6 +351,38 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
context?.stopService(intent) // Service will auto-restart
|
context?.stopService(intent) // Service will auto-restart
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun copyLogsToClipboard() {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val log = Log.getAll().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"
|
||||||
|
}
|
||||||
|
val context = context ?: return@launch
|
||||||
|
requireActivity().runOnUiThread {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("ntfy logs", log)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
Toast
|
||||||
|
.makeText(context, getString(R.string.settings_advanced_copy_logs_copied), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
|
|
@ -19,10 +19,11 @@ fun topicUrlUp(baseUrl: String, topic: String) = "${baseUrl}/${topic}?up=1" // U
|
||||||
fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since"
|
fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since"
|
||||||
fun topicUrlWs(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/ws?since=$since"
|
fun topicUrlWs(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/ws?since=$since"
|
||||||
fun topicUrlJsonPoll(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?poll=1&since=$since"
|
fun topicUrlJsonPoll(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?poll=1&since=$since"
|
||||||
fun topicShortUrl(baseUrl: String, topic: String) =
|
fun topicShortUrl(baseUrl: String, topic: String) = shortUrl(topicUrl(baseUrl, topic))
|
||||||
topicUrl(baseUrl, topic)
|
|
||||||
.replace("http://", "")
|
fun shortUrl(url: String) = url
|
||||||
.replace("https://", "")
|
.replace("http://", "")
|
||||||
|
.replace("https://", "")
|
||||||
|
|
||||||
fun formatDateShort(timestampSecs: Long): String {
|
fun formatDateShort(timestampSecs: Long): String {
|
||||||
val date = Date(timestampSecs*1000)
|
val date = Date(timestampSecs*1000)
|
||||||
|
|
|
@ -230,6 +230,17 @@
|
||||||
<string name="settings_advanced_broadcast_title">Broadcast messages</string>
|
<string name="settings_advanced_broadcast_title">Broadcast messages</string>
|
||||||
<string name="settings_advanced_broadcast_summary_enabled">Apps can receive incoming notifications as broadcasts</string>
|
<string name="settings_advanced_broadcast_summary_enabled">Apps can receive incoming notifications as broadcasts</string>
|
||||||
<string name="settings_advanced_broadcast_summary_disabled">Apps cannot receive notifications as broadcasts</string>
|
<string name="settings_advanced_broadcast_summary_disabled">Apps cannot receive notifications as broadcasts</string>
|
||||||
|
<string name="settings_advanced_record_logs_key">RecordLogs</string>
|
||||||
|
<string name="settings_advanced_record_logs_title">Record logs</string>
|
||||||
|
<string name="settings_advanced_record_logs_summary_enabled">Logs are currently being recorded to your device. Up to 5,000 log lines are stored.</string>
|
||||||
|
<string name="settings_advanced_record_logs_summary_disabled">Enable log recording, so you can share the logs later. This is useful for diagnosing issues.</string>
|
||||||
|
<string name="settings_advanced_record_logs_delete_dialog_message">Would you like to delete the existing logs?</string>
|
||||||
|
<string name="settings_advanced_record_logs_delete_dialog_button_keep">Keep logs</string>
|
||||||
|
<string name="settings_advanced_record_logs_delete_dialog_button_delete">Delete logs</string>
|
||||||
|
<string name="settings_advanced_copy_logs_key">CopyLogs</string>
|
||||||
|
<string name="settings_advanced_copy_logs_title">Copy logs</string>
|
||||||
|
<string name="settings_advanced_copy_logs_summary">Copy logs to the clipboard. Hostnames and topics are scrubbed, notifications are not.</string>
|
||||||
|
<string name="settings_advanced_copy_logs_copied">Copied to clipboard</string>
|
||||||
<string name="settings_experimental_header">Experimental</string>
|
<string name="settings_experimental_header">Experimental</string>
|
||||||
<string name="settings_advanced_connection_protocol_key">ConnectionProtocol</string>
|
<string name="settings_advanced_connection_protocol_key">ConnectionProtocol</string>
|
||||||
<string name="settings_advanced_connection_protocol_title">Connection protocol</string>
|
<string name="settings_advanced_connection_protocol_title">Connection protocol</string>
|
||||||
|
|
|
@ -38,6 +38,14 @@
|
||||||
app:key="@string/settings_advanced_broadcast_key"
|
app:key="@string/settings_advanced_broadcast_key"
|
||||||
app:title="@string/settings_advanced_broadcast_title"
|
app:title="@string/settings_advanced_broadcast_title"
|
||||||
app:enabled="true"/>
|
app:enabled="true"/>
|
||||||
|
<SwitchPreference
|
||||||
|
app:key="@string/settings_advanced_record_logs_key"
|
||||||
|
app:title="@string/settings_advanced_record_logs_title"
|
||||||
|
app:enabled="true"/>
|
||||||
|
<Preference
|
||||||
|
app:key="@string/settings_advanced_copy_logs_key"
|
||||||
|
app:title="@string/settings_advanced_copy_logs_title"
|
||||||
|
android:summary="@string/settings_advanced_copy_logs_summary"/>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory app:title="@string/settings_experimental_header">
|
<PreferenceCategory app:title="@string/settings_experimental_header">
|
||||||
<ListPreference
|
<ListPreference
|
||||||
|
|
Loading…
Reference in a new issue