Remove per-topic user association; this is so. much. better.
This commit is contained in:
parent
6593d60d43
commit
28bfd087c7
17 changed files with 139 additions and 269 deletions
|
@ -2,11 +2,11 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"identityHash": "f8551fac095d795a83ba7a95277e3f0f",
|
"identityHash": "eda2cb9740c4542f24462779eb6ff81d",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "Subscription",
|
"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, `authUserId` INTEGER, `upAppId` TEXT, `upConnectorToken` TEXT, PRIMARY KEY(`id`))",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
|
@ -38,12 +38,6 @@
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldPath": "authUserId",
|
|
||||||
"columnName": "authUserId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldPath": "upAppId",
|
"fieldPath": "upAppId",
|
||||||
"columnName": "upAppId",
|
"columnName": "upAppId",
|
||||||
|
@ -206,14 +200,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "User",
|
"tableName": "User",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `baseUrl` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL)",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldPath": "baseUrl",
|
"fieldPath": "baseUrl",
|
||||||
"columnName": "baseUrl",
|
"columnName": "baseUrl",
|
||||||
|
@ -235,9 +223,9 @@
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"id"
|
"baseUrl"
|
||||||
],
|
],
|
||||||
"autoGenerate": true
|
"autoGenerate": false
|
||||||
},
|
},
|
||||||
"indices": [],
|
"indices": [],
|
||||||
"foreignKeys": []
|
"foreignKeys": []
|
||||||
|
@ -296,7 +284,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, 'f8551fac095d795a83ba7a95277e3f0f')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eda2cb9740c4542f24462779eb6ff81d')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,7 +14,6 @@ data class Subscription(
|
||||||
@ColumnInfo(name = "topic") val topic: String,
|
@ColumnInfo(name = "topic") val topic: String,
|
||||||
@ColumnInfo(name = "instant") val instant: Boolean,
|
@ColumnInfo(name = "instant") val instant: Boolean,
|
||||||
@ColumnInfo(name = "mutedUntil") val mutedUntil: Long, // TODO notificationSound, notificationSchedule
|
@ColumnInfo(name = "mutedUntil") val mutedUntil: Long, // TODO notificationSound, notificationSchedule
|
||||||
@ColumnInfo(name = "authUserId") val authUserId: Long?,
|
|
||||||
@ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name
|
@ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name
|
||||||
@ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, // UnifiedPush connector token
|
@ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, // UnifiedPush connector token
|
||||||
// TODO autoDownloadAttachments, minPriority
|
// TODO autoDownloadAttachments, minPriority
|
||||||
|
@ -23,8 +22,8 @@ data class Subscription(
|
||||||
@Ignore val lastActive: Long = 0, // Unix timestamp
|
@Ignore val lastActive: Long = 0, // Unix timestamp
|
||||||
@Ignore val state: ConnectionState = ConnectionState.NOT_APPLICABLE
|
@Ignore val state: ConnectionState = ConnectionState.NOT_APPLICABLE
|
||||||
) {
|
) {
|
||||||
constructor(id: Long, baseUrl: String, topic: String, instant: Boolean, mutedUntil: Long, authUserId: Long?, upAppId: String, upConnectorToken: String) :
|
constructor(id: Long, baseUrl: String, topic: String, instant: Boolean, mutedUntil: Long, upAppId: String, upConnectorToken: String) :
|
||||||
this(id, baseUrl, topic, instant, mutedUntil, authUserId, upAppId, upConnectorToken, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
|
this(id, baseUrl, topic, instant, mutedUntil, upAppId, upConnectorToken, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ConnectionState {
|
enum class ConnectionState {
|
||||||
|
@ -37,7 +36,6 @@ data class SubscriptionWithMetadata(
|
||||||
val topic: String,
|
val topic: String,
|
||||||
val instant: Boolean,
|
val instant: Boolean,
|
||||||
val mutedUntil: Long,
|
val mutedUntil: Long,
|
||||||
val authUserId: Long?,
|
|
||||||
val upAppId: String?,
|
val upAppId: String?,
|
||||||
val upConnectorToken: String?,
|
val upConnectorToken: String?,
|
||||||
val totalCount: Int,
|
val totalCount: Int,
|
||||||
|
@ -82,8 +80,7 @@ const val PROGRESS_DONE = 100
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
data class User(
|
data class User(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Long,
|
@PrimaryKey @ColumnInfo(name = "baseUrl") val baseUrl: String,
|
||||||
@ColumnInfo(name = "baseUrl") val baseUrl: String,
|
|
||||||
@ColumnInfo(name = "username") val username: String,
|
@ColumnInfo(name = "username") val username: String,
|
||||||
@ColumnInfo(name = "password") val password: String
|
@ColumnInfo(name = "password") val password: String
|
||||||
) {
|
) {
|
||||||
|
@ -194,7 +191,7 @@ abstract class Database : RoomDatabase() {
|
||||||
interface SubscriptionDao {
|
interface SubscriptionDao {
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT
|
SELECT
|
||||||
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.authUserId, 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
|
||||||
|
@ -207,7 +204,7 @@ interface SubscriptionDao {
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT
|
SELECT
|
||||||
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.authUserId, 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
|
||||||
|
@ -220,7 +217,7 @@ interface SubscriptionDao {
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT
|
SELECT
|
||||||
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.authUserId, 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
|
||||||
|
@ -233,7 +230,7 @@ interface SubscriptionDao {
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT
|
SELECT
|
||||||
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.authUserId, 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
|
||||||
|
@ -246,7 +243,7 @@ interface SubscriptionDao {
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT
|
SELECT
|
||||||
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.authUserId, 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
|
||||||
|
@ -263,14 +260,8 @@ interface SubscriptionDao {
|
||||||
@Update
|
@Update
|
||||||
fun update(subscription: Subscription)
|
fun update(subscription: Subscription)
|
||||||
|
|
||||||
@Query("UPDATE subscription SET authUserId = :authUserId WHERE id = :subscriptionId")
|
|
||||||
fun updateSubscriptionAuthUserId(subscriptionId: Long, authUserId: Long?)
|
|
||||||
|
|
||||||
@Query("DELETE FROM subscription WHERE id = :subscriptionId")
|
@Query("DELETE FROM subscription WHERE id = :subscriptionId")
|
||||||
fun remove(subscriptionId: Long)
|
fun remove(subscriptionId: Long)
|
||||||
|
|
||||||
@Query("UPDATE subscription SET authUserId = null WHERE authUserId = :authUserId")
|
|
||||||
fun removeAuthUserFromSubscriptions(authUserId: Long)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
@ -311,14 +302,14 @@ interface UserDao {
|
||||||
@Query("SELECT * FROM user ORDER BY username")
|
@Query("SELECT * FROM user ORDER BY username")
|
||||||
suspend fun list(): List<User>
|
suspend fun list(): List<User>
|
||||||
|
|
||||||
@Query("SELECT * FROM user WHERE id = :id")
|
@Query("SELECT * FROM user WHERE baseUrl = :baseUrl")
|
||||||
suspend fun get(id: Long): User
|
suspend fun get(baseUrl: String): User?
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
suspend fun update(user: User)
|
suspend fun update(user: User)
|
||||||
|
|
||||||
@Query("DELETE FROM user WHERE id = :id")
|
@Query("DELETE FROM user WHERE baseUrl = :baseUrl")
|
||||||
suspend fun delete(id: Long)
|
suspend fun delete(baseUrl: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
|
|
@ -78,20 +78,12 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
||||||
subscriptionDao.update(subscription)
|
subscriptionDao.update(subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSubscriptionAuthUserId(subscriptionId: Long, authUserId: Long?) {
|
|
||||||
subscriptionDao.updateSubscriptionAuthUserId(subscriptionId, authUserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("RedundantSuspendModifier")
|
@Suppress("RedundantSuspendModifier")
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
suspend fun removeSubscription(subscriptionId: Long) {
|
suspend fun removeSubscription(subscriptionId: Long) {
|
||||||
subscriptionDao.remove(subscriptionId)
|
subscriptionDao.remove(subscriptionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAuthUserFromSubscriptions(authUserId: Long) {
|
|
||||||
subscriptionDao.removeAuthUserFromSubscriptions(authUserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {
|
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {
|
||||||
return notificationDao.listFlow(subscriptionId).asLiveData()
|
return notificationDao.listFlow(subscriptionId).asLiveData()
|
||||||
}
|
}
|
||||||
|
@ -152,12 +144,12 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
||||||
userDao.update(user)
|
userDao.update(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUser(userId: Long): User {
|
suspend fun getUser(baseUrl: String): User? {
|
||||||
return userDao.get(userId)
|
return userDao.get(baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteUser(userId: Long) {
|
suspend fun deleteUser(baseUrl: String) {
|
||||||
userDao.delete(userId)
|
userDao.delete(baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPollWorkerVersion(): Int {
|
fun getPollWorkerVersion(): Int {
|
||||||
|
@ -346,7 +338,6 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
||||||
topic = s.topic,
|
topic = s.topic,
|
||||||
instant = s.instant,
|
instant = s.instant,
|
||||||
mutedUntil = s.mutedUntil,
|
mutedUntil = s.mutedUntil,
|
||||||
authUserId = s.authUserId,
|
|
||||||
upAppId = s.upAppId,
|
upAppId = s.upAppId,
|
||||||
upConnectorToken = s.upConnectorToken,
|
upConnectorToken = s.upConnectorToken,
|
||||||
totalCount = s.totalCount,
|
totalCount = s.totalCount,
|
||||||
|
@ -367,7 +358,6 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
||||||
topic = s.topic,
|
topic = s.topic,
|
||||||
instant = s.instant,
|
instant = s.instant,
|
||||||
mutedUntil = s.mutedUntil,
|
mutedUntil = s.mutedUntil,
|
||||||
authUserId = s.authUserId,
|
|
||||||
upAppId = s.upAppId,
|
upAppId = s.upAppId,
|
||||||
upConnectorToken = s.upConnectorToken,
|
upConnectorToken = s.upConnectorToken,
|
||||||
totalCount = s.totalCount,
|
totalCount = s.totalCount,
|
||||||
|
|
|
@ -12,7 +12,6 @@ import io.heckel.ntfy.util.topicUrlJsonPoll
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.nio.charset.StandardCharsets.UTF_8
|
import java.nio.charset.StandardCharsets.UTF_8
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
@ -134,30 +133,26 @@ class ApiService {
|
||||||
return call
|
return call
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkAnonTopicRead(baseUrl: String, topic: String): Boolean {
|
fun authTopicRead(baseUrl: String, topic: String, user: User?): Boolean {
|
||||||
return checkTopicRead(baseUrl, topic, creds = null)
|
if (user == null) {
|
||||||
}
|
Log.d(TAG, "Checking anonymous read against ${topicUrl(baseUrl, topic)}")
|
||||||
|
} else {
|
||||||
fun checkUserTopicRead(baseUrl: String, topic: String, username: String, password: String): Boolean {
|
Log.d(TAG, "Checking read access for user ${user.username} against ${topicUrl(baseUrl, topic)}")
|
||||||
Log.d(TAG, "Authorizing user $username against ${topicUrl(baseUrl, topic)}")
|
}
|
||||||
return checkTopicRead(baseUrl, topic, creds = Credentials.basic(username, password, UTF_8))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkTopicRead(baseUrl: String, topic: String, creds: String?): Boolean {
|
|
||||||
val url = topicUrlAuth(baseUrl, topic)
|
val url = topicUrlAuth(baseUrl, topic)
|
||||||
val builder = Request.Builder()
|
val builder = Request.Builder()
|
||||||
.get()
|
.get()
|
||||||
.url(url)
|
.url(url)
|
||||||
.addHeader("User-Agent", USER_AGENT)
|
.addHeader("User-Agent", USER_AGENT)
|
||||||
if (creds != null) {
|
if (user != null) {
|
||||||
builder.addHeader("Authorization", creds)
|
builder.addHeader("Authorization", Credentials.basic(user.username, user.password, UTF_8))
|
||||||
}
|
}
|
||||||
val request = builder.build()
|
val request = builder.build()
|
||||||
client.newCall(request).execute().use { response ->
|
client.newCall(request).execute().use { response ->
|
||||||
if (creds == null) {
|
return if (user == null) {
|
||||||
return response.isSuccessful || response.code == 404 // Treat 404 as success (old server; to be removed in future versions)
|
response.isSuccessful || response.code == 404 // Treat 404 as success (old server; to be removed in future versions)
|
||||||
} else {
|
} else {
|
||||||
return response.isSuccessful
|
response.isSuccessful
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,5 @@ interface Connection {
|
||||||
|
|
||||||
data class ConnectionId(
|
data class ConnectionId(
|
||||||
val baseUrl: String,
|
val baseUrl: String,
|
||||||
val authUserId: Long?,
|
|
||||||
val topicsToSubscriptionIds: Map<String, Long>
|
val topicsToSubscriptionIds: Map<String, Long>
|
||||||
)
|
)
|
||||||
|
|
|
@ -155,7 +155,7 @@ class SubscriberService : Service() {
|
||||||
.filter { s -> s.instant }
|
.filter { s -> s.instant }
|
||||||
val activeConnectionIds = connections.keys().toList().toSet()
|
val activeConnectionIds = connections.keys().toList().toSet()
|
||||||
val desiredConnectionIds = instantSubscriptions // Set<ConnectionId>
|
val desiredConnectionIds = instantSubscriptions // Set<ConnectionId>
|
||||||
.groupBy { s -> ConnectionId(s.baseUrl, s.authUserId, emptyMap()) }
|
.groupBy { s -> ConnectionId(s.baseUrl, emptyMap()) }
|
||||||
.map { entry -> entry.key.copy(topicsToSubscriptionIds = entry.value.associate { s -> s.topic to s.id }) }
|
.map { entry -> entry.key.copy(topicsToSubscriptionIds = entry.value.associate { s -> s.topic to s.id }) }
|
||||||
.toSet()
|
.toSet()
|
||||||
val newConnectionIds = desiredConnectionIds subtract activeConnectionIds
|
val newConnectionIds = desiredConnectionIds subtract activeConnectionIds
|
||||||
|
@ -183,11 +183,7 @@ class SubscriberService : Service() {
|
||||||
|
|
||||||
val since = System.currentTimeMillis()/1000
|
val since = System.currentTimeMillis()/1000
|
||||||
val serviceActive = { -> isServiceStarted }
|
val serviceActive = { -> isServiceStarted }
|
||||||
val user = if (connectionId.authUserId != null) {
|
val user = repository.getUser(connectionId.baseUrl)
|
||||||
repository.getUser(connectionId.authUserId)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) {
|
val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) {
|
||||||
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
|
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
|
||||||
WsConnection(connectionId, repository, user, since, ::onStateChanged, ::onNotificationReceived, alarmManager)
|
WsConnection(connectionId, repository, user, since, ::onStateChanged, ::onNotificationReceived, alarmManager)
|
||||||
|
|
|
@ -24,7 +24,7 @@ class SubscriberServiceManager(private val context: Context) {
|
||||||
workManager.enqueue(startServiceRequest)
|
workManager.enqueue(startServiceRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun restart() {
|
||||||
Intent(context, SubscriberService::class.java).also { intent ->
|
Intent(context, SubscriberService::class.java).also { intent ->
|
||||||
context.stopService(intent) // Service will auto-restart
|
context.stopService(intent) // Service will auto-restart
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.util.topicUrl
|
import io.heckel.ntfy.util.topicUrl
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class AddFragment : DialogFragment() {
|
class AddFragment : DialogFragment() {
|
||||||
private val api = ApiService()
|
private val api = ApiService()
|
||||||
|
@ -51,7 +50,6 @@ class AddFragment : DialogFragment() {
|
||||||
|
|
||||||
// Login page
|
// Login page
|
||||||
private lateinit var users: List<User>
|
private lateinit var users: List<User>
|
||||||
private lateinit var loginUsersSpinner: Spinner
|
|
||||||
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
|
||||||
|
@ -60,7 +58,7 @@ class AddFragment : DialogFragment() {
|
||||||
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
|
||||||
|
|
||||||
interface SubscribeListener {
|
interface SubscribeListener {
|
||||||
fun onSubscribe(topic: String, baseUrl: String, instant: Boolean, authUserId: Long?)
|
fun onSubscribe(topic: String, baseUrl: String, instant: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
|
@ -98,7 +96,6 @@ class AddFragment : DialogFragment() {
|
||||||
subscribeErrorImage = view.findViewById(R.id.add_dialog_error_image)
|
subscribeErrorImage = view.findViewById(R.id.add_dialog_error_image)
|
||||||
|
|
||||||
// Fields for "login page"
|
// Fields for "login page"
|
||||||
loginUsersSpinner = view.findViewById(R.id.add_dialog_login_users_spinner)
|
|
||||||
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)
|
||||||
|
@ -178,31 +175,6 @@ class AddFragment : DialogFragment() {
|
||||||
// Show/hide based on flavor
|
// Show/hide based on flavor
|
||||||
subscribeInstantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
subscribeInstantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
// Show/hide drop-down and username/password fields
|
|
||||||
loginUsersSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
||||||
if (position == 0) {
|
|
||||||
loginUsernameText.visibility = View.VISIBLE
|
|
||||||
loginUsernameText.isEnabled = true
|
|
||||||
loginPasswordText.visibility = View.VISIBLE
|
|
||||||
loginPasswordText.isEnabled = true
|
|
||||||
if (loginUsernameText.requestFocus()) {
|
|
||||||
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
||||||
imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
loginUsernameText.visibility = View.GONE
|
|
||||||
loginUsernameText.isEnabled = false
|
|
||||||
loginPasswordText.visibility = View.GONE
|
|
||||||
loginPasswordText.isEnabled = false
|
|
||||||
}
|
|
||||||
validateInputLoginView()
|
|
||||||
}
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
|
||||||
// This should not happen, ha!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Username/password validation on type
|
// Username/password validation on type
|
||||||
val textWatcher = object : TextWatcher {
|
val textWatcher = object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
@ -288,97 +260,94 @@ class AddFragment : DialogFragment() {
|
||||||
val topic = subscribeTopicText.text.toString()
|
val topic = subscribeTopicText.text.toString()
|
||||||
val baseUrl = getBaseUrl()
|
val baseUrl = getBaseUrl()
|
||||||
if (subscribeView.visibility == View.VISIBLE) {
|
if (subscribeView.visibility == View.VISIBLE) {
|
||||||
checkAnonReadAndMaybeShowLogin(baseUrl, topic)
|
checkReadAndMaybeShowLogin(baseUrl, topic)
|
||||||
} else if (loginView.visibility == View.VISIBLE) {
|
} else if (loginView.visibility == View.VISIBLE) {
|
||||||
loginAndMaybeDismiss(baseUrl, topic)
|
loginAndMaybeDismiss(baseUrl, topic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkAnonReadAndMaybeShowLogin(baseUrl: String, topic: String) {
|
private fun checkReadAndMaybeShowLogin(baseUrl: String, topic: String) {
|
||||||
subscribeProgress.visibility = View.VISIBLE
|
subscribeProgress.visibility = View.VISIBLE
|
||||||
subscribeErrorImage.visibility = View.GONE
|
subscribeErrorImage.visibility = View.GONE
|
||||||
enableSubscribeView(false)
|
enableSubscribeView(false)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
Log.d(TAG, "Checking anonymous read access to topic ${topicUrl(baseUrl, topic)}")
|
|
||||||
try {
|
try {
|
||||||
val authorized = api.checkAnonTopicRead(baseUrl, topic)
|
val user = repository.getUser(baseUrl) // May be null
|
||||||
|
val authorized = api.authTopicRead(baseUrl, topic, user)
|
||||||
if (authorized) {
|
if (authorized) {
|
||||||
Log.d(TAG, "Anonymous access granted to topic ${topicUrl(baseUrl, topic)}")
|
Log.d(TAG, "Access granted to topic ${topicUrl(baseUrl, topic)}")
|
||||||
dismiss(authUserId = null)
|
dismissDialog()
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Anonymous access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog")
|
if (user != null) {
|
||||||
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, but user already exists")
|
||||||
activity.runOnUiThread {
|
showToastAndReenableSubscribeView(getString(R.string.add_dialog_login_error_not_authorized))
|
||||||
showLoginView(activity, baseUrl)
|
} else {
|
||||||
|
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog")
|
||||||
|
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
||||||
|
activity.runOnUiThread {
|
||||||
|
showLoginView(activity, baseUrl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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)
|
||||||
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
showToastAndReenableSubscribeView(e.message)
|
||||||
activity.runOnUiThread {
|
|
||||||
subscribeProgress.visibility = View.GONE
|
|
||||||
subscribeErrorImage.visibility = View.VISIBLE
|
|
||||||
enableSubscribeView(true)
|
|
||||||
Toast
|
|
||||||
.makeText(context, getString(R.string.add_dialog_error_connection_failed, e.message), Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showToastAndReenableSubscribeView(message: String?) {
|
||||||
|
val activity = activity ?: return // We may have pressed "Cancel"
|
||||||
|
activity.runOnUiThread {
|
||||||
|
subscribeProgress.visibility = View.GONE
|
||||||
|
subscribeErrorImage.visibility = View.VISIBLE
|
||||||
|
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
|
loginErrorImage.visibility = View.GONE
|
||||||
enableLoginView(false)
|
enableLoginView(false)
|
||||||
val existingUser = loginUsersSpinner.selectedItem != null && loginUsersSpinner.selectedItem is User && loginUsersSpinner.selectedItemPosition > 0
|
val user = User(
|
||||||
val user = if (existingUser) {
|
baseUrl = baseUrl,
|
||||||
loginUsersSpinner.selectedItem as User
|
username = loginUsernameText.text.toString(),
|
||||||
} else {
|
password = loginPasswordText.text.toString()
|
||||||
User(
|
)
|
||||||
id = Random.nextLong(),
|
|
||||||
baseUrl = baseUrl,
|
|
||||||
username = loginUsernameText.text.toString(),
|
|
||||||
password = loginPasswordText.text.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
Log.d(TAG, "Checking read access for user ${user.username} to topic ${topicUrl(baseUrl, topic)}")
|
Log.d(TAG, "Checking read access for user ${user.username} to topic ${topicUrl(baseUrl, topic)}")
|
||||||
try {
|
try {
|
||||||
val authorized = api.checkUserTopicRead(baseUrl, topic, user.username, user.password)
|
val authorized = api.authTopicRead(baseUrl, topic, user)
|
||||||
if (authorized) {
|
if (authorized) {
|
||||||
Log.d(TAG, "Access granted for user ${user.username} to topic ${topicUrl(baseUrl, topic)}")
|
Log.d(TAG, "Access granted for user ${user.username} to topic ${topicUrl(baseUrl, topic)}, adding to database")
|
||||||
if (!existingUser) {
|
repository.addUser(user)
|
||||||
Log.d(TAG, "Adding new user ${user.username} to database")
|
dismissDialog()
|
||||||
repository.addUser(user)
|
|
||||||
}
|
|
||||||
dismiss(authUserId = user.id)
|
|
||||||
} 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)}")
|
||||||
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
showToastAndReenableLoginView(getString(R.string.add_dialog_login_error_not_authorized))
|
||||||
activity.runOnUiThread {
|
|
||||||
loginProgress.visibility = View.GONE
|
|
||||||
loginErrorImage.visibility = View.VISIBLE
|
|
||||||
enableLoginView(true)
|
|
||||||
Toast
|
|
||||||
.makeText(context, getString(R.string.add_dialog_login_error_not_authorized), Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
Log.w(TAG, "Connection to topic failed during login: ${e.message}", e)
|
||||||
activity.runOnUiThread {
|
showToastAndReenableLoginView(e.message)
|
||||||
loginProgress.visibility = View.GONE
|
|
||||||
loginErrorImage.visibility = View.VISIBLE
|
|
||||||
enableLoginView(true)
|
|
||||||
Toast
|
|
||||||
.makeText(context, getString(R.string.add_dialog_error_connection_failed, e.message), Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showToastAndReenableLoginView(message: String?) {
|
||||||
|
val activity = activity ?: return // We may have pressed "Cancel"
|
||||||
|
activity.runOnUiThread {
|
||||||
|
loginProgress.visibility = View.GONE
|
||||||
|
loginErrorImage.visibility = View.VISIBLE
|
||||||
|
enableLoginView(true)
|
||||||
|
Toast
|
||||||
|
.makeText(context, message, Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun negativeButtonClick() {
|
private fun negativeButtonClick() {
|
||||||
if (subscribeView.visibility == View.VISIBLE) {
|
if (subscribeView.visibility == View.VISIBLE) {
|
||||||
dialog?.cancel()
|
dialog?.cancel()
|
||||||
|
@ -420,7 +389,7 @@ class AddFragment : DialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dismiss(authUserId: Long?) {
|
private fun dismissDialog() {
|
||||||
Log.d(TAG, "Closing dialog and calling onSubscribe handler")
|
Log.d(TAG, "Closing dialog and calling onSubscribe handler")
|
||||||
val activity = activity?: return // We may have pressed "Cancel"
|
val activity = activity?: return // We may have pressed "Cancel"
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
|
@ -431,7 +400,7 @@ class AddFragment : DialogFragment() {
|
||||||
} else {
|
} else {
|
||||||
subscribeInstantDeliveryCheckbox.isChecked
|
subscribeInstantDeliveryCheckbox.isChecked
|
||||||
}
|
}
|
||||||
subscribeListener.onSubscribe(topic, baseUrl, instant, authUserId = authUserId)
|
subscribeListener.onSubscribe(topic, baseUrl, instant)
|
||||||
dialog?.dismiss()
|
dialog?.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,21 +432,9 @@ class AddFragment : DialogFragment() {
|
||||||
negativeButton.text = getString(R.string.add_dialog_button_back)
|
negativeButton.text = getString(R.string.add_dialog_button_back)
|
||||||
subscribeView.visibility = View.GONE
|
subscribeView.visibility = View.GONE
|
||||||
loginView.visibility = View.VISIBLE
|
loginView.visibility = View.VISIBLE
|
||||||
|
if (loginUsernameText.requestFocus()) {
|
||||||
// Show/hide dropdown
|
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
val relevantUsers = users.filter { it.baseUrl == baseUrl }
|
imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT)
|
||||||
if (relevantUsers.isEmpty()) {
|
|
||||||
loginUsersSpinner.visibility = View.GONE
|
|
||||||
loginUsersSpinner.adapter = ArrayAdapter(activity, R.layout.fragment_add_dialog_dropdown_item, emptyArray<User>())
|
|
||||||
if (loginUsernameText.requestFocus()) {
|
|
||||||
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
||||||
imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val spinnerEntries = relevantUsers.toMutableList()
|
|
||||||
spinnerEntries.add(0, User(0, "", getString(R.string.add_dialog_login_new_user), ""))
|
|
||||||
loginUsersSpinner.adapter = ArrayAdapter(activity, R.layout.fragment_add_dialog_dropdown_item, spinnerEntries)
|
|
||||||
loginUsersSpinner.setSelection(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,7 +455,6 @@ class AddFragment : DialogFragment() {
|
||||||
private fun enableLoginView(enable: Boolean) {
|
private fun enableLoginView(enable: Boolean) {
|
||||||
loginUsernameText.isEnabled = enable
|
loginUsernameText.isEnabled = enable
|
||||||
loginPasswordText.isEnabled = enable
|
loginPasswordText.isEnabled = enable
|
||||||
loginUsersSpinner.isEnabled = enable
|
|
||||||
positiveButton.isEnabled = enable
|
positiveButton.isEnabled = enable
|
||||||
if (enable && loginUsernameText.requestFocus()) {
|
if (enable && loginUsernameText.requestFocus()) {
|
||||||
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
|
@ -509,7 +465,6 @@ class AddFragment : DialogFragment() {
|
||||||
private fun resetLoginView() {
|
private fun resetLoginView() {
|
||||||
loginProgress.visibility = View.GONE
|
loginProgress.visibility = View.GONE
|
||||||
loginErrorImage.visibility = View.GONE
|
loginErrorImage.visibility = View.GONE
|
||||||
loginUsersSpinner.visibility = View.VISIBLE
|
|
||||||
loginUsernameText.visibility = View.VISIBLE
|
loginUsernameText.visibility = View.VISIBLE
|
||||||
loginUsernameText.text?.clear()
|
loginUsernameText.text?.clear()
|
||||||
loginPasswordText.visibility = View.VISIBLE
|
loginPasswordText.visibility = View.VISIBLE
|
||||||
|
|
|
@ -252,10 +252,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
onClearClick()
|
onClearClick()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.detail_menu_settings -> {
|
/*R.id.detail_menu_settings -> {
|
||||||
onSettingsClick()
|
onSettingsClick()
|
||||||
true
|
true
|
||||||
}
|
}*/
|
||||||
R.id.detail_menu_unsubscribe -> {
|
R.id.detail_menu_unsubscribe -> {
|
||||||
onDeleteClick()
|
onDeleteClick()
|
||||||
true
|
true
|
||||||
|
|
|
@ -3,14 +3,10 @@ package io.heckel.ntfy.ui
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceDataStore
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.Subscription
|
import io.heckel.ntfy.db.Subscription
|
||||||
import io.heckel.ntfy.db.User
|
|
||||||
import io.heckel.ntfy.log.Log
|
import io.heckel.ntfy.log.Log
|
||||||
import io.heckel.ntfy.service.SubscriberServiceManager
|
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -72,53 +68,14 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
val subscriptionId = arguments?.getLong(DetailActivity.EXTRA_SUBSCRIPTION_ID) ?: return
|
val subscriptionId = arguments?.getLong(DetailActivity.EXTRA_SUBSCRIPTION_ID) ?: return
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val subscription = repository.getSubscription(subscriptionId) ?: return@launch
|
val subscription = repository.getSubscription(subscriptionId) ?: return@launch
|
||||||
val users = repository.getUsers().filter { it.baseUrl == subscription.baseUrl }
|
|
||||||
val authUser = users.firstOrNull { it.id == subscription.authUserId }
|
|
||||||
Log.d(TAG, "subscription: $subscription")
|
|
||||||
activity?.runOnUiThread {
|
activity?.runOnUiThread {
|
||||||
loadView(subscription, authUser, users)
|
loadView(subscription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadView(subscription: Subscription, authUser: User?, users: List<User>) {
|
private fun loadView(subscription: Subscription) {
|
||||||
// Login user
|
// ...
|
||||||
val anonUser = User(0, "", getString(R.string.detail_settings_auth_user_entry_anon), "")
|
|
||||||
val usersWithAnon = users.toMutableList()
|
|
||||||
usersWithAnon.add(0, anonUser)
|
|
||||||
val authUserPrefId = getString(R.string.detail_settings_auth_user_key)
|
|
||||||
val authUserPref: ListPreference? = findPreference(authUserPrefId)
|
|
||||||
authUserPref?.entries = usersWithAnon.map { it.username }.toTypedArray()
|
|
||||||
authUserPref?.entryValues = usersWithAnon.map { it.id.toString() }.toTypedArray()
|
|
||||||
authUserPref?.value = authUser?.id?.toString() ?: anonUser.id.toString()
|
|
||||||
Log.d(TAG, "--> ${authUser?.id?.toString() ?: anonUser.id.toString()}")
|
|
||||||
authUserPref?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
|
|
||||||
when (pref.value) {
|
|
||||||
anonUser.id.toString() -> getString(R.string.detail_settings_auth_user_summary_none)
|
|
||||||
else -> {
|
|
||||||
val username = users.firstOrNull { it.id.toString() == pref.value } ?: "?"
|
|
||||||
getString(R.string.detail_settings_auth_user_summary_user_x, username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
authUserPref?.preferenceDataStore = object : PreferenceDataStore() {
|
|
||||||
override fun putString(key: String?, value: String?) {
|
|
||||||
val newAuthUserId = when (value) {
|
|
||||||
anonUser.id.toString() -> null
|
|
||||||
else -> value?.toLongOrNull()
|
|
||||||
}
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
Log.d(TAG, "Updating subscription ${subscription.id} with new auth user ID $newAuthUserId")
|
|
||||||
repository.updateSubscriptionAuthUserId(subscription.id, newAuthUserId)
|
|
||||||
Log.d(TAG, "after save: ${repository.getSubscription(subscription.id)}")
|
|
||||||
serviceManager.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun getString(key: String?, defValue: String?): String? {
|
|
||||||
Log.d(TAG, "getstring called $key $defValue -> ${authUser?.id?.toString() ?: anonUser.id.toString()}")
|
|
||||||
return authUser?.id?.toString() ?: anonUser.id.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -348,7 +348,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
newFragment.show(supportFragmentManager, AddFragment.TAG)
|
newFragment.show(supportFragmentManager, AddFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSubscribe(topic: String, baseUrl: String, instant: Boolean, authUserId: Long?) {
|
override fun onSubscribe(topic: String, baseUrl: String, instant: Boolean) {
|
||||||
Log.d(TAG, "Adding subscription ${topicShortUrl(baseUrl, topic)}")
|
Log.d(TAG, "Adding subscription ${topicShortUrl(baseUrl, topic)}")
|
||||||
|
|
||||||
// Add subscription to database
|
// Add subscription to database
|
||||||
|
@ -358,7 +358,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
topic = topic,
|
topic = topic,
|
||||||
instant = instant,
|
instant = instant,
|
||||||
mutedUntil = 0,
|
mutedUntil = 0,
|
||||||
authUserId = authUserId,
|
|
||||||
upAppId = null,
|
upAppId = null,
|
||||||
upConnectorToken = null,
|
upConnectorToken = null,
|
||||||
totalCount = 0,
|
totalCount = 0,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.Manifest
|
||||||
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.Intent
|
|
||||||
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
|
||||||
|
@ -25,7 +24,6 @@ import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.User
|
import io.heckel.ntfy.db.User
|
||||||
import io.heckel.ntfy.log.Log
|
import io.heckel.ntfy.log.Log
|
||||||
import io.heckel.ntfy.service.SubscriberService
|
|
||||||
import io.heckel.ntfy.service.SubscriberServiceManager
|
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||||
import io.heckel.ntfy.util.formatBytes
|
import io.heckel.ntfy.util.formatBytes
|
||||||
import io.heckel.ntfy.util.formatDateShort
|
import io.heckel.ntfy.util.formatDateShort
|
||||||
|
@ -439,7 +437,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restartService() {
|
private fun restartService() {
|
||||||
serviceManager.stop() // Service will auto-restart
|
serviceManager.restart() // Service will auto-restart
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyLogsToClipboard() {
|
private fun copyLogsToClipboard() {
|
||||||
|
@ -543,12 +541,12 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
fun reload() {
|
fun reload() {
|
||||||
preferenceScreen.removeAll()
|
preferenceScreen.removeAll()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val userIdsWithTopics = repository.getSubscriptions()
|
val baseUrlsWithTopics = repository.getSubscriptions()
|
||||||
.groupBy { it.authUserId }
|
.groupBy { it.baseUrl }
|
||||||
.mapValues { e -> e.value.map { it.topic } }
|
.mapValues { e -> e.value.map { it.topic } }
|
||||||
val usersByBaseUrl = repository.getUsers()
|
val usersByBaseUrl = repository.getUsers()
|
||||||
.map { user ->
|
.map { user ->
|
||||||
val topics = userIdsWithTopics[user.id] ?: emptyList()
|
val topics = baseUrlsWithTopics[user.baseUrl] ?: emptyList()
|
||||||
UserWithMetadata(user, topics)
|
UserWithMetadata(user, topics)
|
||||||
}
|
}
|
||||||
.groupBy { it.user.baseUrl }
|
.groupBy { it.user.baseUrl }
|
||||||
|
@ -559,6 +557,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addUserPreferences(usersByBaseUrl: Map<String, List<UserWithMetadata>>) {
|
private fun addUserPreferences(usersByBaseUrl: Map<String, List<UserWithMetadata>>) {
|
||||||
|
val baseUrlsInUse = ArrayList(usersByBaseUrl.keys)
|
||||||
usersByBaseUrl.forEach { entry ->
|
usersByBaseUrl.forEach { entry ->
|
||||||
val baseUrl = entry.key
|
val baseUrl = entry.key
|
||||||
val users = entry.value
|
val users = entry.value
|
||||||
|
@ -580,7 +579,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
preference.onPreferenceClickListener = OnPreferenceClickListener { _ ->
|
preference.onPreferenceClickListener = OnPreferenceClickListener { _ ->
|
||||||
activity?.let {
|
activity?.let {
|
||||||
UserFragment
|
UserFragment
|
||||||
.newInstance(user.user)
|
.newInstance(user.user, baseUrlsInUse)
|
||||||
.show(it.supportFragmentManager, UserFragment.TAG)
|
.show(it.supportFragmentManager, UserFragment.TAG)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
@ -600,7 +599,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
userAddPref.onPreferenceClickListener = OnPreferenceClickListener { _ ->
|
userAddPref.onPreferenceClickListener = OnPreferenceClickListener { _ ->
|
||||||
activity?.let {
|
activity?.let {
|
||||||
UserFragment
|
UserFragment
|
||||||
.newInstance(user = null)
|
.newInstance(user = null, baseUrlsInUse = baseUrlsInUse)
|
||||||
.show(it.supportFragmentManager, UserFragment.TAG)
|
.show(it.supportFragmentManager, UserFragment.TAG)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
@ -630,18 +629,17 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
override fun onUpdateUser(dialog: DialogFragment, user: User) {
|
override fun onUpdateUser(dialog: DialogFragment, user: User) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
repository.updateUser(user)
|
repository.updateUser(user)
|
||||||
serviceManager.stop() // Editing does not change the user ID
|
serviceManager.restart() // Editing does not change the user ID
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
userSettingsFragment.reload()
|
userSettingsFragment.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleteUser(dialog: DialogFragment, authUserId: Long) {
|
override fun onDeleteUser(dialog: DialogFragment, baseUrl: String) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
repository.removeAuthUserFromSubscriptions(authUserId)
|
repository.deleteUser(baseUrl)
|
||||||
repository.deleteUser(authUserId)
|
serviceManager.restart()
|
||||||
serviceManager.refresh() // authUserId changed, so refresh is enough
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
userSettingsFragment.reload()
|
userSettingsFragment.reload()
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import kotlin.random.Random
|
||||||
|
|
||||||
class UserFragment : DialogFragment() {
|
class UserFragment : DialogFragment() {
|
||||||
private var user: User? = null
|
private var user: User? = null
|
||||||
|
private lateinit var baseUrlsInUse: ArrayList<String>
|
||||||
private lateinit var listener: UserDialogListener
|
private lateinit var listener: UserDialogListener
|
||||||
|
|
||||||
private lateinit var baseUrlView: TextInputEditText
|
private lateinit var baseUrlView: TextInputEditText
|
||||||
|
@ -29,7 +30,7 @@ class UserFragment : DialogFragment() {
|
||||||
interface UserDialogListener {
|
interface UserDialogListener {
|
||||||
fun onAddUser(dialog: DialogFragment, user: User)
|
fun onAddUser(dialog: DialogFragment, user: User)
|
||||||
fun onUpdateUser(dialog: DialogFragment, user: User)
|
fun onUpdateUser(dialog: DialogFragment, user: User)
|
||||||
fun onDeleteUser(dialog: DialogFragment, authUserId: Long)
|
fun onDeleteUser(dialog: DialogFragment, baseUrl: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
|
@ -39,15 +40,17 @@ class UserFragment : DialogFragment() {
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
// Reconstruct user (if it is present in the bundle)
|
// Reconstruct user (if it is present in the bundle)
|
||||||
val userId = arguments?.getLong(BUNDLE_USER_ID)
|
|
||||||
val baseUrl = arguments?.getString(BUNDLE_BASE_URL)
|
val baseUrl = arguments?.getString(BUNDLE_BASE_URL)
|
||||||
val username = arguments?.getString(BUNDLE_USERNAME)
|
val username = arguments?.getString(BUNDLE_USERNAME)
|
||||||
val password = arguments?.getString(BUNDLE_PASSWORD)
|
val password = arguments?.getString(BUNDLE_PASSWORD)
|
||||||
|
|
||||||
if (userId != null && baseUrl != null && username != null && password != null) {
|
if (baseUrl != null && username != null && password != null) {
|
||||||
user = User(userId, baseUrl, username, password)
|
user = User(baseUrl, username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Required for validation
|
||||||
|
baseUrlsInUse = arguments?.getStringArrayList(BUNDLE_BASE_URLS_IN_USE) ?: arrayListOf()
|
||||||
|
|
||||||
// Build root view
|
// Build root view
|
||||||
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_user_dialog, null)
|
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_user_dialog, null)
|
||||||
|
|
||||||
|
@ -83,8 +86,8 @@ class UserFragment : DialogFragment() {
|
||||||
}
|
}
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
builder.setNeutralButton(R.string.user_dialog_button_delete) { _, _ ->
|
builder.setNeutralButton(R.string.user_dialog_button_delete) { _, _ ->
|
||||||
if (this::listener.isInitialized && userId != null) {
|
if (this::listener.isInitialized) {
|
||||||
listener.onDeleteUser(this, userId)
|
listener.onDeleteUser(this, user!!.baseUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,7 +134,7 @@ class UserFragment : DialogFragment() {
|
||||||
val username = usernameView.text?.toString() ?: ""
|
val username = usernameView.text?.toString() ?: ""
|
||||||
val password = passwordView.text?.toString() ?: ""
|
val password = passwordView.text?.toString() ?: ""
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
user = User(Random.nextLong(), baseUrl, username, password)
|
user = User(baseUrl, username, password)
|
||||||
listener.onAddUser(this, user!!)
|
listener.onAddUser(this, user!!)
|
||||||
} else {
|
} else {
|
||||||
user = if (password.isNotEmpty()) {
|
user = if (password.isNotEmpty()) {
|
||||||
|
@ -149,6 +152,7 @@ class UserFragment : DialogFragment() {
|
||||||
val password = passwordView.text?.toString() ?: ""
|
val password = passwordView.text?.toString() ?: ""
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
positiveButton.isEnabled = (baseUrl.startsWith("http://") || baseUrl.startsWith("https://"))
|
positiveButton.isEnabled = (baseUrl.startsWith("http://") || baseUrl.startsWith("https://"))
|
||||||
|
&& !baseUrlsInUse.contains(baseUrl)
|
||||||
&& username.isNotEmpty() && password.isNotEmpty()
|
&& username.isNotEmpty() && password.isNotEmpty()
|
||||||
} else {
|
} else {
|
||||||
positiveButton.isEnabled = username.isNotEmpty() // Unchanged if left blank
|
positiveButton.isEnabled = username.isNotEmpty() // Unchanged if left blank
|
||||||
|
@ -157,21 +161,21 @@ class UserFragment : DialogFragment() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "NtfyUserFragment"
|
const val TAG = "NtfyUserFragment"
|
||||||
private const val BUNDLE_USER_ID = "userId"
|
|
||||||
private const val BUNDLE_BASE_URL = "baseUrl"
|
private const val BUNDLE_BASE_URL = "baseUrl"
|
||||||
private const val BUNDLE_USERNAME = "username"
|
private const val BUNDLE_USERNAME = "username"
|
||||||
private const val BUNDLE_PASSWORD = "password"
|
private const val BUNDLE_PASSWORD = "password"
|
||||||
|
private const val BUNDLE_BASE_URLS_IN_USE = "baseUrlsInUse"
|
||||||
|
|
||||||
fun newInstance(user: User?): UserFragment {
|
fun newInstance(user: User?, baseUrlsInUse: ArrayList<String>): UserFragment {
|
||||||
val fragment = UserFragment()
|
val fragment = UserFragment()
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
|
args.putStringArrayList(BUNDLE_BASE_URLS_IN_USE, baseUrlsInUse)
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
args.putLong(BUNDLE_USER_ID, user.id)
|
|
||||||
args.putString(BUNDLE_BASE_URL, user.baseUrl)
|
args.putString(BUNDLE_BASE_URL, user.baseUrl)
|
||||||
args.putString(BUNDLE_USERNAME, user.username)
|
args.putString(BUNDLE_USERNAME, user.username)
|
||||||
args.putString(BUNDLE_PASSWORD, user.password)
|
args.putString(BUNDLE_PASSWORD, user.password)
|
||||||
fragment.arguments = args
|
|
||||||
}
|
}
|
||||||
|
fragment.arguments = args
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,6 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
|
||||||
topic = topic,
|
topic = topic,
|
||||||
instant = true, // No Firebase, always instant!
|
instant = true, // No Firebase, always instant!
|
||||||
mutedUntil = 0,
|
mutedUntil = 0,
|
||||||
authUserId = null, // FIXME add UP user in settings
|
|
||||||
upAppId = appId,
|
upAppId = appId,
|
||||||
upConnectorToken = connectorToken,
|
upConnectorToken = connectorToken,
|
||||||
totalCount = 0,
|
totalCount = 0,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
|
@ -148,36 +148,31 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingTop="16dp"
|
android:paddingTop="16dp"
|
||||||
android:paddingBottom="3dp"
|
android:paddingBottom="3dp"
|
||||||
android:text="Login required"
|
android:text="@string/add_dialog_login_title"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
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"/>
|
app:layout_constraintEnd_toStartOf="@id/add_dialog_login_error_image"/>
|
||||||
<TextView
|
<TextView
|
||||||
android:text="This topic requires you to login. Please pick an existing user or type in a username and password."
|
android:text="@string/add_dialog_login_description"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_description"
|
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_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_login_title"/>
|
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_title" android:paddingEnd="4dp"/>
|
||||||
<Spinner
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_users_spinner"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_description" android:paddingStart="0dp"/>
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/add_dialog_login_username"
|
android:id="@+id/add_dialog_login_username"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:hint="Username"
|
android:layout_height="wrap_content" android:hint="@string/add_dialog_login_username_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_login_users_spinner"/>
|
android:layout_marginTop="10dp" app:layout_constraintTop_toBottomOf="@+id/add_dialog_login_description"/>
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/add_dialog_login_password"
|
android:id="@+id/add_dialog_login_password"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:hint="Password"
|
android:layout_height="wrap_content" android:hint="@string/add_dialog_login_password_hint"
|
||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
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"
|
||||||
|
|
|
@ -14,6 +14,6 @@
|
||||||
<item android:id="@+id/detail_menu_test" android:title="@string/detail_menu_test"/>
|
<item android:id="@+id/detail_menu_test" android:title="@string/detail_menu_test"/>
|
||||||
<item android:id="@+id/detail_menu_copy_url" android:title="@string/detail_menu_copy_url"/>
|
<item android:id="@+id/detail_menu_copy_url" android:title="@string/detail_menu_copy_url"/>
|
||||||
<item android:id="@+id/detail_menu_clear" android:title="@string/detail_menu_clear"/>
|
<item android:id="@+id/detail_menu_clear" android:title="@string/detail_menu_clear"/>
|
||||||
<item android:id="@+id/detail_menu_settings" android:title="@string/detail_menu_settings"/>
|
<!--<item android:id="@+id/detail_menu_settings" android:title="@string/detail_menu_settings"/>-->
|
||||||
<item android:id="@+id/detail_menu_unsubscribe" android:title="@string/detail_menu_unsubscribe"/>
|
<item android:id="@+id/detail_menu_unsubscribe" android:title="@string/detail_menu_unsubscribe"/>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
@ -100,6 +100,10 @@
|
||||||
<string name="add_dialog_button_back">Back</string>
|
<string name="add_dialog_button_back">Back</string>
|
||||||
<string name="add_dialog_button_login">Login</string>
|
<string name="add_dialog_button_login">Login</string>
|
||||||
<string name="add_dialog_error_connection_failed">Connection failed: %1$s</string>
|
<string name="add_dialog_error_connection_failed">Connection failed: %1$s</string>
|
||||||
|
<string name="add_dialog_login_title">Login required</string>
|
||||||
|
<string name="add_dialog_login_description">This topic requires you to login. Please type in a username and password.</string>
|
||||||
|
<string name="add_dialog_login_username_hint">Username</string>
|
||||||
|
<string name="add_dialog_login_password_hint">Password</string>
|
||||||
<string name="add_dialog_login_error_not_authorized">Login failed. User not authorized.</string>
|
<string name="add_dialog_login_error_not_authorized">Login failed. User not authorized.</string>
|
||||||
<string name="add_dialog_login_new_user">New user</string>
|
<string name="add_dialog_login_new_user">New user</string>
|
||||||
|
|
||||||
|
@ -259,7 +263,7 @@
|
||||||
<string name="settings_users_prefs_user_used_by_many">Used by topics %1$s</string>
|
<string name="settings_users_prefs_user_used_by_many">Used by topics %1$s</string>
|
||||||
<string name="settings_users_prefs_user_add">Add users</string>
|
<string name="settings_users_prefs_user_add">Add users</string>
|
||||||
<string name="settings_users_prefs_user_add_title">Add new user</string>
|
<string name="settings_users_prefs_user_add_title">Add new user</string>
|
||||||
<string name="settings_users_prefs_user_add_summary">Create a new user that can be associated to topics. You can also create a new user when adding a topic.</string>
|
<string name="settings_users_prefs_user_add_summary">Create a new user for a new server</string>
|
||||||
<string name="settings_unified_push_header">UnifiedPush</string>
|
<string name="settings_unified_push_header">UnifiedPush</string>
|
||||||
<string name="settings_unified_push_header_summary">Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org.</string>
|
<string name="settings_unified_push_header_summary">Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org.</string>
|
||||||
<string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>
|
<string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>
|
||||||
|
|
Loading…
Reference in a new issue