Remove denormalized subscription, add schema migration for sqlite db
This commit is contained in:
parent
72d7a2f93d
commit
86738d5441
13 changed files with 265 additions and 61 deletions
|
@ -16,6 +16,14 @@ android {
|
|||
versionName "1.0.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
/* Required for Room schema migrations */
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
100
app/schemas/io.heckel.ntfy.data.Database/2.json
Normal file
100
app/schemas/io.heckel.ntfy.data.Database/2.json
Normal file
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "30177aa8688290d24499babf22b15720",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Subscription",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `topic` TEXT NOT NULL, 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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Subscription_baseUrl_topic",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"baseUrl",
|
||||
"topic"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Notification",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `message` TEXT NOT NULL, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"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": "message",
|
||||
"columnName": "message",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "deleted",
|
||||
"columnName": "deleted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"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, '30177aa8688290d24499babf22b15720')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
package io.heckel.ntfy.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Entity(indices = [Index(value = ["baseUrl", "topic"], unique = true)])
|
||||
|
@ -10,8 +13,18 @@ data class Subscription(
|
|||
@PrimaryKey val id: Long, // Internal ID, only used in Repository and activities
|
||||
@ColumnInfo(name = "baseUrl") val baseUrl: String,
|
||||
@ColumnInfo(name = "topic") val topic: String,
|
||||
@ColumnInfo(name = "notifications") val notifications: Int,
|
||||
@ColumnInfo(name = "lastActive") val lastActive: Long, // Unix timestamp
|
||||
@Ignore val notifications: Int,
|
||||
@Ignore val lastActive: Long = 0 // Unix timestamp
|
||||
) {
|
||||
constructor(id: Long, baseUrl: String, topic: String) : this(id, baseUrl, topic, 0, 0)
|
||||
}
|
||||
|
||||
data class SubscriptionWithMetadata(
|
||||
val id: Long,
|
||||
val baseUrl: String,
|
||||
val topic: String,
|
||||
val notifications: Int,
|
||||
val lastActive: Long
|
||||
)
|
||||
|
||||
@Entity
|
||||
|
@ -19,10 +32,11 @@ data class Notification(
|
|||
@PrimaryKey val id: String,
|
||||
@ColumnInfo(name = "subscriptionId") val subscriptionId: Long,
|
||||
@ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp
|
||||
@ColumnInfo(name = "message") val message: String
|
||||
@ColumnInfo(name = "message") val message: String,
|
||||
@ColumnInfo(name = "deleted") val deleted: Boolean
|
||||
)
|
||||
|
||||
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 1)
|
||||
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 2)
|
||||
abstract class Database : RoomDatabase() {
|
||||
abstract fun subscriptionDao(): SubscriptionDao
|
||||
abstract fun notificationDao(): NotificationDao
|
||||
|
@ -35,45 +49,81 @@ abstract class Database : RoomDatabase() {
|
|||
return instance ?: synchronized(this) {
|
||||
val instance = Room
|
||||
.databaseBuilder(context.applicationContext, Database::class.java,"AppDatabase")
|
||||
.addMigrations(MIGRATION_1_2)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
this.instance = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// Drop "notifications" & "lastActive" columns (SQLite does not support dropping columns, ...)
|
||||
db.execSQL("CREATE TABLE Subscription_New (id INTEGER NOT NULL, baseUrl TEXT NOT NULL, topic TEXT NOT NULL, PRIMARY KEY(id))")
|
||||
db.execSQL("INSERT INTO Subscription_New SELECT id, baseUrl, topic FROM Subscription")
|
||||
db.execSQL("DROP TABLE Subscription")
|
||||
db.execSQL("ALTER TABLE Subscription_New RENAME TO Subscription")
|
||||
db.execSQL("CREATE UNIQUE INDEX index_Subscription_baseUrl_topic ON Subscription (baseUrl, topic)")
|
||||
|
||||
// Add "deleted" column
|
||||
db.execSQL("ALTER TABLE Notification ADD COLUMN deleted INTEGER NOT NULL DEFAULT('0')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface SubscriptionDao {
|
||||
@Query("SELECT * FROM subscription ORDER BY lastActive DESC")
|
||||
fun listFlow(): Flow<List<Subscription>>
|
||||
@Query(
|
||||
"SELECT s.id, s.baseUrl, s.topic, COUNT(n.id) notifications, IFNULL(MAX(n.timestamp),0) AS lastActive " +
|
||||
"FROM subscription AS s " +
|
||||
"LEFT JOIN notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 " +
|
||||
"GROUP BY s.id " +
|
||||
"ORDER BY MAX(n.timestamp) DESC"
|
||||
)
|
||||
fun listFlow(): Flow<List<SubscriptionWithMetadata>>
|
||||
|
||||
@Query("SELECT * FROM subscription ORDER BY lastActive DESC")
|
||||
fun list(): List<Subscription>
|
||||
@Query(
|
||||
"SELECT s.id, s.baseUrl, s.topic, COUNT(n.id) notifications, IFNULL(MAX(n.timestamp),0) AS lastActive " +
|
||||
"FROM subscription AS s " +
|
||||
"LEFT JOIN notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 " +
|
||||
"GROUP BY s.id " +
|
||||
"ORDER BY MAX(n.timestamp) DESC"
|
||||
)
|
||||
fun list(): List<SubscriptionWithMetadata>
|
||||
|
||||
@Query("SELECT * FROM subscription WHERE baseUrl = :baseUrl AND topic = :topic")
|
||||
fun get(baseUrl: String, topic: String): Subscription?
|
||||
@Query(
|
||||
"SELECT s.id, s.baseUrl, s.topic, COUNT(n.id) notifications, IFNULL(MAX(n.timestamp),0) AS lastActive " +
|
||||
"FROM subscription AS s " +
|
||||
"LEFT JOIN notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 " +
|
||||
"WHERE s.baseUrl = :baseUrl AND s.topic = :topic " +
|
||||
"GROUP BY s.id "
|
||||
)
|
||||
fun get(baseUrl: String, topic: String): SubscriptionWithMetadata?
|
||||
|
||||
@Query("SELECT * FROM subscription WHERE id = :subscriptionId")
|
||||
fun get(subscriptionId: Long): Subscription?
|
||||
@Query(
|
||||
"SELECT s.id, s.baseUrl, s.topic, COUNT(n.id) notifications, IFNULL(MAX(n.timestamp),0) AS lastActive " +
|
||||
"FROM subscription AS s " +
|
||||
"LEFT JOIN notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 " +
|
||||
"WHERE s.id = :subscriptionId " +
|
||||
"GROUP BY s.id "
|
||||
)
|
||||
fun get(subscriptionId: Long): SubscriptionWithMetadata?
|
||||
|
||||
@Insert
|
||||
fun add(subscription: Subscription)
|
||||
|
||||
@Update
|
||||
fun update(subscription: Subscription)
|
||||
|
||||
@Query("DELETE FROM subscription WHERE id = :subscriptionId")
|
||||
fun remove(subscriptionId: Long)
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface NotificationDao {
|
||||
@Query("SELECT * FROM notification WHERE subscriptionId = :subscriptionId ORDER BY timestamp DESC")
|
||||
@Query("SELECT * FROM notification WHERE subscriptionId = :subscriptionId AND deleted != 1 ORDER BY timestamp DESC")
|
||||
fun list(subscriptionId: Long): Flow<List<Notification>>
|
||||
|
||||
@Query("SELECT id FROM notification WHERE subscriptionId = :subscriptionId")
|
||||
@Query("SELECT id FROM notification WHERE subscriptionId = :subscriptionId") // Includes deleted
|
||||
fun listIds(subscriptionId: Long): List<String>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
|
@ -82,7 +132,7 @@ interface NotificationDao {
|
|||
@Query("SELECT * FROM notification WHERE id = :notificationId")
|
||||
fun get(notificationId: String): Notification?
|
||||
|
||||
@Query("DELETE FROM notification WHERE id = :notificationId")
|
||||
@Query("UPDATE notification SET deleted = 1 WHERE id = :notificationId")
|
||||
fun remove(notificationId: String)
|
||||
|
||||
@Query("DELETE FROM notification WHERE subscriptionId = :subscriptionId")
|
||||
|
|
|
@ -4,7 +4,8 @@ import android.util.Log
|
|||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import java.util.*
|
||||
import androidx.lifecycle.map
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class Repository(private val subscriptionDao: SubscriptionDao, private val notificationDao: NotificationDao) {
|
||||
init {
|
||||
|
@ -12,17 +13,20 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
|
|||
}
|
||||
|
||||
fun getSubscriptionsLiveData(): LiveData<List<Subscription>> {
|
||||
return subscriptionDao.listFlow().asLiveData()
|
||||
return subscriptionDao
|
||||
.listFlow()
|
||||
.asLiveData()
|
||||
.map { list -> toSubscriptionList(list) }
|
||||
}
|
||||
|
||||
fun getSubscriptions(): List<Subscription> {
|
||||
return subscriptionDao.list()
|
||||
return toSubscriptionList(subscriptionDao.list())
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
@WorkerThread
|
||||
suspend fun getSubscription(baseUrl: String, topic: String): Subscription? {
|
||||
return subscriptionDao.get(baseUrl, topic)
|
||||
return toSubscription(subscriptionDao.get(baseUrl, topic))
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
|
@ -31,12 +35,6 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
|
|||
subscriptionDao.add(subscription)
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
@WorkerThread
|
||||
suspend fun updateSubscription(subscription: Subscription) {
|
||||
subscriptionDao.update(subscription)
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
@WorkerThread
|
||||
suspend fun removeSubscription(subscriptionId: Long) {
|
||||
|
@ -54,24 +52,16 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
|
|||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
@WorkerThread
|
||||
suspend fun addNotification(subscriptionId: Long, notification: Notification) {
|
||||
suspend fun addNotification(notification: Notification) {
|
||||
val maybeExistingNotification = notificationDao.get(notification.id)
|
||||
if (maybeExistingNotification != null) {
|
||||
return
|
||||
}
|
||||
|
||||
val subscription = subscriptionDao.get(subscriptionId) ?: return
|
||||
val newSubscription = subscription.copy(notifications = subscription.notifications + 1, lastActive = Date().time/1000)
|
||||
subscriptionDao.update(newSubscription)
|
||||
if (maybeExistingNotification == null) {
|
||||
notificationDao.add(notification)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
@WorkerThread
|
||||
suspend fun removeNotification(subscriptionId: Long, notificationId: String) {
|
||||
val subscription = subscriptionDao.get(subscriptionId) ?: return
|
||||
val newSubscription = subscription.copy(notifications = subscription.notifications - 1, lastActive = Date().time/1000)
|
||||
subscriptionDao.update(newSubscription)
|
||||
suspend fun removeNotification(notificationId: String) {
|
||||
notificationDao.remove(notificationId)
|
||||
}
|
||||
|
||||
|
@ -81,6 +71,31 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
|
|||
notificationDao.removeAll(subscriptionId)
|
||||
}
|
||||
|
||||
private fun toSubscriptionList(list: List<SubscriptionWithMetadata>): List<Subscription> {
|
||||
return list.map { s ->
|
||||
Subscription(
|
||||
id = s.id,
|
||||
baseUrl = s.baseUrl,
|
||||
topic = s.topic,
|
||||
lastActive = s.lastActive,
|
||||
notifications = s.notifications
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toSubscription(s: SubscriptionWithMetadata?): Subscription? {
|
||||
if (s == null) {
|
||||
return null
|
||||
}
|
||||
return Subscription(
|
||||
id = s.id,
|
||||
baseUrl = s.baseUrl,
|
||||
topic = s.topic,
|
||||
lastActive = s.lastActive,
|
||||
notifications = s.notifications
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "NtfyRepository"
|
||||
private var instance: Repository? = null
|
||||
|
|
|
@ -53,7 +53,7 @@ class ApiService {
|
|||
|
||||
private fun fromString(subscriptionId: Long, s: String): Notification {
|
||||
val n = gson.fromJson(s, NotificationData::class.java) // Indirection to prevent accidental field renames, etc.
|
||||
return Notification(n.id, subscriptionId, n.time, n.message)
|
||||
return Notification(n.id, subscriptionId, n.time, n.message, false)
|
||||
}
|
||||
|
||||
private data class NotificationData(
|
||||
|
|
|
@ -39,8 +39,8 @@ class FirebaseService : FirebaseMessagingService() {
|
|||
|
||||
// Add notification
|
||||
val subscription = repository.getSubscription(baseUrl, topic) ?: return@launch
|
||||
val notification = Notification(id = id, subscriptionId = subscription.id, timestamp = timestamp, message = message)
|
||||
repository.addNotification(subscription.id, notification)
|
||||
val notification = Notification(id = id, subscriptionId = subscription.id, timestamp = timestamp, message = message, deleted = false)
|
||||
repository.addNotification(notification)
|
||||
|
||||
// Send notification
|
||||
Log.d(TAG, "Sending notification for message: from=${remoteMessage.from}, data=${data}")
|
||||
|
|
|
@ -23,6 +23,7 @@ import io.heckel.ntfy.R
|
|||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.topicShortUrl
|
||||
import io.heckel.ntfy.data.topicUrl
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -113,6 +114,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
|
|||
onRefreshClick()
|
||||
true
|
||||
}
|
||||
R.id.detail_menu_copy_url -> {
|
||||
onCopyUrlClick()
|
||||
true
|
||||
}
|
||||
R.id.detail_menu_unsubscribe -> {
|
||||
onDeleteClick()
|
||||
true
|
||||
|
@ -136,6 +141,18 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
|
|||
}
|
||||
}
|
||||
|
||||
private fun onCopyUrlClick() {
|
||||
val url = topicUrl(subscriptionBaseUrl, subscriptionTopic)
|
||||
Log.d(TAG, "Copying topic URL $url to clipboard ")
|
||||
|
||||
val clipboard: ClipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("topic address", url)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast
|
||||
.makeText(this, getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onRefreshClick() {
|
||||
Log.d(TAG, "Fetching cached notifications for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
|
||||
|
||||
|
@ -144,15 +161,15 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
|
|||
val notifications = api.poll(subscriptionId, subscriptionBaseUrl, subscriptionTopic)
|
||||
val newNotifications = repository.onlyNewNotifications(subscriptionId, notifications)
|
||||
val toastMessage = if (newNotifications.isEmpty()) {
|
||||
getString(R.string.detail_refresh_message_no_results)
|
||||
getString(R.string.refresh_message_no_results)
|
||||
} else {
|
||||
getString(R.string.detail_refresh_message_result, newNotifications.size)
|
||||
getString(R.string.refresh_message_result, newNotifications.size)
|
||||
}
|
||||
newNotifications.forEach { notification -> repository.addNotification(subscriptionId, notification) }
|
||||
newNotifications.forEach { notification -> repository.addNotification(notification) }
|
||||
runOnUiThread { Toast.makeText(this@DetailActivity, toastMessage, Toast.LENGTH_LONG).show() }
|
||||
} catch (e: Exception) {
|
||||
Toast
|
||||
.makeText(this@DetailActivity, getString(R.string.detail_refresh_message_error, e.message), Toast.LENGTH_LONG)
|
||||
.makeText(this@DetailActivity, getString(R.string.refresh_message_error, e.message), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
@ -243,7 +260,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
|
|||
builder
|
||||
.setMessage(R.string.detail_action_mode_delete_dialog_message)
|
||||
.setPositiveButton(R.string.detail_action_mode_delete_dialog_permanently_delete) { _, _ ->
|
||||
adapter.selected.map { notificationId -> viewModel.remove(subscriptionId, notificationId) }
|
||||
adapter.selected.map { notificationId -> viewModel.remove(notificationId) }
|
||||
finishActionMode()
|
||||
}
|
||||
.setNegativeButton(R.string.detail_action_mode_delete_dialog_cancel) { _, _ ->
|
||||
|
|
|
@ -14,8 +14,8 @@ class DetailViewModel(private val repository: Repository) : ViewModel() {
|
|||
return repository.getNotificationsLiveData(subscriptionId)
|
||||
}
|
||||
|
||||
fun remove(subscriptionId: Long, notificationId: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.removeNotification(subscriptionId, notificationId)
|
||||
fun remove(notificationId: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.removeNotification(notificationId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,8 +76,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback {
|
|||
mainList.adapter = adapter
|
||||
|
||||
viewModel.list().observe(this) {
|
||||
it?.let {
|
||||
adapter.submitList(it as MutableList<Subscription>)
|
||||
it?.let { subscriptions ->
|
||||
adapter.submitList(subscriptions as MutableList<Subscription>)
|
||||
if (it.isEmpty()) {
|
||||
mainList.visibility = View.GONE
|
||||
noEntries.visibility = View.VISIBLE
|
||||
|
@ -168,7 +168,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback {
|
|||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic)
|
||||
notifications.forEach { notification -> repository.addNotification(subscription.id, notification) }
|
||||
notifications.forEach { notification -> repository.addNotification(notification) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to fetch notifications: ${e.stackTrace}")
|
||||
}
|
||||
|
@ -196,19 +196,27 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback {
|
|||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
Log.d(TAG, "Polling for new notifications")
|
||||
var newNotificationsCount = 0
|
||||
repository.getSubscriptions().forEach { subscription ->
|
||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic)
|
||||
val newNotifications = repository.onlyNewNotifications(subscription.id, notifications)
|
||||
newNotifications.forEach { notification ->
|
||||
repository.addNotification(subscription.id, notification)
|
||||
repository.addNotification(notification)
|
||||
notifier?.send(subscription, notification.message)
|
||||
newNotificationsCount++
|
||||
}
|
||||
}
|
||||
val toastMessage = if (newNotificationsCount == 0) {
|
||||
getString(R.string.refresh_message_no_results)
|
||||
} else {
|
||||
getString(R.string.refresh_message_result, newNotificationsCount)
|
||||
}
|
||||
runOnUiThread { Toast.makeText(this@MainActivity, toastMessage, Toast.LENGTH_LONG).show() }
|
||||
Log.d(TAG, "Finished polling for new notifications")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Polling failed: ${e.message}", e)
|
||||
runOnUiThread {
|
||||
Toast.makeText(this@MainActivity, getString(R.string.poll_worker_exception, e.message), Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(this@MainActivity, getString(R.string.refresh_message_error, e.message), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,10 +56,12 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon
|
|||
} else {
|
||||
context.getString(R.string.main_item_status_text_not_one, subscription.notifications)
|
||||
}
|
||||
val dateText = if (System.currentTimeMillis()/1000 - subscription.lastActive < 24 * 60 * 60) {
|
||||
val dateText = if (subscription.lastActive == 0L) {
|
||||
""
|
||||
} else if (System.currentTimeMillis()/1000 - subscription.lastActive < 24 * 60 * 60) {
|
||||
SimpleDateFormat("HH:mm").format(Date(subscription.lastActive*1000))
|
||||
} else {
|
||||
SimpleDateFormat("MM/dd").format(Date(subscription.lastActive*1000))
|
||||
SimpleDateFormat("M/d/yy").format(Date(subscription.lastActive*1000))
|
||||
}
|
||||
nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic)
|
||||
statusView.text = statusMessage
|
||||
|
|
|
@ -31,7 +31,7 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
|
|||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic)
|
||||
val newNotifications = repository.onlyNewNotifications(subscription.id, notifications)
|
||||
newNotifications.forEach { notification ->
|
||||
repository.addNotification(subscription.id, notification)
|
||||
repository.addNotification(notification)
|
||||
notifier.send(subscription, notification.message)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/detail_menu_test" android:title="@string/detail_menu_test"/>
|
||||
<item android:id="@+id/detail_menu_refresh" android:title="@string/detail_menu_refresh"/>
|
||||
<item android:id="@+id/detail_menu_copy_url" android:title="@string/detail_menu_copy_url"/>
|
||||
<item android:id="@+id/detail_menu_unsubscribe" android:title="@string/detail_menu_unsubscribe"/>
|
||||
</menu>
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
<string name="notification_channel_name">Ntfy</string>
|
||||
<string name="notification_channel_id">ntfy</string>
|
||||
|
||||
<!-- Common refresh toasts -->
|
||||
<string name="refresh_message_result">%1$d notification(s) received</string>
|
||||
<string name="refresh_message_no_results">Everything is up-to-date</string>
|
||||
<string name="refresh_message_error">Could not refresh topic: %1$s</string>
|
||||
|
||||
<!-- Main activity: Action bar -->
|
||||
<string name="main_action_bar_title">Subscribed topics</string>
|
||||
<string name="main_menu_refresh">Force refresh</string>
|
||||
|
@ -46,13 +51,11 @@
|
|||
<string name="detail_delete_dialog_cancel">Cancel</string>
|
||||
<string name="detail_test_message">This is a test notification from the Ntfy Android app. It was sent at %1$s.</string>
|
||||
<string name="detail_test_message_error">Could not send test message: %1$s</string>
|
||||
<string name="detail_refresh_message_result">%1$d notification(s) added</string>
|
||||
<string name="detail_refresh_message_no_results">No new notifications found</string>
|
||||
<string name="detail_refresh_message_error">Could not refresh topic: %1$s</string>
|
||||
<string name="detail_copied_to_clipboard_message">Copied to clipboard</string>
|
||||
|
||||
<!-- Detail activity: Action bar -->
|
||||
<string name="detail_menu_test">Send test notification</string>
|
||||
<string name="detail_menu_copy_url">Copy topic address</string>
|
||||
<string name="detail_menu_refresh">Force refresh</string>
|
||||
<string name="detail_menu_unsubscribe">Unsubscribe</string>
|
||||
|
||||
|
|
Loading…
Reference in a new issue