SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.kt

351 lines
13 KiB
Kotlin
Raw Normal View History

package jp.juggler.subwaytooter.table
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.provider.BaseColumns
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.putMayNull
2018-12-01 00:02:18 +01:00
import jp.juggler.util.LogCategory
2021-06-24 06:49:27 +02:00
import jp.juggler.util.TableCompanion
2018-12-01 00:02:18 +01:00
import jp.juggler.util.getLong
import jp.juggler.util.minComparable
import java.util.concurrent.ConcurrentHashMap
class NotificationTracking {
private var dirty = false
private var id = -1L
set(value) {
dirty = true; field = value
}
private var accountDbId: Long = 0
set(value) {
dirty = true; field = value
}
private var notificationType: String = ""
set(value) {
dirty = true; field = value
}
var nid_read: EntityId? = null
set(value) {
dirty = true; field = value
}
var nid_show: EntityId? = null
set(value) {
dirty = true; field = value
}
var post_id: EntityId? = null
set(value) {
dirty = true; field = value
}
var post_time: Long = 0
set(value) {
dirty = true; field = value
}
fun save(acct: String) {
if (dirty) {
try {
val cv = ContentValues()
cv.put(COL_ACCOUNT_DB_ID, accountDbId)
cv.put(COL_NOTIFICATION_TYPE, notificationType)
nid_read.putMayNull(cv, COL_NID_READ)
nid_show.putMayNull(cv, COL_NID_SHOW)
post_id.putMayNull(cv, COL_POST_ID)
cv.put(COL_POST_TIME, post_time)
val rv = App1.database.replaceOrThrow(table, null, cv)
if (rv != -1L && id == -1L) id = rv
log.d("$acct/$notificationType save. post=($post_id,$post_time)")
dirty = false
clearCache(accountDbId, notificationType)
} catch (ex: Throwable) {
log.e(ex, "save failed.")
}
}
}
fun updatePost(postId: EntityId, postTime: Long) {
this.post_id = postId
this.post_time = postTime
if (dirty) {
try {
val cv = ContentValues()
postId.putTo(cv, COL_POST_ID)
cv.put(COL_POST_TIME, postTime)
val rows = App1.database.update(
table,
cv,
WHERE_AID,
arrayOf(accountDbId.toString(), notificationType)
)
log.d("updatePost account_db_id=$accountDbId, nt=$notificationType, post=$postId,$postTime update_rows=$rows")
dirty = false
clearCache(accountDbId, notificationType)
} catch (ex: Throwable) {
log.e(ex, "updatePost failed.")
}
}
}
companion object : TableCompanion {
private val log = LogCategory("NotificationTracking")
override val table = "noti_trac"
private const val COL_ID = BaseColumns._ID
// アカウントDBの行ID。 サーバ側のIDではない
private const val COL_ACCOUNT_DB_ID = "a"
// 通知ID。ここまで既読
private const val COL_NID_READ = "nr"
// 通知ID。もっとも最近取得したもの
private const val COL_NID_SHOW = "ns"
// 最後に表示した通知のID
private const val COL_POST_ID = "pi"
// 最後に表示した通知の作成時刻
private const val COL_POST_TIME = "pt"
// 返信だけ通知グループを分ける
private const val COL_NOTIFICATION_TYPE = "nt"
override fun onDBCreate(db: SQLiteDatabase) {
db.execSQL(
"""
create table if not exists $table
($COL_ID INTEGER PRIMARY KEY
,$COL_ACCOUNT_DB_ID integer not null
,$COL_NID_READ text
,$COL_NID_SHOW text
,$COL_POST_ID text
,$COL_POST_TIME integer default 0
,$COL_NOTIFICATION_TYPE text default ''
)
"""
)
db.execSQL(
"create unique index if not exists ${table}_b on $table ($COL_ACCOUNT_DB_ID,$COL_NOTIFICATION_TYPE)"
)
}
override fun onDBUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (newVersion < oldVersion) {
try {
db.execSQL("drop table if exists $table")
} catch (ex: Throwable) {
log.trace(ex, "delete DB failed.")
}
onDBCreate(db)
return
}
if (oldVersion < 2 && newVersion >= 2) {
onDBCreate(db)
return
}
if (oldVersion < 40 && newVersion >= 40) {
try {
db.execSQL("drop index if exists ${table}_a")
} catch (ex: Throwable) {
log.trace(ex)
}
try {
db.execSQL("alter table $table add column $COL_NOTIFICATION_TYPE text default ''")
} catch (ex: Throwable) {
log.trace(ex)
}
try {
db.execSQL(
"create unique index if not exists ${table}_b on $table ($COL_ACCOUNT_DB_ID,$COL_NOTIFICATION_TYPE)"
)
} catch (ex: Throwable) {
log.trace(ex)
}
}
}
/////////////////////////////////////////////////////////////////////////////////
private val cache =
ConcurrentHashMap<Long, ConcurrentHashMap<String, NotificationTracking>>()
private fun <K : Any, V : Any> ConcurrentHashMap<K, V>.getOrCreate(
key: K,
creator: () -> V
): V {
var v = this[key]
if (v == null) v = creator().also { this[key] = it }
return v
}
private fun loadCache(accountDbId: Long, notificationType: String): NotificationTracking? =
cache[accountDbId]?.get(notificationType)
private fun clearCache(accountDbId: Long, notificationType: String): NotificationTracking? =
cache[accountDbId]?.remove(notificationType)
private fun saveCache(
accountDbId: Long,
notificationType: String,
nt: NotificationTracking
) {
cache.getOrCreate(accountDbId) {
ConcurrentHashMap<String, NotificationTracking>()
}[notificationType] = nt
}
/////////////////////////////////////////////////////////////////////////////////
private const val WHERE_AID = "$COL_ACCOUNT_DB_ID=? and $COL_NOTIFICATION_TYPE=?"
fun load(acct: String, accountDbId: Long, notificationType: String): NotificationTracking {
loadCache(accountDbId, notificationType)?.let { dst ->
if (!dst.dirty) {
log.d(
"$acct/$notificationType load-cached. post=(${dst.post_id},${dst.post_time}), read=${dst.nid_read}, show=${dst.nid_show}"
)
return dst
}
}
val dst = NotificationTracking()
dst.accountDbId = accountDbId
dst.notificationType = notificationType
try {
App1.database.query(
table,
null,
WHERE_AID,
arrayOf(accountDbId.toString(), notificationType),
null,
null,
null
)?.use { cursor ->
if (cursor.moveToFirst()) {
dst.id = cursor.getLong(COL_ID)
dst.post_id = EntityId.from(cursor, COL_POST_ID)
dst.post_time = cursor.getLong(COL_POST_TIME)
val show = EntityId.from(cursor, COL_NID_SHOW)
if (show == null) {
dst.nid_show = null
dst.nid_read = null
} else {
dst.nid_show = show
val read = EntityId.from(cursor, COL_NID_READ)
if (read == null) {
dst.nid_read = null
} else {
val r2 = minComparable(show, read)
dst.nid_read = r2
if (r2 != read) {
log.i("$acct/$notificationType read>show! clip to $show")
val cv = ContentValues()
show.putTo(cv, COL_NID_READ) //変数名とキー名が異なるのに注意
val where_args =
arrayOf(accountDbId.toString(), notificationType)
App1.database.update(table, cv, WHERE_AID, where_args)
}
}
}
log.d(
"$acct/$notificationType load. post=(${dst.post_id},${dst.post_time}), read=${dst.nid_read}, show=${dst.nid_show}"
)
saveCache(accountDbId, notificationType, dst)
}
}
} catch (ex: Throwable) {
log.trace(ex, "load failed.")
} finally {
dst.dirty = false
}
return dst
}
fun updateRead(accountDbId: Long, notificationType: String) {
try {
val where_args = arrayOf(accountDbId.toString(), notificationType)
App1.database.query(
table,
arrayOf(COL_NID_SHOW, COL_NID_READ),
WHERE_AID,
where_args,
null,
null,
null
)?.use { cursor ->
when {
!cursor.moveToFirst() -> log.e("updateRead[$accountDbId,$notificationType]: can't find the data row.")
else -> {
val nid_show = EntityId.from(cursor, COL_NID_SHOW)
val nid_read = EntityId.from(cursor, COL_NID_READ)
when {
nid_show == null ->
log.e("updateRead[$accountDbId,$notificationType]: nid_show is null.")
nid_read != null && nid_read >= nid_show ->
log.e("updateRead[$accountDbId,$notificationType]: nid_read already updated.")
else -> {
log.i("updateRead[$accountDbId,$notificationType]: update nid_read as $nid_show...")
val cv = ContentValues()
nid_show.putTo(cv, COL_NID_READ) //変数名とキー名が異なるのに注意
App1.database.update(table, cv, WHERE_AID, where_args)
clearCache(accountDbId, notificationType)
}
}
}
}
}
} catch (ex: Throwable) {
log.e(ex, "updateRead[$accountDbId] failed.")
}
}
fun resetPostAll() {
try {
val cv = ContentValues()
cv.putNull(COL_POST_ID)
cv.put(COL_POST_TIME, 0)
App1.database.update(table, cv, null, null)
cache.clear()
} catch (ex: Throwable) {
log.e(ex, "resetPostAll failed.")
}
}
// アカウント設定から手動で呼ばれる
fun resetTrackingState(accountDbId: Long?) {
accountDbId ?: return
try {
App1.database.delete(table, "$COL_ACCOUNT_DB_ID=?", arrayOf(accountDbId.toString()))
cache.remove(accountDbId)
} catch (ex: Throwable) {
log.e(ex, "resetTrackingState failed.")
}
}
}
}