Use since=<id> API, #165

This commit is contained in:
Philipp Heckel 2022-06-18 21:01:05 -04:00
parent eabb163fde
commit 18f3dc883e
10 changed files with 365 additions and 14 deletions

View file

@ -0,0 +1,326 @@
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "b439720b55cf5e6bfdec2b56dd46103d",
"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, `minPriority` INTEGER NOT NULL, `autoDelete` INTEGER NOT NULL, `lastNotificationId` TEXT, `icon` TEXT, `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": "minPriority",
"columnName": "minPriority",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "autoDelete",
"columnName": "autoDelete",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": false
},
{
"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, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `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": "encoding",
"columnName": "encoding",
"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": "actions",
"columnName": "actions",
"affinity": "TEXT",
"notNull": false
},
{
"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, 'b439720b55cf5e6bfdec2b56dd46103d')"
]
}
}

View file

@ -97,6 +97,7 @@ class Backuper(val context: Context) {
mutedUntil = s.mutedUntil,
minPriority = s.minPriority ?: Repository.MIN_PRIORITY_USE_GLOBAL,
autoDelete = s.autoDelete ?: Repository.AUTO_DELETE_USE_GLOBAL,
lastNotificationId = s.lastNotificationId,
icon = s.icon,
upAppId = s.upAppId,
upConnectorToken = s.upConnectorToken
@ -220,6 +221,7 @@ class Backuper(val context: Context) {
mutedUntil = s.mutedUntil,
minPriority = s.minPriority,
autoDelete = s.autoDelete,
lastNotificationId = s.lastNotificationId,
icon = s.icon,
upAppId = s.upAppId,
upConnectorToken = s.upConnectorToken
@ -326,6 +328,7 @@ data class Subscription(
val mutedUntil: Long,
val minPriority: Int?,
val autoDelete: Long?,
val lastNotificationId: String?,
val icon: String?,
val upAppId: String?,
val upConnectorToken: String?

View file

@ -18,6 +18,7 @@ data class Subscription(
@ColumnInfo(name = "mutedUntil") val mutedUntil: Long,
@ColumnInfo(name = "minPriority") val minPriority: Int,
@ColumnInfo(name = "autoDelete") val autoDelete: Long, // Seconds
@ColumnInfo(name = "lastNotificationId") val lastNotificationId: String?, // Used for polling, with since=<id>
@ColumnInfo(name = "icon") val icon: String?, // content://-URI (or later other identifier)
@ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name
@ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, // UnifiedPush connector token
@ -26,8 +27,8 @@ data class Subscription(
@Ignore val lastActive: Long = 0, // Unix timestamp
@Ignore val state: ConnectionState = ConnectionState.NOT_APPLICABLE
) {
constructor(id: Long, baseUrl: String, topic: String, instant: Boolean, mutedUntil: Long, minPriority: Int, autoDelete: Long, icon: String, upAppId: String, upConnectorToken: String) :
this(id, baseUrl, topic, instant, mutedUntil, minPriority, autoDelete, icon, upAppId, upConnectorToken, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
constructor(id: Long, baseUrl: String, topic: String, instant: Boolean, mutedUntil: Long, minPriority: Int, autoDelete: Long, lastNotificationId: String, icon: String, upAppId: String, upConnectorToken: String) :
this(id, baseUrl, topic, instant, mutedUntil, minPriority, autoDelete, lastNotificationId, icon, upAppId, upConnectorToken, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
}
enum class ConnectionState {
@ -42,6 +43,7 @@ data class SubscriptionWithMetadata(
val mutedUntil: Long,
val autoDelete: Long,
val minPriority: Int,
val lastNotificationId: String?,
val icon: String?,
val upAppId: String?,
val upConnectorToken: String?,
@ -144,7 +146,7 @@ data class LogEntry(
this(0, timestamp, tag, level, message, exception)
}
@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 11)
@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 12)
@TypeConverters(Converters::class)
abstract class Database : RoomDatabase() {
abstract fun subscriptionDao(): SubscriptionDao
@ -170,6 +172,7 @@ abstract class Database : RoomDatabase() {
.addMigrations(MIGRATION_8_9)
.addMigrations(MIGRATION_9_10)
.addMigrations(MIGRATION_10_11)
.addMigrations(MIGRATION_11_12)
.fallbackToDestructiveMigration()
.build()
this.instance = instance
@ -259,6 +262,12 @@ abstract class Database : RoomDatabase() {
db.execSQL("ALTER TABLE Subscription ADD COLUMN icon TEXT")
}
}
private val MIGRATION_11_12 = object : Migration(11, 12) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE Subscription ADD COLUMN lastNotificationId TEXT")
}
}
}
}
@ -266,7 +275,7 @@ abstract class Database : RoomDatabase() {
interface SubscriptionDao {
@Query("""
SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.icon, s.upAppId, s.upConnectorToken,
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@ -279,7 +288,7 @@ interface SubscriptionDao {
@Query("""
SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.icon, s.upAppId, s.upConnectorToken,
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@ -292,7 +301,7 @@ interface SubscriptionDao {
@Query("""
SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.icon, s.upAppId, s.upConnectorToken,
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@ -305,7 +314,7 @@ interface SubscriptionDao {
@Query("""
SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.icon, s.upAppId, s.upConnectorToken,
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@ -318,7 +327,7 @@ interface SubscriptionDao {
@Query("""
SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.icon, s.upAppId, s.upConnectorToken,
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@ -335,6 +344,9 @@ interface SubscriptionDao {
@Update
fun update(subscription: Subscription)
@Query("UPDATE subscription SET lastNotificationId = :lastNotificationId WHERE id = :subscriptionId")
fun updateLastNotificationId(subscriptionId: Long, lastNotificationId: String)
@Query("DELETE FROM subscription WHERE id = :subscriptionId")
fun remove(subscriptionId: Long)
}

View file

@ -116,6 +116,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
if (maybeExistingNotification != null) {
return false
}
subscriptionDao.updateLastNotificationId(notification.subscriptionId, notification.id)
notificationDao.add(notification)
return true
}
@ -379,6 +380,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
mutedUntil = s.mutedUntil,
minPriority = s.minPriority,
autoDelete = s.autoDelete,
lastNotificationId = s.lastNotificationId,
icon = s.icon,
upAppId = s.upAppId,
upConnectorToken = s.upConnectorToken,
@ -402,6 +404,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
mutedUntil = s.mutedUntil,
minPriority = s.minPriority,
autoDelete = s.autoDelete,
lastNotificationId = s.lastNotificationId,
icon = s.icon,
upAppId = s.upAppId,
upConnectorToken = s.upConnectorToken,

View file

@ -84,8 +84,8 @@ class ApiService {
}
}
fun poll(subscriptionId: Long, baseUrl: String, topic: String, user: User?, since: Long = 0L): List<Notification> {
val sinceVal = if (since == 0L) "all" else since.toString()
fun poll(subscriptionId: Long, baseUrl: String, topic: String, user: User?, since: String? = null): List<Notification> {
val sinceVal = since ?: "all"
val url = topicUrlJsonPoll(baseUrl, topic, sinceVal)
Log.d(TAG, "Polling topic $url")

View file

@ -112,6 +112,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
mutedUntil = 0,
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
lastNotificationId = null,
icon = null,
upAppId = null,
upConnectorToken = null,
@ -457,8 +458,9 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
lifecycleScope.launch(Dispatchers.IO) {
try {
val user = repository.getUser(subscriptionBaseUrl) // May be null
val notifications = api.poll(subscriptionId, subscriptionBaseUrl, subscriptionTopic, user)
val subscription = repository.getSubscription(subscriptionId) ?: return@launch
val user = repository.getUser(subscription.baseUrl) // May be null
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user, subscription.lastNotificationId)
val newNotifications = repository.onlyNewNotifications(subscriptionId, notifications)
val toastMessage = if (newNotifications.isEmpty()) {
getString(R.string.refresh_message_no_results)

View file

@ -427,6 +427,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
mutedUntil = 0,
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
lastNotificationId = null,
icon = null,
upAppId = null,
upConnectorToken = null,
@ -490,7 +491,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
repository.getSubscriptions().forEach { subscription ->
try {
val user = repository.getUser(subscription.baseUrl) // May be null
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user)
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user, subscription.lastNotificationId)
val newNotifications = repository.onlyNewNotifications(subscription.id, notifications)
newNotifications.forEach { notification ->
newNotificationsCount++

View file

@ -78,6 +78,7 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
mutedUntil = 0,
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
lastNotificationId = null,
icon = null,
upAppId = appId,
upConnectorToken = connectorToken,

View file

@ -45,7 +45,7 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
baseUrl = subscription.baseUrl,
topic = subscription.topic,
user = user,
since = subscription.lastActive
since = subscription.lastNotificationId
)
val newNotifications = repository
.onlyNewNotifications(subscription.id, notifications)

View file

@ -1,3 +1,6 @@
Features:
* Polling is now done with since=<id> API, which makes deduping easier (#165)
Bugs:
* Long-click selecting of notifications doesn't scoll to the top anymore (#235, thanks to @wunter8)
* Add attachment and click URL extras to MESSAGE_RECEIVED broadcast (#329, thanks to @wunter8)