All the things; this should be it
This commit is contained in:
parent
c772d15043
commit
3abd0b3da9
23 changed files with 677 additions and 335 deletions
|
@ -2,7 +2,7 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"identityHash": "eda2cb9740c4542f24462779eb6ff81d",
|
"identityHash": "ecb1b85b2ae822dc62b2843620368477",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "Subscription",
|
"tableName": "Subscription",
|
||||||
|
@ -65,7 +65,6 @@
|
||||||
"baseUrl",
|
"baseUrl",
|
||||||
"topic"
|
"topic"
|
||||||
],
|
],
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -74,7 +73,6 @@
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"upConnectorToken"
|
"upConnectorToken"
|
||||||
],
|
],
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_upConnectorToken` ON `${TABLE_NAME}` (`upConnectorToken`)"
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_upConnectorToken` ON `${TABLE_NAME}` (`upConnectorToken`)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -198,38 +196,6 @@
|
||||||
"indices": [],
|
"indices": [],
|
||||||
"foreignKeys": []
|
"foreignKeys": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"tableName": "User",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "baseUrl",
|
|
||||||
"columnName": "baseUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "username",
|
|
||||||
"columnName": "username",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "password",
|
|
||||||
"columnName": "password",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"columnNames": [
|
|
||||||
"baseUrl"
|
|
||||||
],
|
|
||||||
"autoGenerate": false
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"tableName": "Log",
|
"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)",
|
"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)",
|
||||||
|
@ -284,7 +250,7 @@
|
||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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, 'eda2cb9740c4542f24462779eb6ff81d')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ecb1b85b2ae822dc62b2843620368477')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
290
app/schemas/io.heckel.ntfy.db.Database/8.json
Normal file
290
app/schemas/io.heckel.ntfy.db.Database/8.json
Normal file
|
@ -0,0 +1,290 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 8,
|
||||||
|
"identityHash": "eda2cb9740c4542f24462779eb6ff81d",
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_Subscription_upConnectorToken",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"upConnectorToken"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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": "User",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "baseUrl",
|
||||||
|
"columnName": "baseUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "password",
|
||||||
|
"columnName": "password",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"baseUrl"
|
||||||
|
],
|
||||||
|
"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, 'eda2cb9740c4542f24462779eb6ff81d')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -100,7 +100,7 @@ data class LogEntry(
|
||||||
this(0, timestamp, tag, level, message, exception)
|
this(0, timestamp, tag, level, message, exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 7)
|
@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 8)
|
||||||
abstract class Database : RoomDatabase() {
|
abstract class Database : RoomDatabase() {
|
||||||
abstract fun subscriptionDao(): SubscriptionDao
|
abstract fun subscriptionDao(): SubscriptionDao
|
||||||
abstract fun notificationDao(): NotificationDao
|
abstract fun notificationDao(): NotificationDao
|
||||||
|
@ -121,6 +121,7 @@ abstract class Database : RoomDatabase() {
|
||||||
.addMigrations(MIGRATION_4_5)
|
.addMigrations(MIGRATION_4_5)
|
||||||
.addMigrations(MIGRATION_5_6)
|
.addMigrations(MIGRATION_5_6)
|
||||||
.addMigrations(MIGRATION_6_7)
|
.addMigrations(MIGRATION_6_7)
|
||||||
|
.addMigrations(MIGRATION_7_8)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
this.instance = instance
|
this.instance = instance
|
||||||
|
@ -184,6 +185,12 @@ abstract class Database : RoomDatabase() {
|
||||||
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)")
|
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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val MIGRATION_7_8 = object : Migration(7, 8) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL("CREATE TABLE User (baseUrl TEXT NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, PRIMARY KEY(baseUrl))")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +250,7 @@ interface SubscriptionDao {
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT
|
SELECT
|
||||||
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil,s.upAppId, s.upConnectorToken,
|
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.upAppId, s.upConnectorToken,
|
||||||
COUNT(n.id) totalCount,
|
COUNT(n.id) totalCount,
|
||||||
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
|
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
|
||||||
IFNULL(MAX(n.timestamp),0) AS lastActive
|
IFNULL(MAX(n.timestamp),0) AS lastActive
|
||||||
|
|
|
@ -219,16 +219,6 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
||||||
return sharedPrefs.getInt(SHARED_PREFS_DARK_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
return sharedPrefs.getInt(SHARED_PREFS_DARK_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getWakelockEnabled(): Boolean {
|
|
||||||
return sharedPrefs.getBoolean(SHARED_PREFS_WAKELOCK_ENABLED, false) // Disabled by default!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setWakelockEnabled(enabled: Boolean) {
|
|
||||||
sharedPrefs.edit()
|
|
||||||
.putBoolean(SHARED_PREFS_WAKELOCK_ENABLED, enabled)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setConnectionProtocol(connectionProtocol: String) {
|
fun setConnectionProtocol(connectionProtocol: String) {
|
||||||
if (connectionProtocol == CONNECTION_PROTOCOL_JSONHTTP) {
|
if (connectionProtocol == CONNECTION_PROTOCOL_JSONHTTP) {
|
||||||
sharedPrefs.edit()
|
sharedPrefs.edit()
|
||||||
|
|
|
@ -6,8 +6,6 @@ import io.heckel.ntfy.BuildConfig
|
||||||
import io.heckel.ntfy.db.Database
|
import io.heckel.ntfy.db.Database
|
||||||
import io.heckel.ntfy.db.LogDao
|
import io.heckel.ntfy.db.LogDao
|
||||||
import io.heckel.ntfy.db.LogEntry
|
import io.heckel.ntfy.db.LogEntry
|
||||||
import io.heckel.ntfy.db.Repository
|
|
||||||
import io.heckel.ntfy.util.isIgnoringBatteryOptimizations
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -34,15 +32,19 @@ class Log(private val logsDao: LogDao) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFormatted(): String {
|
fun getFormatted(scrub: Boolean): String {
|
||||||
return prependDeviceInfo(formatEntries(scrubEntries(logsDao.getAll())))
|
return if (scrub) {
|
||||||
|
prependDeviceInfo(formatEntries(scrubEntries(logsDao.getAll())), scrubLine = true)
|
||||||
|
} else {
|
||||||
|
prependDeviceInfo(formatEntries(logsDao.getAll()), scrubLine = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun prependDeviceInfo(s: String): String {
|
private fun prependDeviceInfo(s: String, scrubLine: Boolean): String {
|
||||||
|
val maybeScrubLine = if (scrubLine) "Server URLs (aside from ntfy.sh) and topics have been replaced with fruits 🍌🥝🍋🥥🥑🍊🍎🍑.\n" else ""
|
||||||
return """
|
return """
|
||||||
This is a log of the ntfy Android app. The log shows up to 2,000 lines.
|
This is a log of the ntfy Android app. The log shows up to 1,000 entries.
|
||||||
Server URLs (aside from ntfy.sh) and topics have been replaced with fruits 🍌🥝🍋🥥🥑🍊🍎🍑.
|
$maybeScrubLine
|
||||||
|
|
||||||
Device info:
|
Device info:
|
||||||
--
|
--
|
||||||
ntfy: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR})
|
ntfy: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR})
|
||||||
|
@ -116,7 +118,7 @@ class Log(private val logsDao: LogDao) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "NtfyLog"
|
private const val TAG = "NtfyLog"
|
||||||
private const val PRUNE_EVERY = 100
|
private const val PRUNE_EVERY = 100
|
||||||
private const val ENTRIES_MAX = 2000
|
private const val ENTRIES_MAX = 1000
|
||||||
private val IGNORE_TERMS = listOf("ntfy.sh")
|
private val IGNORE_TERMS = listOf("ntfy.sh")
|
||||||
private val REPLACE_TERMS = listOf(
|
private val REPLACE_TERMS = listOf(
|
||||||
"banana", "kiwi", "lemon", "coconut", "avocado", "orange", "apple", "peach"
|
"banana", "kiwi", "lemon", "coconut", "avocado", "orange", "apple", "peach"
|
||||||
|
@ -153,8 +155,12 @@ class Log(private val logsDao: LogDao) {
|
||||||
return getInstance()?.record?.get() ?: false
|
return getInstance()?.record?.get() ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFormatted(): String {
|
fun getFormatted(scrub: Boolean): String {
|
||||||
return getInstance()?.getFormatted() ?: "(no logs)"
|
return getInstance()?.getFormatted(scrub) ?: "(no logs)"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getScrubTerms(): Map<String, String> {
|
||||||
|
return getInstance()?.scrubTerms!!.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
|
|
|
@ -47,6 +47,9 @@ class ApiService {
|
||||||
builder.addHeader("X-Delay", delay)
|
builder.addHeader("X-Delay", delay)
|
||||||
}
|
}
|
||||||
client.newCall(builder.build()).execute().use { response ->
|
client.newCall(builder.build()).execute().use { response ->
|
||||||
|
if (response.code == 401 || response.code == 403) {
|
||||||
|
throw UnauthorizedException(user)
|
||||||
|
}
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
throw Exception("Unexpected response ${response.code} when publishing to $url")
|
throw Exception("Unexpected response ${response.code} when publishing to $url")
|
||||||
}
|
}
|
||||||
|
@ -132,6 +135,8 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UnauthorizedException(val user: User?) : Exception()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})"
|
val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})"
|
||||||
private const val TAG = "NtfyApiService"
|
private const val TAG = "NtfyApiService"
|
||||||
|
|
|
@ -68,16 +68,21 @@ class BroadcastService(private val ctx: Context) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
val repository = Repository.getInstance(ctx)
|
val repository = Repository.getInstance(ctx)
|
||||||
val user = repository.getUser(baseUrl) // May be null
|
val user = repository.getUser(baseUrl) // May be null
|
||||||
api.publish(
|
try {
|
||||||
baseUrl = baseUrl,
|
Log.d(TAG, "Publishing message $intent")
|
||||||
topic = topic,
|
api.publish(
|
||||||
user = user,
|
baseUrl = baseUrl,
|
||||||
message = message,
|
topic = topic,
|
||||||
title = title,
|
user = user,
|
||||||
priority = priority,
|
message = message,
|
||||||
tags = splitTags(tags),
|
title = title,
|
||||||
delay = delay
|
priority = priority,
|
||||||
)
|
tags = splitTags(tags),
|
||||||
|
delay = delay
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Unable to publish message: ${e.message}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,11 @@ import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||||
import io.heckel.ntfy.ui.MainActivity
|
import io.heckel.ntfy.ui.MainActivity
|
||||||
import io.heckel.ntfy.util.topicUrl
|
import io.heckel.ntfy.util.topicUrl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
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 kotlinx.coroutines.sync.Mutex
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,16 +65,16 @@ class SubscriberService : Service() {
|
||||||
private val api = ApiService()
|
private val api = ApiService()
|
||||||
private var notificationManager: NotificationManager? = null
|
private var notificationManager: NotificationManager? = null
|
||||||
private var serviceNotification: Notification? = null
|
private var serviceNotification: Notification? = null
|
||||||
|
private val refreshMutex = Mutex() // Ensure refreshConnections() is only run one at a time
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.d(TAG, "onStartCommand executed with startId: $startId")
|
Log.d(TAG, "onStartCommand executed with startId: $startId")
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
val action = intent.action
|
Log.d(TAG, "using an intent with action ${intent.action}")
|
||||||
Log.d(TAG, "using an intent with action $action")
|
when (intent.action) {
|
||||||
when (action) {
|
|
||||||
Action.START.name -> startService()
|
Action.START.name -> startService()
|
||||||
Action.STOP.name -> stopService()
|
Action.STOP.name -> stopService()
|
||||||
else -> Log.e(TAG, "This should never happen. No action in the received intent")
|
else -> Log.w(TAG, "This should never happen. No action in the received intent")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "with a null intent. It has been probably restarted by the system.")
|
Log.d(TAG, "with a null intent. It has been probably restarted by the system.")
|
||||||
|
@ -116,9 +118,6 @@ class SubscriberService : Service() {
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG)
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG)
|
||||||
}
|
}
|
||||||
if (repository.getWakelockEnabled()) {
|
|
||||||
wakeLock?.acquire()
|
|
||||||
}
|
|
||||||
refreshConnections()
|
refreshConnections()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,95 +147,115 @@ class SubscriberService : Service() {
|
||||||
saveServiceState(this, ServiceState.STOPPED)
|
saveServiceState(this, ServiceState.STOPPED)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshConnections() =
|
private fun refreshConnections() {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
// Group INSTANT subscriptions by base URL, there is only one connection per base URL
|
if (!refreshMutex.tryLock()) {
|
||||||
val instantSubscriptions = repository.getSubscriptions()
|
Log.d(TAG, "Refreshing subscriptions already in progress. Skipping.")
|
||||||
.filter { s -> s.instant }
|
|
||||||
val activeConnectionIds = connections.keys().toList().toSet()
|
|
||||||
val desiredConnectionIds = instantSubscriptions // Set<ConnectionId>
|
|
||||||
.groupBy { s -> ConnectionId(s.baseUrl, emptyMap()) }
|
|
||||||
.map { entry -> entry.key.copy(topicsToSubscriptionIds = entry.value.associate { s -> s.topic to s.id }) }
|
|
||||||
.toSet()
|
|
||||||
val newConnectionIds = desiredConnectionIds subtract activeConnectionIds
|
|
||||||
val obsoleteConnectionIds = activeConnectionIds subtract desiredConnectionIds
|
|
||||||
val match = activeConnectionIds == desiredConnectionIds
|
|
||||||
|
|
||||||
Log.d(TAG, "Refreshing subscriptions")
|
|
||||||
Log.d(TAG, "- Desired connections: $desiredConnectionIds")
|
|
||||||
Log.d(TAG, "- Active connections: $activeConnectionIds")
|
|
||||||
Log.d(TAG, "- New connections: $newConnectionIds")
|
|
||||||
Log.d(TAG, "- Obsolete connections: $obsoleteConnectionIds")
|
|
||||||
Log.d(TAG, "- Match? --> $match")
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
Log.d(TAG, "- No action required.")
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
// Open new connections
|
reallyRefreshConnections(this)
|
||||||
newConnectionIds.forEach { connectionId ->
|
} finally {
|
||||||
// FIXME since !!!
|
refreshMutex.unlock()
|
||||||
|
|
||||||
// Do NOT request old messages for new connections; we'll call poll() in MainActivity.
|
|
||||||
// This is important, so we don't download attachments from old messages, which is not desired.
|
|
||||||
|
|
||||||
val since = System.currentTimeMillis()/1000
|
|
||||||
val serviceActive = { -> isServiceStarted }
|
|
||||||
val user = repository.getUser(connectionId.baseUrl)
|
|
||||||
val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) {
|
|
||||||
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
|
|
||||||
WsConnection(connectionId, repository, user, since, ::onStateChanged, ::onNotificationReceived, alarmManager)
|
|
||||||
} else {
|
|
||||||
JsonConnection(connectionId, this, repository, api, user, since, ::onStateChanged, ::onNotificationReceived, serviceActive)
|
|
||||||
}
|
|
||||||
connections[connectionId] = connection
|
|
||||||
connection.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close connections without subscriptions
|
|
||||||
obsoleteConnectionIds.forEach { connectionId ->
|
|
||||||
val connection = connections.remove(connectionId)
|
|
||||||
connection?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update foreground service notification popup
|
|
||||||
if (connections.size > 0) {
|
|
||||||
synchronized(this) {
|
|
||||||
val title = getString(R.string.channel_subscriber_notification_title)
|
|
||||||
val text = if (BuildConfig.FIREBASE_AVAILABLE) {
|
|
||||||
when (instantSubscriptions.size) {
|
|
||||||
1 -> getString(R.string.channel_subscriber_notification_instant_text_one)
|
|
||||||
2 -> getString(R.string.channel_subscriber_notification_instant_text_two)
|
|
||||||
3 -> getString(R.string.channel_subscriber_notification_instant_text_three)
|
|
||||||
4 -> getString(R.string.channel_subscriber_notification_instant_text_four)
|
|
||||||
else -> getString(R.string.channel_subscriber_notification_instant_text_more, instantSubscriptions.size)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (instantSubscriptions.size) {
|
|
||||||
1 -> getString(R.string.channel_subscriber_notification_noinstant_text_one)
|
|
||||||
2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two)
|
|
||||||
3 -> getString(R.string.channel_subscriber_notification_noinstant_text_three)
|
|
||||||
4 -> getString(R.string.channel_subscriber_notification_noinstant_text_four)
|
|
||||||
else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serviceNotification = createNotification(title, text)
|
|
||||||
notificationManager?.notify(NOTIFICATION_SERVICE_ID, serviceNotification)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start/stop connections based on the desired state
|
||||||
|
* It is guaranteed that only one of function is run at a time (see mutex above).
|
||||||
|
*/
|
||||||
|
private suspend fun reallyRefreshConnections(scope: CoroutineScope) {
|
||||||
|
// Group INSTANT subscriptions by base URL, there is only one connection per base URL
|
||||||
|
val instantSubscriptions = repository.getSubscriptions()
|
||||||
|
.filter { s -> s.instant }
|
||||||
|
val activeConnectionIds = connections.keys().toList().toSet()
|
||||||
|
val desiredConnectionIds = instantSubscriptions // Set<ConnectionId>
|
||||||
|
.groupBy { s -> ConnectionId(s.baseUrl, emptyMap()) }
|
||||||
|
.map { entry -> entry.key.copy(topicsToSubscriptionIds = entry.value.associate { s -> s.topic to s.id }) }
|
||||||
|
.toSet()
|
||||||
|
val newConnectionIds = desiredConnectionIds.subtract(activeConnectionIds)
|
||||||
|
val obsoleteConnectionIds = activeConnectionIds.subtract(desiredConnectionIds)
|
||||||
|
val match = activeConnectionIds == desiredConnectionIds
|
||||||
|
val newSinceByBaseUrl = connections
|
||||||
|
.map { e ->
|
||||||
|
// Get last message timestamp to determine new ?since= param; set to $last+1 if it
|
||||||
|
// is defined to avoid retrieving old messages. See comment below too.
|
||||||
|
val lastMessage = e.value.since()
|
||||||
|
val newSince = if (lastMessage > 0) lastMessage+1 else 0
|
||||||
|
e.key.baseUrl to newSince
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
|
||||||
|
Log.d(TAG, "Refreshing subscriptions")
|
||||||
|
Log.d(TAG, "- Desired connections: $desiredConnectionIds")
|
||||||
|
Log.d(TAG, "- Active connections: $activeConnectionIds")
|
||||||
|
Log.d(TAG, "- New connections: $newConnectionIds")
|
||||||
|
Log.d(TAG, "- Obsolete connections: $obsoleteConnectionIds")
|
||||||
|
Log.d(TAG, "- Match? --> $match")
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
Log.d(TAG, "- No action required.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open new connections
|
||||||
|
newConnectionIds.forEach { connectionId ->
|
||||||
|
// IMPORTANT: Do NOT request old messages for new connections; we call poll() in MainActivity to
|
||||||
|
// retrieve old messages. This is important, so we don't download attachments from old messages.
|
||||||
|
|
||||||
|
val since = newSinceByBaseUrl[connectionId.baseUrl] ?: (System.currentTimeMillis() / 1000)
|
||||||
|
val serviceActive = { -> isServiceStarted }
|
||||||
|
val user = repository.getUser(connectionId.baseUrl)
|
||||||
|
val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) {
|
||||||
|
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
|
||||||
|
WsConnection(connectionId, repository, user, since, ::onStateChanged, ::onNotificationReceived, alarmManager)
|
||||||
|
} else {
|
||||||
|
JsonConnection(connectionId, scope, repository, api, user, since, ::onStateChanged, ::onNotificationReceived, serviceActive)
|
||||||
|
}
|
||||||
|
connections[connectionId] = connection
|
||||||
|
connection.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close connections without subscriptions
|
||||||
|
obsoleteConnectionIds.forEach { connectionId ->
|
||||||
|
val connection = connections.remove(connectionId)
|
||||||
|
connection?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update foreground service notification popup
|
||||||
|
if (connections.size > 0) {
|
||||||
|
val title = getString(R.string.channel_subscriber_notification_title)
|
||||||
|
val text = if (BuildConfig.FIREBASE_AVAILABLE) {
|
||||||
|
when (instantSubscriptions.size) {
|
||||||
|
1 -> getString(R.string.channel_subscriber_notification_instant_text_one)
|
||||||
|
2 -> getString(R.string.channel_subscriber_notification_instant_text_two)
|
||||||
|
3 -> getString(R.string.channel_subscriber_notification_instant_text_three)
|
||||||
|
4 -> getString(R.string.channel_subscriber_notification_instant_text_four)
|
||||||
|
else -> getString(R.string.channel_subscriber_notification_instant_text_more, instantSubscriptions.size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (instantSubscriptions.size) {
|
||||||
|
1 -> getString(R.string.channel_subscriber_notification_noinstant_text_one)
|
||||||
|
2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two)
|
||||||
|
3 -> getString(R.string.channel_subscriber_notification_noinstant_text_three)
|
||||||
|
4 -> getString(R.string.channel_subscriber_notification_noinstant_text_four)
|
||||||
|
else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serviceNotification = createNotification(title, text)
|
||||||
|
notificationManager?.notify(NOTIFICATION_SERVICE_ID, serviceNotification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onStateChanged(subscriptionIds: Collection<Long>, state: ConnectionState) {
|
private fun onStateChanged(subscriptionIds: Collection<Long>, state: ConnectionState) {
|
||||||
repository.updateState(subscriptionIds, state)
|
repository.updateState(subscriptionIds, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onNotificationReceived(subscription: Subscription, notification: io.heckel.ntfy.db.Notification) {
|
private fun onNotificationReceived(subscription: Subscription, notification: io.heckel.ntfy.db.Notification) {
|
||||||
// If permanent wakelock is not enabled, still take the wakelock while notifications are being dispatched
|
// Wakelock while notifications are being dispatched
|
||||||
if (!repository.getWakelockEnabled()) {
|
// Wakelocks are reference counted by default so that should work neatly here
|
||||||
// Wakelocks are reference counted by default so that should work neatly here
|
wakeLock?.acquire(NOTIFICATION_RECEIVED_WAKELOCK_TIMEOUT_MILLIS)
|
||||||
wakeLock?.acquire(10*60*1000L /*10 minutes*/)
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = topicUrl(subscription.baseUrl, subscription.topic)
|
val url = topicUrl(subscription.baseUrl, subscription.topic)
|
||||||
Log.d(TAG, "[$url] Received notification: $notification")
|
Log.d(TAG, "[$url] Received notification: $notification")
|
||||||
|
@ -245,12 +264,9 @@ class SubscriberService : Service() {
|
||||||
Log.d(TAG, "[$url] Dispatching notification $notification")
|
Log.d(TAG, "[$url] Dispatching notification $notification")
|
||||||
dispatcher.dispatch(subscription, notification)
|
dispatcher.dispatch(subscription, notification)
|
||||||
}
|
}
|
||||||
|
wakeLock?.let {
|
||||||
if (!repository.getWakelockEnabled()) {
|
if (it.isHeld) {
|
||||||
wakeLock?.let {
|
it.release()
|
||||||
if (it.isHeld) {
|
|
||||||
it.release()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -337,6 +353,7 @@ class SubscriberService : Service() {
|
||||||
private const val WAKE_LOCK_TAG = "SubscriberService:lock"
|
private const val WAKE_LOCK_TAG = "SubscriberService:lock"
|
||||||
private const val NOTIFICATION_CHANNEL_ID = "ntfy-subscriber"
|
private const val NOTIFICATION_CHANNEL_ID = "ntfy-subscriber"
|
||||||
private const val NOTIFICATION_SERVICE_ID = 2586
|
private const val NOTIFICATION_SERVICE_ID = 2586
|
||||||
|
private const val NOTIFICATION_RECEIVED_WAKELOCK_TIMEOUT_MILLIS = 10*60*1000L /*10 minutes*/
|
||||||
private const val SHARED_PREFS_ID = "SubscriberService"
|
private const val SHARED_PREFS_ID = "SubscriberService"
|
||||||
private const val SHARED_PREFS_SERVICE_STATE = "ServiceState"
|
private const val SHARED_PREFS_SERVICE_STATE = "ServiceState"
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ class SubscriberServiceManager(private val context: Context) {
|
||||||
Log.d(TAG, "Enqueuing work to refresh subscriber service")
|
Log.d(TAG, "Enqueuing work to refresh subscriber service")
|
||||||
val workManager = WorkManager.getInstance(context)
|
val workManager = WorkManager.getInstance(context)
|
||||||
val startServiceRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java).build()
|
val startServiceRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java).build()
|
||||||
workManager.enqueue(startServiceRequest)
|
workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restart() {
|
fun restart() {
|
||||||
|
@ -59,6 +59,7 @@ class SubscriberServiceManager(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "NtfySubscriberMgr"
|
const val TAG = "NtfySubscriberMgr"
|
||||||
|
const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
|
||||||
|
|
||||||
fun refresh(context: Context) {
|
fun refresh(context: Context) {
|
||||||
val manager = SubscriberServiceManager(context)
|
val manager = SubscriberServiceManager(context)
|
||||||
|
|
|
@ -43,43 +43,49 @@ class WsConnection(
|
||||||
.build()
|
.build()
|
||||||
private var errorCount = 0
|
private var errorCount = 0
|
||||||
private var webSocket: WebSocket? = null
|
private var webSocket: WebSocket? = null
|
||||||
private val listenerId = AtomicLong(0)
|
|
||||||
private var state: State? = null
|
private var state: State? = null
|
||||||
private var closed = false
|
private var closed = false
|
||||||
|
|
||||||
private var since: Long = sinceTime
|
private val globalId = GLOBAL_ID.incrementAndGet()
|
||||||
|
private val listenerId = AtomicLong(0)
|
||||||
|
|
||||||
|
private val since = AtomicLong(sinceTime)
|
||||||
private val baseUrl = connectionId.baseUrl
|
private val baseUrl = connectionId.baseUrl
|
||||||
private val topicsToSubscriptionIds = connectionId.topicsToSubscriptionIds
|
private val topicsToSubscriptionIds = connectionId.topicsToSubscriptionIds
|
||||||
private val subscriptionIds = topicsToSubscriptionIds.values
|
private val subscriptionIds = topicsToSubscriptionIds.values
|
||||||
private val topicsStr = topicsToSubscriptionIds.keys.joinToString(separator = ",")
|
private val topicsStr = topicsToSubscriptionIds.keys.joinToString(separator = ",")
|
||||||
private val shortUrl = topicShortUrl(baseUrl, topicsStr)
|
private val shortUrl = topicShortUrl(baseUrl, topicsStr)
|
||||||
|
|
||||||
|
init {
|
||||||
|
Log.d(TAG, "$shortUrl (gid=$globalId): New connection with global ID $globalId")
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun start() {
|
override fun start() {
|
||||||
if (closed || state == State.Connecting || state == State.Connected) {
|
if (closed || state == State.Connecting || state == State.Connected) {
|
||||||
Log.d(TAG,"$shortUrl: Not (re-)starting, because connection is marked closed/connecting/connected")
|
Log.d(TAG,"$shortUrl (gid=$globalId): Not (re-)starting, because connection is marked closed/connecting/connected")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (webSocket != null) {
|
if (webSocket != null) {
|
||||||
webSocket!!.close(WS_CLOSE_NORMAL, "")
|
webSocket!!.close(WS_CLOSE_NORMAL, "")
|
||||||
}
|
}
|
||||||
state = State.Connecting
|
state = State.Connecting
|
||||||
val nextId = listenerId.incrementAndGet()
|
val nextListenerId = listenerId.incrementAndGet()
|
||||||
val sinceVal = if (since == 0L) "all" else since.toString()
|
val sinceVal = if (since.get() == 0L) "all" else since.get().toString()
|
||||||
val urlWithSince = topicUrlWs(baseUrl, topicsStr, sinceVal)
|
val urlWithSince = topicUrlWs(baseUrl, topicsStr, sinceVal)
|
||||||
val request = requestBuilder(urlWithSince, user).build()
|
val request = requestBuilder(urlWithSince, user).build()
|
||||||
Log.d(TAG, "$shortUrl: Opening $urlWithSince with listener ID $nextId ...")
|
Log.d(TAG, "$shortUrl (gid=$globalId): Opening $urlWithSince with listener ID $nextListenerId ...")
|
||||||
webSocket = client.newWebSocket(request, Listener(nextId))
|
webSocket = client.newWebSocket(request, Listener(nextListenerId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun close() {
|
override fun close() {
|
||||||
closed = true
|
closed = true
|
||||||
if (webSocket == null) {
|
if (webSocket == null) {
|
||||||
Log.d(TAG,"$shortUrl: Not closing existing connection, because there is no active web socket")
|
Log.d(TAG,"$shortUrl (gid=$globalId): Not closing existing connection, because there is no active web socket")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.d(TAG, "$shortUrl: Closing existing connection")
|
Log.d(TAG, "$shortUrl (gid=$globalId): Closing connection")
|
||||||
state = State.Disconnected
|
state = State.Disconnected
|
||||||
webSocket!!.close(WS_CLOSE_NORMAL, "")
|
webSocket!!.close(WS_CLOSE_NORMAL, "")
|
||||||
webSocket = null
|
webSocket = null
|
||||||
|
@ -87,23 +93,23 @@ class WsConnection(
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun since(): Long {
|
override fun since(): Long {
|
||||||
return since
|
return since.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun scheduleReconnect(seconds: Int) {
|
fun scheduleReconnect(seconds: Int) {
|
||||||
if (closed || state == State.Connecting || state == State.Connected) {
|
if (closed || state == State.Connecting || state == State.Connected) {
|
||||||
Log.d(TAG,"$shortUrl: Not rescheduling connection, because connection is marked closed/connecting/connected")
|
Log.d(TAG,"$shortUrl (gid=$globalId): Not rescheduling connection, because connection is marked closed/connecting/connected")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state = State.Scheduled
|
state = State.Scheduled
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
Log.d(TAG,"$shortUrl: Scheduling a restart in $seconds seconds (via alarm manager)")
|
Log.d(TAG,"$shortUrl (gid=$globalId): Scheduling a restart in $seconds seconds (via alarm manager)")
|
||||||
val reconnectTime = Calendar.getInstance()
|
val reconnectTime = Calendar.getInstance()
|
||||||
reconnectTime.add(Calendar.SECOND, seconds)
|
reconnectTime.add(Calendar.SECOND, seconds)
|
||||||
alarmManager.setExact(AlarmManager.RTC_WAKEUP, reconnectTime.timeInMillis, RECONNECT_TAG, { start() }, null)
|
alarmManager.setExact(AlarmManager.RTC_WAKEUP, reconnectTime.timeInMillis, RECONNECT_TAG, { start() }, null)
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "$shortUrl: Scheduling a restart in $seconds seconds (via handler)")
|
Log.d(TAG, "$shortUrl (gid=$globalId): Scheduling a restart in $seconds seconds (via handler)")
|
||||||
val handler = Handler(Looper.getMainLooper())
|
val handler = Handler(Looper.getMainLooper())
|
||||||
handler.postDelayed({ start() }, TimeUnit.SECONDS.toMillis(seconds.toLong()))
|
handler.postDelayed({ start() }, TimeUnit.SECONDS.toMillis(seconds.toLong()))
|
||||||
}
|
}
|
||||||
|
@ -112,7 +118,7 @@ class WsConnection(
|
||||||
private inner class Listener(private val id: Long) : WebSocketListener() {
|
private inner class Listener(private val id: Long) : WebSocketListener() {
|
||||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
synchronize("onOpen") {
|
synchronize("onOpen") {
|
||||||
Log.d(TAG, "$shortUrl (listener $id): Opened connection")
|
Log.d(TAG, "$shortUrl (gid=$globalId, lid=$id): Opened connection")
|
||||||
state = State.Connected
|
state = State.Connected
|
||||||
if (errorCount > 0) {
|
if (errorCount > 0) {
|
||||||
errorCount = 0
|
errorCount = 0
|
||||||
|
@ -123,10 +129,10 @@ class WsConnection(
|
||||||
|
|
||||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
synchronize("onMessage") {
|
synchronize("onMessage") {
|
||||||
Log.d(TAG, "$shortUrl (listener $id): Received message: $text")
|
Log.d(TAG, "$shortUrl (gid=$globalId, lid=$id): Received message: $text")
|
||||||
val notificationWithTopic = parser.parseWithTopic(text, subscriptionId = 0, notificationId = Random.nextInt())
|
val notificationWithTopic = parser.parseWithTopic(text, subscriptionId = 0, notificationId = Random.nextInt())
|
||||||
if (notificationWithTopic == null) {
|
if (notificationWithTopic == null) {
|
||||||
Log.d(TAG, "$shortUrl (listener $id): Irrelevant or unknown message. Discarding.")
|
Log.d(TAG, "$shortUrl (gid=$globalId, lid=$id): Irrelevant or unknown message. Discarding.")
|
||||||
return@synchronize
|
return@synchronize
|
||||||
}
|
}
|
||||||
val topic = notificationWithTopic.topic
|
val topic = notificationWithTopic.topic
|
||||||
|
@ -135,13 +141,13 @@ class WsConnection(
|
||||||
val subscription = repository.getSubscription(subscriptionId) ?: return@synchronize
|
val subscription = repository.getSubscription(subscriptionId) ?: return@synchronize
|
||||||
val notificationWithSubscriptionId = notification.copy(subscriptionId = subscription.id)
|
val notificationWithSubscriptionId = notification.copy(subscriptionId = subscription.id)
|
||||||
notificationListener(subscription, notificationWithSubscriptionId)
|
notificationListener(subscription, notificationWithSubscriptionId)
|
||||||
since = notification.timestamp
|
since.set(notification.timestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
synchronize("onClosed") {
|
synchronize("onClosed") {
|
||||||
Log.w(TAG, "$shortUrl (listener $id): Closed connection")
|
Log.w(TAG, "$shortUrl (gid=$globalId, lid=$id): Closed connection")
|
||||||
state = State.Disconnected
|
state = State.Disconnected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,12 +155,12 @@ class WsConnection(
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
synchronize("onFailure") {
|
synchronize("onFailure") {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
Log.e(TAG, "$shortUrl (listener $id): Connection failed (response is null): ${t.message}", t)
|
Log.e(TAG, "$shortUrl (gid=$globalId, lid=$id): Connection failed (response is null): ${t.message}", t)
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "$shortUrl (listener $id): Connection failed (response code ${response.code}, message: ${response.message}): ${t.message}", t)
|
Log.e(TAG, "$shortUrl (gid=$globalId, lid=$id): Connection failed (response code ${response.code}, message: ${response.message}): ${t.message}", t)
|
||||||
}
|
}
|
||||||
if (closed) {
|
if (closed) {
|
||||||
Log.d(TAG, "$shortUrl (listener $id): Connection marked as closed. Not retrying.")
|
Log.d(TAG, "$shortUrl (gid=$globalId, lid=$id): Connection marked as closed. Not retrying.")
|
||||||
return@synchronize
|
return@synchronize
|
||||||
}
|
}
|
||||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
||||||
|
@ -170,7 +176,7 @@ class WsConnection(
|
||||||
if (listenerId.get() == id) {
|
if (listenerId.get() == id) {
|
||||||
fn()
|
fn()
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "$shortUrl (listener $id): Skipping synchronized block '$tag', because listener ID does not match ${listenerId.get()}")
|
Log.w(TAG, "$shortUrl (gid=$globalId, lid=$id): Skipping synchronized block '$tag', because listener ID does not match ${listenerId.get()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,5 +191,6 @@ class WsConnection(
|
||||||
private const val RECONNECT_TAG = "WsReconnect"
|
private const val RECONNECT_TAG = "WsReconnect"
|
||||||
private const val WS_CLOSE_NORMAL = 1000
|
private const val WS_CLOSE_NORMAL = 1000
|
||||||
private val RETRY_SECONDS = listOf(5, 10, 15, 20, 30, 45, 60, 120)
|
private val RETRY_SECONDS = listOf(5, 10, 15, 20, 30, 45, 60, 120)
|
||||||
|
private val GLOBAL_ID = AtomicLong(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,14 +46,16 @@ class AddFragment : DialogFragment() {
|
||||||
private lateinit var subscribeInstantDeliveryCheckbox: CheckBox
|
private lateinit var subscribeInstantDeliveryCheckbox: CheckBox
|
||||||
private lateinit var subscribeInstantDeliveryDescription: View
|
private lateinit var subscribeInstantDeliveryDescription: View
|
||||||
private lateinit var subscribeProgress: ProgressBar
|
private lateinit var subscribeProgress: ProgressBar
|
||||||
private lateinit var subscribeErrorImage: View
|
private lateinit var subscribeErrorText: TextView
|
||||||
|
private lateinit var subscribeErrorTextImage: View
|
||||||
|
|
||||||
// Login page
|
// Login page
|
||||||
private lateinit var users: List<User>
|
private lateinit var users: List<User>
|
||||||
private lateinit var loginUsernameText: TextInputEditText
|
private lateinit var loginUsernameText: TextInputEditText
|
||||||
private lateinit var loginPasswordText: TextInputEditText
|
private lateinit var loginPasswordText: TextInputEditText
|
||||||
private lateinit var loginProgress: ProgressBar
|
private lateinit var loginProgress: ProgressBar
|
||||||
private lateinit var loginErrorImage: View
|
private lateinit var loginErrorText: TextView
|
||||||
|
private lateinit var loginErrorTextImage: View
|
||||||
|
|
||||||
private lateinit var baseUrls: List<String> // List of base URLs already used, excluding app_base_url
|
private lateinit var baseUrls: List<String> // List of base URLs already used, excluding app_base_url
|
||||||
|
|
||||||
|
@ -84,22 +86,26 @@ class AddFragment : DialogFragment() {
|
||||||
loginView.visibility = View.GONE
|
loginView.visibility = View.GONE
|
||||||
|
|
||||||
// Fields for "subscribe page"
|
// Fields for "subscribe page"
|
||||||
subscribeTopicText = view.findViewById(R.id.add_dialog_topic_text)
|
subscribeTopicText = view.findViewById(R.id.add_dialog_subscribe_topic_text)
|
||||||
subscribeBaseUrlLayout = view.findViewById(R.id.add_dialog_base_url_layout)
|
subscribeBaseUrlLayout = view.findViewById(R.id.add_dialog_subscribe_base_url_layout)
|
||||||
subscribeBaseUrlText = view.findViewById(R.id.add_dialog_base_url_text)
|
subscribeBaseUrlText = view.findViewById(R.id.add_dialog_subscribe_base_url_text)
|
||||||
subscribeInstantDeliveryBox = view.findViewById(R.id.add_dialog_instant_delivery_box)
|
subscribeInstantDeliveryBox = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_box)
|
||||||
subscribeInstantDeliveryCheckbox = view.findViewById(R.id.add_dialog_instant_delivery_checkbox)
|
subscribeInstantDeliveryCheckbox = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_checkbox)
|
||||||
subscribeInstantDeliveryDescription = view.findViewById(R.id.add_dialog_instant_delivery_description)
|
subscribeInstantDeliveryDescription = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_description)
|
||||||
subscribeUseAnotherServerCheckbox = view.findViewById(R.id.add_dialog_use_another_server_checkbox)
|
subscribeUseAnotherServerCheckbox = view.findViewById(R.id.add_dialog_subscribe_use_another_server_checkbox)
|
||||||
subscribeUseAnotherServerDescription = view.findViewById(R.id.add_dialog_use_another_server_description)
|
subscribeUseAnotherServerDescription = view.findViewById(R.id.add_dialog_subscribe_use_another_server_description)
|
||||||
subscribeProgress = view.findViewById(R.id.add_dialog_progress)
|
subscribeProgress = view.findViewById(R.id.add_dialog_subscribe_progress)
|
||||||
subscribeErrorImage = view.findViewById(R.id.add_dialog_error_image)
|
subscribeErrorText = view.findViewById(R.id.add_dialog_subscribe_error_text)
|
||||||
|
subscribeErrorText.visibility = View.GONE
|
||||||
|
subscribeErrorTextImage = view.findViewById(R.id.add_dialog_subscribe_error_text_image)
|
||||||
|
subscribeErrorTextImage.visibility = View.GONE
|
||||||
|
|
||||||
// Fields for "login page"
|
// Fields for "login page"
|
||||||
loginUsernameText = view.findViewById(R.id.add_dialog_login_username)
|
loginUsernameText = view.findViewById(R.id.add_dialog_login_username)
|
||||||
loginPasswordText = view.findViewById(R.id.add_dialog_login_password)
|
loginPasswordText = view.findViewById(R.id.add_dialog_login_password)
|
||||||
loginProgress = view.findViewById(R.id.add_dialog_login_progress)
|
loginProgress = view.findViewById(R.id.add_dialog_login_progress)
|
||||||
loginErrorImage = view.findViewById(R.id.add_dialog_login_error_image)
|
loginErrorText = view.findViewById(R.id.add_dialog_login_error_text)
|
||||||
|
loginErrorTextImage = view.findViewById(R.id.add_dialog_login_error_text_image)
|
||||||
|
|
||||||
// Set "Use another server" description based on flavor
|
// Set "Use another server" description based on flavor
|
||||||
subscribeUseAnotherServerDescription.text = if (BuildConfig.FIREBASE_AVAILABLE) {
|
subscribeUseAnotherServerDescription.text = if (BuildConfig.FIREBASE_AVAILABLE) {
|
||||||
|
@ -268,7 +274,8 @@ class AddFragment : DialogFragment() {
|
||||||
|
|
||||||
private fun checkReadAndMaybeShowLogin(baseUrl: String, topic: String) {
|
private fun checkReadAndMaybeShowLogin(baseUrl: String, topic: String) {
|
||||||
subscribeProgress.visibility = View.VISIBLE
|
subscribeProgress.visibility = View.VISIBLE
|
||||||
subscribeErrorImage.visibility = View.GONE
|
subscribeErrorText.visibility = View.GONE
|
||||||
|
subscribeErrorTextImage.visibility = View.GONE
|
||||||
enableSubscribeView(false)
|
enableSubscribeView(false)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
@ -280,7 +287,7 @@ class AddFragment : DialogFragment() {
|
||||||
} else {
|
} else {
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, but user already exists")
|
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, but user already exists")
|
||||||
showToastAndReenableSubscribeView(getString(R.string.add_dialog_login_error_not_authorized))
|
showErrorAndReenableSubscribeView(getString(R.string.add_dialog_login_error_not_authorized))
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog")
|
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog")
|
||||||
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
||||||
|
@ -291,26 +298,26 @@ class AddFragment : DialogFragment() {
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Connection to topic failed: ${e.message}", e)
|
Log.w(TAG, "Connection to topic failed: ${e.message}", e)
|
||||||
showToastAndReenableSubscribeView(e.message)
|
showErrorAndReenableSubscribeView(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showToastAndReenableSubscribeView(message: String?) {
|
private fun showErrorAndReenableSubscribeView(message: String?) {
|
||||||
val activity = activity ?: return // We may have pressed "Cancel"
|
val activity = activity ?: return // We may have pressed "Cancel"
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
subscribeProgress.visibility = View.GONE
|
subscribeProgress.visibility = View.GONE
|
||||||
subscribeErrorImage.visibility = View.VISIBLE
|
subscribeErrorText.visibility = View.VISIBLE
|
||||||
|
subscribeErrorText.text = message
|
||||||
|
subscribeErrorTextImage.visibility = View.VISIBLE
|
||||||
enableSubscribeView(true)
|
enableSubscribeView(true)
|
||||||
Toast
|
|
||||||
.makeText(context, message, Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loginAndMaybeDismiss(baseUrl: String, topic: String) {
|
private fun loginAndMaybeDismiss(baseUrl: String, topic: String) {
|
||||||
loginProgress.visibility = View.VISIBLE
|
loginProgress.visibility = View.VISIBLE
|
||||||
loginErrorImage.visibility = View.GONE
|
loginErrorText.visibility = View.GONE
|
||||||
|
loginErrorTextImage.visibility = View.GONE
|
||||||
enableLoginView(false)
|
enableLoginView(false)
|
||||||
val user = User(
|
val user = User(
|
||||||
baseUrl = baseUrl,
|
baseUrl = baseUrl,
|
||||||
|
@ -327,24 +334,23 @@ class AddFragment : DialogFragment() {
|
||||||
dismissDialog()
|
dismissDialog()
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Access not allowed for user ${user.username} to topic ${topicUrl(baseUrl, topic)}")
|
Log.w(TAG, "Access not allowed for user ${user.username} to topic ${topicUrl(baseUrl, topic)}")
|
||||||
showToastAndReenableLoginView(getString(R.string.add_dialog_login_error_not_authorized))
|
showErrorAndReenableLoginView(getString(R.string.add_dialog_login_error_not_authorized))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Connection to topic failed during login: ${e.message}", e)
|
Log.w(TAG, "Connection to topic failed during login: ${e.message}", e)
|
||||||
showToastAndReenableLoginView(e.message)
|
showErrorAndReenableLoginView(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showToastAndReenableLoginView(message: String?) {
|
private fun showErrorAndReenableLoginView(message: String?) {
|
||||||
val activity = activity ?: return // We may have pressed "Cancel"
|
val activity = activity ?: return // We may have pressed "Cancel"
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
loginProgress.visibility = View.GONE
|
loginProgress.visibility = View.GONE
|
||||||
loginErrorImage.visibility = View.VISIBLE
|
loginErrorText.visibility = View.VISIBLE
|
||||||
|
loginErrorText.text = message
|
||||||
|
loginErrorTextImage.visibility = View.VISIBLE
|
||||||
enableLoginView(true)
|
enableLoginView(true)
|
||||||
Toast
|
|
||||||
.makeText(context, message, Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,7 +454,8 @@ class AddFragment : DialogFragment() {
|
||||||
|
|
||||||
private fun resetSubscribeView() {
|
private fun resetSubscribeView() {
|
||||||
subscribeProgress.visibility = View.GONE
|
subscribeProgress.visibility = View.GONE
|
||||||
subscribeErrorImage.visibility = View.GONE
|
subscribeErrorText.visibility = View.GONE
|
||||||
|
subscribeErrorTextImage.visibility = View.GONE
|
||||||
enableSubscribeView(true)
|
enableSubscribeView(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,7 +471,8 @@ class AddFragment : DialogFragment() {
|
||||||
|
|
||||||
private fun resetLoginView() {
|
private fun resetLoginView() {
|
||||||
loginProgress.visibility = View.GONE
|
loginProgress.visibility = View.GONE
|
||||||
loginErrorImage.visibility = View.GONE
|
loginErrorText.visibility = View.GONE
|
||||||
|
loginErrorTextImage.visibility = View.GONE
|
||||||
loginUsernameText.visibility = View.VISIBLE
|
loginUsernameText.visibility = View.VISIBLE
|
||||||
loginUsernameText.text?.clear()
|
loginUsernameText.text?.clear()
|
||||||
loginPasswordText.visibility = View.VISIBLE
|
loginPasswordText.visibility = View.VISIBLE
|
||||||
|
|
|
@ -281,8 +281,17 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
api.publish(subscriptionBaseUrl, subscriptionTopic, user, message, title, priority, tags, delay = "")
|
api.publish(subscriptionBaseUrl, subscriptionTopic, user, message, title, priority, tags, delay = "")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
|
val message = if (e is ApiService.UnauthorizedException) {
|
||||||
|
if (e.user != null) {
|
||||||
|
getString(R.string.detail_test_message_error_unauthorized_user, e.user.username)
|
||||||
|
} else {
|
||||||
|
getString(R.string.detail_test_message_error_unauthorized_anon)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getString(R.string.detail_test_message_error, e.message)
|
||||||
|
}
|
||||||
Toast
|
Toast
|
||||||
.makeText(this@DetailActivity, getString(R.string.detail_test_message_error, e.message), Toast.LENGTH_LONG)
|
.makeText(this@DetailActivity, message, Toast.LENGTH_LONG)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription settings
|
* Subscription settings
|
||||||
|
*
|
||||||
|
* THIS IS CURRENTLY UNUSED.
|
||||||
*/
|
*/
|
||||||
class DetailSettingsActivity : AppCompatActivity() {
|
class DetailSettingsActivity : AppCompatActivity() {
|
||||||
private lateinit var repository: Repository
|
private lateinit var repository: Repository
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
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
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -98,14 +100,9 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
pref: Preference
|
pref: Preference
|
||||||
): Boolean {
|
): Boolean {
|
||||||
// Instantiate the new Fragment
|
// Instantiate the new Fragment
|
||||||
val args = pref.extras
|
val fragmentClass = pref.fragment ?: return false
|
||||||
val fragment = supportFragmentManager.fragmentFactory.instantiate(
|
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, fragmentClass)
|
||||||
classLoader,
|
fragment.arguments = pref.extras
|
||||||
pref.fragment!!
|
|
||||||
).apply {
|
|
||||||
arguments = args
|
|
||||||
setTargetFragment(caller, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the existing Fragment with the new Fragment
|
// Replace the existing Fragment with the new Fragment
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
|
@ -118,7 +115,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
if (fragment is UserSettingsFragment) {
|
if (fragment is UserSettingsFragment) {
|
||||||
userSettingsFragment = fragment
|
userSettingsFragment = fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,8 +327,10 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
exportLogs?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
|
exportLogs?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
|
||||||
exportLogs?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, v ->
|
exportLogs?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, v ->
|
||||||
when (v) {
|
when (v) {
|
||||||
EXPORT_LOGS_COPY -> copyLogsToClipboard()
|
EXPORT_LOGS_COPY_ORIGINAL -> copyLogsToClipboard(scrub = false)
|
||||||
EXPORT_LOGS_UPLOAD -> uploadLogsToNopaste()
|
EXPORT_LOGS_COPY_SCRUBBED -> copyLogsToClipboard(scrub = true)
|
||||||
|
EXPORT_LOGS_UPLOAD_ORIGINAL -> uploadLogsToNopaste(scrub = false)
|
||||||
|
EXPORT_LOGS_UPLOAD_SCRUBBED -> uploadLogsToNopaste(scrub = true)
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -368,6 +366,15 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
getString(R.string.settings_advanced_record_logs_summary_disabled)
|
getString(R.string.settings_advanced_record_logs_summary_disabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
recordLogsEnabled?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, v ->
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
repository.getSubscriptions().forEach { s ->
|
||||||
|
Log.addScrubTerm(shortUrl(s.baseUrl), Log.TermType.Domain)
|
||||||
|
Log.addScrubTerm(s.topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
// Connection protocol
|
// Connection protocol
|
||||||
val connectionProtocolPrefId = context?.getString(R.string.settings_advanced_connection_protocol_key) ?: return
|
val connectionProtocolPrefId = context?.getString(R.string.settings_advanced_connection_protocol_key) ?: return
|
||||||
|
@ -390,27 +397,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
@ -441,25 +427,29 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
serviceManager.restart() // Service will auto-restart
|
serviceManager.restart() // Service will auto-restart
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyLogsToClipboard() {
|
private fun copyLogsToClipboard(scrub: Boolean) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val log = Log.getFormatted()
|
val log = Log.getFormatted(scrub = scrub)
|
||||||
val context = context ?: return@launch
|
val context = context ?: return@launch
|
||||||
requireActivity().runOnUiThread {
|
requireActivity().runOnUiThread {
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("ntfy logs", log)
|
val clip = ClipData.newPlainText("ntfy logs", log)
|
||||||
clipboard.setPrimaryClip(clip)
|
clipboard.setPrimaryClip(clip)
|
||||||
Toast
|
if (scrub) {
|
||||||
.makeText(context, getString(R.string.settings_advanced_export_logs_copied_logs), Toast.LENGTH_LONG)
|
showScrubDialog(getString(R.string.settings_advanced_export_logs_copied_logs))
|
||||||
.show()
|
} else {
|
||||||
|
Toast
|
||||||
|
.makeText(context, getString(R.string.settings_advanced_export_logs_copied_logs), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun uploadLogsToNopaste() {
|
private fun uploadLogsToNopaste(scrub: Boolean) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
Log.d(TAG, "Uploading log to $EXPORT_LOGS_UPLOAD_URL ...")
|
Log.d(TAG, "Uploading log to $EXPORT_LOGS_UPLOAD_URL ...")
|
||||||
val log = Log.getFormatted()
|
val log = Log.getFormatted(scrub = scrub)
|
||||||
if (log.length > EXPORT_LOGS_UPLOAD_NOTIFY_SIZE_THRESHOLD) {
|
if (log.length > EXPORT_LOGS_UPLOAD_NOTIFY_SIZE_THRESHOLD) {
|
||||||
requireActivity().runOnUiThread {
|
requireActivity().runOnUiThread {
|
||||||
Toast
|
Toast
|
||||||
|
@ -492,9 +482,13 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("logs URL", resp.url)
|
val clip = ClipData.newPlainText("logs URL", resp.url)
|
||||||
clipboard.setPrimaryClip(clip)
|
clipboard.setPrimaryClip(clip)
|
||||||
Toast
|
if (scrub) {
|
||||||
.makeText(context, getString(R.string.settings_advanced_export_logs_copied_url), Toast.LENGTH_LONG)
|
showScrubDialog(getString(R.string.settings_advanced_export_logs_copied_url))
|
||||||
.show()
|
} else {
|
||||||
|
Toast
|
||||||
|
.makeText(context, getString(R.string.settings_advanced_export_logs_copied_url), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -509,6 +503,22 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showScrubDialog(title: String) {
|
||||||
|
val scrubbed = Log.getScrubTerms()
|
||||||
|
val scrubbedText = if (scrubbed.isNotEmpty()) {
|
||||||
|
val scrubTerms = scrubbed.map { e -> "${e.key} -> ${e.value}"}.joinToString(separator = "\n")
|
||||||
|
getString(R.string.settings_advanced_export_logs_scrub_dialog_text, scrubTerms)
|
||||||
|
} else {
|
||||||
|
getString(R.string.settings_advanced_export_logs_scrub_dialog_empty)
|
||||||
|
}
|
||||||
|
val dialog = AlertDialog.Builder(activity)
|
||||||
|
.setTitle(title)
|
||||||
|
.setMessage(scrubbedText)
|
||||||
|
.setPositiveButton(R.string.settings_advanced_export_logs_scrub_dialog_button_ok) { _, _ -> /* Nothing */ }
|
||||||
|
.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun deleteLogs() {
|
private fun deleteLogs() {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
Log.deleteAll()
|
Log.deleteAll()
|
||||||
|
@ -657,8 +667,10 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
private const val TITLE_TAG = "title"
|
private const val TITLE_TAG = "title"
|
||||||
private const val REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD = 2586
|
private const val REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD = 2586
|
||||||
private const val AUTO_DOWNLOAD_SELECTION_NOT_SET = -99L
|
private const val AUTO_DOWNLOAD_SELECTION_NOT_SET = -99L
|
||||||
private const val EXPORT_LOGS_COPY = "copy"
|
private const val EXPORT_LOGS_COPY_ORIGINAL = "copy_original"
|
||||||
private const val EXPORT_LOGS_UPLOAD = "upload"
|
private const val EXPORT_LOGS_COPY_SCRUBBED = "copy_scrubbed"
|
||||||
|
private const val EXPORT_LOGS_UPLOAD_ORIGINAL = "upload_original"
|
||||||
|
private const val EXPORT_LOGS_UPLOAD_SCRUBBED = "upload_scrubbed"
|
||||||
private const val EXPORT_LOGS_UPLOAD_URL = "https://nopaste.net/?f=json" // Run by binwiederhier; see https://github.com/binwiederhier/pcopy
|
private const val EXPORT_LOGS_UPLOAD_URL = "https://nopaste.net/?f=json" // Run by binwiederhier; see https://github.com/binwiederhier/pcopy
|
||||||
private const val EXPORT_LOGS_UPLOAD_NOTIFY_SIZE_THRESHOLD = 100 * 1024 // Show "Uploading ..." if log larger than X
|
private const val EXPORT_LOGS_UPLOAD_NOTIFY_SIZE_THRESHOLD = 100 * 1024 // Show "Uploading ..." if log larger than X
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,16 @@ class UserFragment : DialogFragment() {
|
||||||
usernameView.addTextChangedListener(textWatcher)
|
usernameView.addTextChangedListener(textWatcher)
|
||||||
passwordView.addTextChangedListener(textWatcher)
|
passwordView.addTextChangedListener(textWatcher)
|
||||||
|
|
||||||
|
// Focus
|
||||||
|
if (user != null) {
|
||||||
|
usernameView.requestFocus()
|
||||||
|
if (usernameView.text != null) {
|
||||||
|
usernameView.setSelection(usernameView.text!!.length)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
baseUrlView.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
// Validate now!
|
// Validate now!
|
||||||
validateInput()
|
validateInput()
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,9 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:id="@+id/add_dialog_subscribe_view"
|
android:id="@+id/add_dialog_subscribe_view"
|
||||||
android:visibility="gone">
|
android:visibility="visible">
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/add_dialog_title_text"
|
android:id="@+id/add_dialog_subscribe_title_text"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingTop="16dp"
|
android:paddingTop="16dp"
|
||||||
|
@ -24,52 +24,51 @@
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/add_dialog_error_image"/>
|
/>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
style="?android:attr/progressBarStyle"
|
style="?android:attr/progressBarStyle"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:id="@+id/add_dialog_progress"
|
android:id="@+id/add_dialog_subscribe_progress"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/add_dialog_error_image"
|
app:layout_constraintBottom_toTopOf="@+id/add_dialog_subscribe_description"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/add_dialog_description_below"
|
|
||||||
android:indeterminate="true" android:layout_marginBottom="5dp" android:visibility="gone"/>
|
android:indeterminate="true" android:layout_marginBottom="5dp" android:visibility="gone"/>
|
||||||
<TextView
|
<TextView
|
||||||
android:text="@string/add_dialog_description_below"
|
android:text="@string/add_dialog_description_below"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_description_below"
|
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_description"
|
||||||
android:paddingStart="4dp" android:paddingTop="3dp" app:layout_constraintStart_toStartOf="parent"
|
android:paddingStart="4dp" android:paddingTop="3dp" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_title_text"/>
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_title_text"/>
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/add_dialog_topic_text"
|
android:id="@+id/add_dialog_subscribe_topic_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:hint="@string/add_dialog_topic_name_hint"
|
android:layout_height="wrap_content" android:hint="@string/add_dialog_topic_name_hint"
|
||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
android:maxLines="1" android:inputType="text" android:maxLength="64"
|
android:maxLines="1" android:inputType="text" android:maxLength="64"
|
||||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_description_below"/>
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_description"/>
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:text="@string/add_dialog_use_another_server"
|
android:text="@string/add_dialog_use_another_server"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_use_another_server_checkbox"
|
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_use_another_server_checkbox"
|
||||||
android:layout_marginStart="-3dp" app:layout_constraintStart_toStartOf="parent"
|
android:layout_marginStart="-3dp" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_topic_text"
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_topic_text"
|
||||||
android:layout_marginTop="-3dp"/>
|
android:layout_marginTop="-3dp"/>
|
||||||
<TextView
|
<TextView
|
||||||
android:text="@string/add_dialog_use_another_server_description"
|
android:text="@string/add_dialog_use_another_server_description"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_use_another_server_description"
|
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_use_another_server_description"
|
||||||
android:paddingStart="4dp" android:paddingTop="0dp"
|
android:paddingStart="4dp" android:paddingTop="0dp"
|
||||||
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
|
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_use_another_server_checkbox"
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_use_another_server_checkbox"
|
||||||
android:layout_marginTop="-5dp"/>
|
android:layout_marginTop="-5dp"/>
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense.ExposedDropdownMenu"
|
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense.ExposedDropdownMenu"
|
||||||
android:id="@+id/add_dialog_base_url_layout"
|
android:id="@+id/add_dialog_subscribe_base_url_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:layout_margin="0dp"
|
android:layout_margin="0dp"
|
||||||
|
@ -80,11 +79,11 @@
|
||||||
app:hintEnabled="false"
|
app:hintEnabled="false"
|
||||||
app:boxBackgroundColor="@android:color/transparent" app:layout_constraintStart_toStartOf="parent"
|
app:boxBackgroundColor="@android:color/transparent" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_use_another_server_description">
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_use_another_server_description">
|
||||||
<AutoCompleteTextView
|
<AutoCompleteTextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:id="@+id/add_dialog_base_url_text"
|
android:id="@+id/add_dialog_subscribe_base_url_text"
|
||||||
android:hint="@string/app_base_url"
|
android:hint="@string/app_base_url"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:layout_marginTop="0dp"
|
android:layout_marginTop="0dp"
|
||||||
|
@ -102,19 +101,19 @@
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_instant_delivery_box"
|
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_box"
|
||||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_base_url_layout" android:layout_marginTop="-3dp">
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_base_url_layout" android:layout_marginTop="-3dp">
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:text="@string/add_dialog_instant_delivery"
|
android:text="@string/add_dialog_instant_delivery"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_instant_delivery_checkbox"
|
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_checkbox"
|
||||||
android:layout_marginTop="-8dp" android:layout_marginBottom="-5dp"
|
android:layout_marginTop="-8dp" android:layout_marginBottom="-5dp"
|
||||||
android:layout_marginStart="-3dp"/>
|
android:layout_marginStart="-3dp"/>
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_bolt_gray_24dp"
|
android:layout_height="24dp" app:srcCompat="@drawable/ic_bolt_gray_24dp"
|
||||||
android:id="@+id/add_dialog_instant_image"
|
android:id="@+id/add_dialog_subscribe_instant_image"
|
||||||
app:layout_constraintTop_toTopOf="@+id/main_item_text"
|
app:layout_constraintTop_toTopOf="@+id/main_item_text"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/main_item_date" android:paddingTop="3dp"
|
app:layout_constraintEnd_toStartOf="@+id/main_item_date" android:paddingTop="3dp"
|
||||||
android:layout_marginTop="3dp"/>
|
android:layout_marginTop="3dp"/>
|
||||||
|
@ -122,25 +121,31 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:text="@string/add_dialog_instant_delivery_description"
|
android:text="@string/add_dialog_instant_delivery_description"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_instant_delivery_description"
|
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_description"
|
||||||
android:paddingStart="4dp" android:paddingTop="0dp"
|
android:paddingStart="4dp" android:paddingTop="0dp"
|
||||||
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
|
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_instant_delivery_box"/>
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_box"/>
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="24dp"
|
android:layout_width="20dp"
|
||||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_error_black_24dp"
|
android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp"
|
||||||
android:id="@+id/add_dialog_error_image"
|
android:id="@+id/add_dialog_subscribe_error_text_image"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/add_dialog_description_below"
|
android:visibility="gone"
|
||||||
android:layout_marginBottom="5dp" app:layout_constraintEnd_toStartOf="@+id/add_dialog_progress"
|
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/add_dialog_subscribe_error_text" android:layout_marginTop="1dp"/>
|
||||||
app:layout_constraintStart_toEndOf="@+id/add_dialog_title_text" android:visibility="gone"/>
|
<TextView
|
||||||
|
android:text="Unable to resolve host example.com"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_error_text"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_description" android:paddingEnd="4dp" android:textColor="@color/primaryDangerButtonColor" app:layout_constraintStart_toEndOf="@id/add_dialog_subscribe_error_text_image" android:layout_marginTop="5dp" tools:visibility="gone"/>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:id="@+id/add_dialog_login_view"
|
android:id="@+id/add_dialog_login_view"
|
||||||
android:visibility="visible"
|
android:visibility="gone"
|
||||||
>
|
>
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/add_dialog_login_title"
|
android:id="@+id/add_dialog_login_title"
|
||||||
|
@ -153,7 +158,7 @@
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/add_dialog_login_error_image"/>
|
/>
|
||||||
<TextView
|
<TextView
|
||||||
android:text="@string/add_dialog_login_description"
|
android:text="@string/add_dialog_login_description"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -177,22 +182,26 @@
|
||||||
android:maxLines="1" android:inputType="textPassword" app:layout_constraintStart_toStartOf="parent"
|
android:maxLines="1" android:inputType="textPassword" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_username"/>
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_username"/>
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp"
|
||||||
|
android:id="@+id/add_dialog_login_error_text_image"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="@+id/add_dialog_login_error_text" app:layout_constraintTop_toTopOf="@+id/add_dialog_login_error_text"/>
|
||||||
|
<TextView
|
||||||
|
android:text="Login failed. User not authorized."
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_error_text"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_password" android:paddingEnd="4dp" android:textColor="@color/primaryDangerButtonColor" app:layout_constraintStart_toEndOf="@id/add_dialog_login_error_text_image"/>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
style="?android:attr/progressBarStyle"
|
style="?android:attr/progressBarStyle"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
android:id="@+id/add_dialog_login_progress"
|
android:id="@+id/add_dialog_login_progress"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/add_dialog_login_error_image"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/add_dialog_login_description"
|
app:layout_constraintBottom_toTopOf="@+id/add_dialog_login_description"
|
||||||
android:indeterminate="true" android:layout_marginBottom="5dp"/>
|
android:indeterminate="true" android:layout_marginBottom="5dp"/>
|
||||||
<ImageView
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_error_black_24dp"
|
|
||||||
android:id="@+id/add_dialog_login_error_image"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/add_dialog_login_description"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/add_dialog_login_progress" android:layout_marginBottom="5dp"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/add_dialog_login_title"/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
<string name="add_dialog_instant_delivery">Instant delivery in doze mode</string>
|
<string name="add_dialog_instant_delivery">Instant delivery in doze mode</string>
|
||||||
<string name="add_dialog_instant_delivery_description">
|
<string name="add_dialog_instant_delivery_description">
|
||||||
Ensures that messages are immediately delivered, even if the device is inactive or in doze mode.
|
Ensures that messages are immediately delivered, even if the device is inactive or in doze mode.
|
||||||
This requires a foreground service and consumes a little more power.
|
This requires a foreground service.
|
||||||
</string>
|
</string>
|
||||||
<string name="add_dialog_button_cancel">Cancel</string>
|
<string name="add_dialog_button_cancel">Cancel</string>
|
||||||
<string name="add_dialog_button_subscribe">Subscribe</string>
|
<string name="add_dialog_button_subscribe">Subscribe</string>
|
||||||
|
@ -127,7 +127,9 @@
|
||||||
<string name="detail_test_message">This is a test notification from the Ntfy Android app. It has a priority of %1$d.
|
<string name="detail_test_message">This is a test notification from the Ntfy Android app. It has a priority of %1$d.
|
||||||
If you send another one, it may look different.
|
If you send another one, it may look different.
|
||||||
</string>
|
</string>
|
||||||
<string name="detail_test_message_error">Could not send test message: %1$s</string>
|
<string name="detail_test_message_error">Cannot send message: %1$s</string>
|
||||||
|
<string name="detail_test_message_error_unauthorized_anon">Cannot send message: Anonymous publishing not allowed</string>
|
||||||
|
<string name="detail_test_message_error_unauthorized_user">Cannot send message: User %1$s not authorized</string>
|
||||||
<string name="detail_copied_to_clipboard_message">Copied to clipboard</string>
|
<string name="detail_copied_to_clipboard_message">Copied to clipboard</string>
|
||||||
<string name="detail_instant_delivery_enabled">Instant delivery enabled</string>
|
<string name="detail_instant_delivery_enabled">Instant delivery enabled</string>
|
||||||
<string name="detail_instant_delivery_disabled">Instant delivery disabled</string>
|
<string name="detail_instant_delivery_disabled">Instant delivery disabled</string>
|
||||||
|
@ -178,13 +180,7 @@
|
||||||
|
|
||||||
<!-- Detail settings -->
|
<!-- Detail settings -->
|
||||||
<string name="detail_settings_title">Subscription settings</string>
|
<string name="detail_settings_title">Subscription settings</string>
|
||||||
<string name="detail_settings_auth_header">Login</string>
|
<!-- ... -->
|
||||||
<string name="detail_settings_auth_header_summary">For topics that require a login, you may pick the login user here. You can add/edit users in the main settings.</string>
|
|
||||||
<string name="detail_settings_auth_user_key">SubscriptionAuthUserKey</string>
|
|
||||||
<string name="detail_settings_auth_user_title">Login user</string>
|
|
||||||
<string name="detail_settings_auth_user_entry_anon">Anonymous login</string>
|
|
||||||
<string name="detail_settings_auth_user_summary_none">No user selected to log in to the topic</string>
|
|
||||||
<string name="detail_settings_auth_user_summary_user_x">User %1$s selected as a login user</string>
|
|
||||||
|
|
||||||
<!-- Notification dialog -->
|
<!-- Notification dialog -->
|
||||||
<string name="notification_dialog_title">Pause notifications</string>
|
<string name="notification_dialog_title">Pause notifications</string>
|
||||||
|
@ -278,17 +274,22 @@
|
||||||
<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_key">RecordLogs</string>
|
||||||
<string name="settings_advanced_record_logs_title">Record logs</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 2,000 log lines are stored.</string>
|
<string name="settings_advanced_record_logs_summary_enabled">Logs are currently being recorded to your device. Up to 1,000 log entries 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_summary_disabled">Enable log recording, so you can share the logs later. This is useful for diagnosing issues.</string>
|
||||||
<string name="settings_advanced_export_logs_key">ExportLogs</string>
|
<string name="settings_advanced_export_logs_key">ExportLogs</string>
|
||||||
<string name="settings_advanced_export_logs_title">Copy/upload logs</string>
|
<string name="settings_advanced_export_logs_title">Copy/upload logs</string>
|
||||||
<string name="settings_advanced_export_logs_summary">Copy logs to the clipboard, or upload to nopaste.net (owned by ntfy author). Hostnames and topics are scrubbed, notifications are not.</string>
|
<string name="settings_advanced_export_logs_summary">Copy logs to the clipboard, or upload to nopaste.net (owned by ntfy author). Hostnames and topics can be censored, notifications will never be.</string>
|
||||||
<string name="settings_advanced_export_logs_entry_copy">Copy to clipboard</string>
|
<string name="settings_advanced_export_logs_entry_copy_original">Copy to clipboard</string>
|
||||||
<string name="settings_advanced_export_logs_entry_upload">Upload to nopaste.net</string>
|
<string name="settings_advanced_export_logs_entry_copy_scrubbed">Copy to clipboard (censored)</string>
|
||||||
|
<string name="settings_advanced_export_logs_entry_upload_original">Upload & copy link</string>
|
||||||
|
<string name="settings_advanced_export_logs_entry_upload_scrubbed">Upload & copy link (censored)</string>
|
||||||
<string name="settings_advanced_export_logs_copied_logs">Logs copied to clipboard</string>
|
<string name="settings_advanced_export_logs_copied_logs">Logs copied to clipboard</string>
|
||||||
<string name="settings_advanced_export_logs_uploading">Uploading log …</string>
|
<string name="settings_advanced_export_logs_uploading">Uploading log …</string>
|
||||||
<string name="settings_advanced_export_logs_copied_url">URL copied to clipboard</string>
|
<string name="settings_advanced_export_logs_copied_url">Logs uploaded & URL copied</string>
|
||||||
<string name="settings_advanced_export_logs_error_uploading">Error uploading logs: %1$s</string>
|
<string name="settings_advanced_export_logs_error_uploading">Error uploading logs: %1$s</string>
|
||||||
|
<string name="settings_advanced_export_logs_scrub_dialog_text">The following topics/hostnames were replaced with fruit names, so you can share the log without worry:\n\n%1$s</string>
|
||||||
|
<string name="settings_advanced_export_logs_scrub_dialog_empty">No topics/hostnames were redacted, maybe you don\'t have any subscriptions?</string>
|
||||||
|
<string name="settings_advanced_export_logs_scrub_dialog_button_ok">Got it</string>
|
||||||
<string name="settings_advanced_clear_logs_key">ClearLogs</string>
|
<string name="settings_advanced_clear_logs_key">ClearLogs</string>
|
||||||
<string name="settings_advanced_clear_logs_title">Clear logs</string>
|
<string name="settings_advanced_clear_logs_title">Clear logs</string>
|
||||||
<string name="settings_advanced_clear_logs_summary">Delete previously recorded logs, and start over</string>
|
<string name="settings_advanced_clear_logs_summary">Delete previously recorded logs, and start over</string>
|
||||||
|
@ -300,10 +301,6 @@
|
||||||
<string name="settings_advanced_connection_protocol_summary_ws">Use WebSockets to connect to the server. This option is experimental. Let us know if it consumes less battery or is unstable.</string>
|
<string name="settings_advanced_connection_protocol_summary_ws">Use WebSockets to connect to the server. This option is experimental. Let us know if it consumes less battery or is unstable.</string>
|
||||||
<string name="settings_advanced_connection_protocol_entry_jsonhttp">JSON stream over HTTP</string>
|
<string name="settings_advanced_connection_protocol_entry_jsonhttp">JSON stream over HTTP</string>
|
||||||
<string name="settings_advanced_connection_protocol_entry_ws">WebSockets (experimental)</string>
|
<string name="settings_advanced_connection_protocol_entry_ws">WebSockets (experimental)</string>
|
||||||
<string name="settings_advanced_wakelock_key">WakelockEnabled</string>
|
|
||||||
<string name="settings_advanced_wakelock_title">Permanent wakelock</string>
|
|
||||||
<string name="settings_advanced_wakelock_summary_enabled">Prevents app from sleeping to ensure timely notification delivery. This consumes a lot of battery, but some devices require this.</string>
|
|
||||||
<string name="settings_advanced_wakelock_summary_disabled">Allows app to enter sleep mode. This may negatively impact notification delivery. It depends on the device.</string>
|
|
||||||
<string name="settings_about_header">About</string>
|
<string name="settings_about_header">About</string>
|
||||||
<string name="settings_about_version_key">Version</string>
|
<string name="settings_about_version_key">Version</string>
|
||||||
<string name="settings_about_version_title">Version</string>
|
<string name="settings_about_version_title">Version</string>
|
||||||
|
|
|
@ -62,12 +62,16 @@
|
||||||
<item>ws</item>
|
<item>ws</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_advanced_export_logs_entries">
|
<string-array name="settings_advanced_export_logs_entries">
|
||||||
<item>@string/settings_advanced_export_logs_entry_copy</item>
|
<item>@string/settings_advanced_export_logs_entry_copy_original</item>
|
||||||
<item>@string/settings_advanced_export_logs_entry_upload</item>
|
<item>@string/settings_advanced_export_logs_entry_copy_scrubbed</item>
|
||||||
|
<item>@string/settings_advanced_export_logs_entry_upload_original</item>
|
||||||
|
<item>@string/settings_advanced_export_logs_entry_upload_scrubbed</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_advanced_export_logs_values">
|
<string-array name="settings_advanced_export_logs_values">
|
||||||
<item>copy</item>
|
<item>copy_original</item>
|
||||||
<item>upload</item>
|
<item>copy_scrubbed</item>
|
||||||
|
<item>upload_original</item>
|
||||||
|
<item>upload_scrubbed</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_appearance_dark_mode_entries">
|
<string-array name="settings_appearance_dark_mode_entries">
|
||||||
<item>@string/settings_appearance_dark_mode_entry_system</item>
|
<item>@string/settings_appearance_dark_mode_entry_system</item>
|
||||||
|
|
|
@ -1,12 +1,3 @@
|
||||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
app:title="@string/detail_settings_title">
|
app:title="@string/detail_settings_title">
|
||||||
<PreferenceCategory
|
|
||||||
app:title="@string/detail_settings_auth_header"
|
|
||||||
app:summary="@string/detail_settings_auth_header_summary"
|
|
||||||
app:layout="@layout/preference_category_material_edited">
|
|
||||||
<ListPreference
|
|
||||||
app:key="@string/detail_settings_auth_user_key"
|
|
||||||
app:title="@string/detail_settings_auth_user_title"
|
|
||||||
app:summary="@string/detail_settings_auth_user_summary_none"/>
|
|
||||||
</PreferenceCategory>
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
|
@ -74,10 +74,6 @@
|
||||||
app:key="@string/settings_advanced_clear_logs_key"
|
app:key="@string/settings_advanced_clear_logs_key"
|
||||||
app:title="@string/settings_advanced_clear_logs_title"
|
app:title="@string/settings_advanced_clear_logs_title"
|
||||||
app:summary="@string/settings_advanced_clear_logs_summary"/>
|
app:summary="@string/settings_advanced_clear_logs_summary"/>
|
||||||
<SwitchPreference
|
|
||||||
app:key="@string/settings_advanced_wakelock_key"
|
|
||||||
app:title="@string/settings_advanced_wakelock_title"
|
|
||||||
app:enabled="true"/>
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory app:title="@string/settings_experimental_header">
|
<PreferenceCategory app:title="@string/settings_experimental_header">
|
||||||
<ListPreference
|
<ListPreference
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
|
Before Width: | Height: | Size: 266 B |
|
@ -1,3 +1,14 @@
|
||||||
|
Features:
|
||||||
|
* Support auth / access control (#19, thanks to @cmeis, @gedw99, @karmanyaahm,
|
||||||
|
@Mek101, @gc-ss, @julianfoad, @nmoseman, Jakob, PeterCxy, Techlosopher)
|
||||||
|
* Export/upload log now allows censored/uncensored logs (no ticket)
|
||||||
|
* Removed wake lock (except for notification dispatching, no ticket)
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
* Fix download issues on SDK 29 "Movement not allowed" (#116, thanks Jakob)
|
* Fix download issues on SDK 29 "Movement not allowed" (#116, thanks Jakob)
|
||||||
* Fix for Android 12 crashes (#124, thanks @eskilop)
|
* Fix for Android 12 crashes (#124, thanks @eskilop)
|
||||||
|
* Fix WebSocket retry logic bug with multiple servers (no ticket)
|
||||||
|
* Fix race in refresh logic leading to duplicate connections (no ticket)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
* Foundational work for per-subscription settings
|
||||||
|
|
Loading…
Reference in a new issue