2019-10-22 20:21:03 +02:00
|
|
|
|
package jp.juggler.subwaytooter.table
|
|
|
|
|
|
|
|
|
|
import android.content.ContentValues
|
|
|
|
|
import android.database.sqlite.SQLiteDatabase
|
|
|
|
|
import android.provider.BaseColumns
|
|
|
|
|
import jp.juggler.subwaytooter.App1
|
2021-05-17 16:13:04 +02:00
|
|
|
|
import jp.juggler.subwaytooter.api.ApiPath
|
2019-10-22 20:21:03 +02:00
|
|
|
|
import jp.juggler.subwaytooter.api.TootApiClient
|
|
|
|
|
import jp.juggler.subwaytooter.api.TootApiResult
|
|
|
|
|
import jp.juggler.subwaytooter.api.entity.EntityId
|
|
|
|
|
import jp.juggler.subwaytooter.api.entity.TootNotification
|
|
|
|
|
import jp.juggler.subwaytooter.api.entity.TootStatus
|
|
|
|
|
import jp.juggler.util.*
|
|
|
|
|
import java.util.*
|
|
|
|
|
import kotlin.collections.ArrayList
|
|
|
|
|
|
2020-12-11 00:25:55 +01:00
|
|
|
|
class NotificationCache(private val account_db_id: Long) {
|
|
|
|
|
|
|
|
|
|
private var id = -1L
|
|
|
|
|
|
|
|
|
|
// サーバから通知を取得した時刻
|
|
|
|
|
private var last_load: Long = 0
|
|
|
|
|
|
|
|
|
|
// 通知のリスト
|
|
|
|
|
var data = ArrayList<JsonObject>()
|
|
|
|
|
|
|
|
|
|
companion object : TableCompanion {
|
|
|
|
|
|
|
|
|
|
private val log = LogCategory("NotificationCache")
|
|
|
|
|
|
2021-10-28 01:37:39 +02:00
|
|
|
|
override val table = "noti_cache"
|
2020-12-11 00:25:55 +01:00
|
|
|
|
|
|
|
|
|
private const val COL_ID = BaseColumns._ID
|
|
|
|
|
|
|
|
|
|
// アカウントDBの行ID。 サーバ側のIDではない
|
|
|
|
|
private const val COL_ACCOUNT_DB_ID = "a"
|
|
|
|
|
|
|
|
|
|
// サーバから通知を取得した時刻
|
|
|
|
|
private const val COL_LAST_LOAD = "l"
|
|
|
|
|
|
|
|
|
|
// サーバから最後に読んだデータ。既読は排除されてるかも
|
|
|
|
|
private const val COL_DATA = "d"
|
|
|
|
|
|
|
|
|
|
// サーバから最後に読んだデータ。既読は排除されてるかも
|
|
|
|
|
private const val COL_SINCE_ID = "si" // 使わなくなった
|
|
|
|
|
|
|
|
|
|
override fun onDBCreate(db: SQLiteDatabase) {
|
|
|
|
|
|
|
|
|
|
db.execSQL(
|
2021-06-20 15:12:25 +02:00
|
|
|
|
"""
|
2019-10-22 20:21:03 +02:00
|
|
|
|
create table if not exists $table
|
|
|
|
|
($COL_ID INTEGER PRIMARY KEY
|
|
|
|
|
,$COL_ACCOUNT_DB_ID integer not null
|
|
|
|
|
,$COL_LAST_LOAD integer default 0
|
|
|
|
|
,$COL_DATA text
|
|
|
|
|
,$COL_SINCE_ID text
|
|
|
|
|
)
|
|
|
|
|
"""
|
2021-06-20 15:12:25 +02:00
|
|
|
|
)
|
2020-12-11 00:25:55 +01:00
|
|
|
|
db.execSQL(
|
2021-06-20 15:12:25 +02:00
|
|
|
|
"create unique index if not exists ${table}_a on $table ($COL_ACCOUNT_DB_ID)"
|
|
|
|
|
)
|
2020-12-11 00:25:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onDBUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
|
|
|
|
if (oldVersion < 41 && newVersion >= 41) {
|
|
|
|
|
onDBCreate(db)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private const val WHERE_AID = "$COL_ACCOUNT_DB_ID=?"
|
|
|
|
|
|
|
|
|
|
private const val KEY_TIME_CREATED_AT = "<>KEY_TIME_CREATED_AT"
|
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
fun resetLastLoad(dbId: Long) {
|
2020-12-11 00:25:55 +01:00
|
|
|
|
try {
|
|
|
|
|
val cv = ContentValues()
|
|
|
|
|
cv.put(COL_LAST_LOAD, 0L)
|
2021-06-20 15:12:25 +02:00
|
|
|
|
App1.database.update(table, cv, WHERE_AID, arrayOf(dbId.toString()))
|
2020-12-11 00:25:55 +01:00
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.e(ex, "resetLastLoad(db_id) failed.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun resetLastLoad() {
|
|
|
|
|
try {
|
|
|
|
|
val cv = ContentValues()
|
|
|
|
|
cv.put(COL_LAST_LOAD, 0L)
|
|
|
|
|
App1.database.update(table, cv, null, null)
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.e(ex, "resetLastLoad() failed.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getEntityOrderId(account: SavedAccount, src: JsonObject): EntityId =
|
|
|
|
|
if (account.isMisskey) {
|
|
|
|
|
// 今のMisskeyはIDをIDとして使っても問題ないのだが、
|
|
|
|
|
// ST的には既読チェックの値の内容が大幅に変わると困るのだった
|
|
|
|
|
when (val created_at = src.string("createdAt")) {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
null -> EntityId.DEFAULT
|
2020-12-11 00:25:55 +01:00
|
|
|
|
else -> EntityId(TootStatus.parseTime(created_at).toString())
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
EntityId.mayDefault(src.string("id"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun makeNotificationUrl(
|
2021-06-20 15:12:25 +02:00
|
|
|
|
accessInfo: SavedAccount,
|
|
|
|
|
flags: Int,
|
|
|
|
|
sinceId: EntityId?,
|
|
|
|
|
) = when {
|
2020-12-11 00:25:55 +01:00
|
|
|
|
// MisskeyはsinceIdを指定すると未読範囲の古い方から読んでしまう?
|
|
|
|
|
accessInfo.isMisskey -> "/api/i/notifications"
|
|
|
|
|
|
|
|
|
|
else -> {
|
2021-05-17 16:13:04 +02:00
|
|
|
|
val sb = StringBuilder(ApiPath.PATH_NOTIFICATIONS) // always contain "?limit=XX"
|
2020-12-11 00:25:55 +01:00
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
if (sinceId != null) sb.append("&since_id=$sinceId")
|
2020-12-11 00:25:55 +01:00
|
|
|
|
|
|
|
|
|
fun noBit(v: Int, mask: Int) = (v and mask) != mask
|
|
|
|
|
|
|
|
|
|
if (noBit(flags, 1)) sb.append("&exclude_types[]=reblog")
|
|
|
|
|
if (noBit(flags, 2)) sb.append("&exclude_types[]=favourite")
|
|
|
|
|
if (noBit(flags, 4)) sb.append("&exclude_types[]=follow")
|
|
|
|
|
if (noBit(flags, 8)) sb.append("&exclude_types[]=mention")
|
|
|
|
|
// if(noBit(flags,16)) /* mastodon has no reaction */
|
|
|
|
|
if (noBit(flags, 32)) sb.append("&exclude_types[]=poll")
|
|
|
|
|
|
|
|
|
|
sb.toString()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun parseNotificationTime(accessInfo: SavedAccount, src: JsonObject): Long =
|
|
|
|
|
when {
|
|
|
|
|
accessInfo.isMisskey -> TootStatus.parseTime(src.string("createdAt"))
|
|
|
|
|
else -> TootStatus.parseTime(src.string("created_at"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun parseNotificationType(accessInfo: SavedAccount, src: JsonObject): String =
|
|
|
|
|
when {
|
|
|
|
|
accessInfo.isMisskey -> src.string("type")
|
|
|
|
|
else -> src.string("type")
|
|
|
|
|
} ?: "?"
|
|
|
|
|
|
|
|
|
|
fun deleteCache(dbId: Long) {
|
|
|
|
|
try {
|
|
|
|
|
val cv = ContentValues()
|
|
|
|
|
cv.put(COL_ACCOUNT_DB_ID, dbId)
|
|
|
|
|
cv.put(COL_LAST_LOAD, 0L)
|
|
|
|
|
cv.putNull(COL_DATA)
|
|
|
|
|
App1.database.replaceOrThrow(table, null, cv)
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.e(ex, "deleteCache failed.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// load into this object
|
|
|
|
|
fun load() {
|
|
|
|
|
try {
|
|
|
|
|
App1.database.query(
|
2021-06-20 15:12:25 +02:00
|
|
|
|
table,
|
|
|
|
|
null,
|
|
|
|
|
WHERE_AID,
|
|
|
|
|
arrayOf(account_db_id.toString()),
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null
|
|
|
|
|
)?.use { cursor ->
|
2020-12-11 00:25:55 +01:00
|
|
|
|
if (cursor.moveToFirst()) {
|
|
|
|
|
this.id = cursor.getLong(COL_ID)
|
|
|
|
|
this.last_load = cursor.getLong(COL_LAST_LOAD)
|
|
|
|
|
|
|
|
|
|
cursor.getStringOrNull(COL_DATA)?.decodeJsonArray()?.objectList()?.let {
|
|
|
|
|
data.addAll(it)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.id = -1
|
|
|
|
|
this.last_load = 0L
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.trace(ex, "load failed.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun save() {
|
|
|
|
|
try {
|
|
|
|
|
val cv = ContentValues()
|
|
|
|
|
cv.put(COL_ACCOUNT_DB_ID, account_db_id)
|
|
|
|
|
cv.put(COL_LAST_LOAD, last_load)
|
|
|
|
|
cv.put(COL_DATA, data.toJsonArray().toString())
|
|
|
|
|
|
|
|
|
|
val rv = App1.database.replaceOrThrow(table, null, cv)
|
|
|
|
|
if (rv != -1L && id == -1L) id = rv
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.e(ex, "save failed.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun normalize(account: SavedAccount) {
|
|
|
|
|
|
|
|
|
|
// 新しい順に並べる
|
|
|
|
|
data.sortWith { a, b ->
|
|
|
|
|
val la = a.optLong(KEY_TIME_CREATED_AT)
|
|
|
|
|
val lb = b.optLong(KEY_TIME_CREATED_AT)
|
|
|
|
|
when {
|
|
|
|
|
la < lb -> 1
|
|
|
|
|
la > lb -> -1
|
|
|
|
|
else -> 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val typeCount = HashMap<String, Int>()
|
|
|
|
|
val it = data.iterator()
|
|
|
|
|
val duplicateMap = HashSet<EntityId>()
|
|
|
|
|
while (it.hasNext()) {
|
|
|
|
|
val item = it.next()
|
|
|
|
|
|
|
|
|
|
val id = getEntityOrderId(account, item)
|
|
|
|
|
if (id.isDefault) {
|
|
|
|
|
it.remove()
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// skip duplicated
|
|
|
|
|
if (duplicateMap.contains(id)) {
|
|
|
|
|
it.remove()
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
duplicateMap.add(id)
|
|
|
|
|
|
|
|
|
|
val type = parseNotificationType(account, item)
|
|
|
|
|
|
|
|
|
|
// 通知しないタイプなら取り除く
|
|
|
|
|
if (!account.canNotificationShowing(type)) {
|
|
|
|
|
it.remove()
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 種類別に一定件数を保持する
|
|
|
|
|
val count = 1 + (typeCount[type] ?: 0)
|
|
|
|
|
if (count > 60) {
|
|
|
|
|
it.remove()
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
typeCount[type] = count
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun requestAsync(
|
2021-06-20 15:12:25 +02:00
|
|
|
|
client: TootApiClient,
|
|
|
|
|
account: SavedAccount,
|
|
|
|
|
flags: Int,
|
|
|
|
|
onError: (TootApiResult) -> Unit,
|
|
|
|
|
isCancelled: () -> Boolean,
|
|
|
|
|
) {
|
2020-12-11 00:25:55 +01:00
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
|
|
|
|
|
// 前回の更新から一定時刻が経過するまでは処理しない
|
|
|
|
|
val remain = last_load + 120000L - now
|
|
|
|
|
if (remain > 0) {
|
|
|
|
|
log.d("${account.acct} skip request. wait ${remain}ms.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.last_load = now
|
|
|
|
|
|
|
|
|
|
// キャッシュ更新時は全データの最新データより新しいものを読みたい
|
|
|
|
|
val newestId = data
|
|
|
|
|
.mapNotNull { getEntityOrderId(account, it).takeIf { id -> !id.isDefault } }
|
|
|
|
|
.reduceOrNull { a, b -> maxComparable(a, b) }
|
|
|
|
|
|
|
|
|
|
val path = makeNotificationUrl(account, flags, newestId)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
for (nTry in 0..3) {
|
|
|
|
|
|
|
|
|
|
if (isCancelled()) {
|
|
|
|
|
log.d("cancelled.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val result = if (account.isMisskey) {
|
|
|
|
|
client.request(path, account.putMisskeyApiToken().toPostRequestBuilder())
|
|
|
|
|
} else {
|
|
|
|
|
client.request(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result == null) {
|
|
|
|
|
log.d("cancelled.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val array = result.jsonArray
|
|
|
|
|
if (array != null) {
|
|
|
|
|
account.updateNotificationError(null)
|
|
|
|
|
|
|
|
|
|
// データをマージする
|
|
|
|
|
array.objectList().forEach { item ->
|
|
|
|
|
item[KEY_TIME_CREATED_AT] = parseNotificationTime(account, item)
|
|
|
|
|
data.add(item)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
normalize(account)
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.d("request error. ${result.error} ${result.requestInfo}")
|
|
|
|
|
|
|
|
|
|
account.updateNotificationError("${result.error} ${result.requestInfo}".trim())
|
|
|
|
|
|
|
|
|
|
onError(result)
|
|
|
|
|
|
|
|
|
|
// サーバからエラー応答が届いているならリトライしない
|
|
|
|
|
val code = result.response?.code
|
|
|
|
|
if (code != null && code in 200 until 600) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.trace(ex, "request failed.")
|
|
|
|
|
} finally {
|
|
|
|
|
save()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline fun filterLatestId(account: SavedAccount, predicate: (JsonObject) -> Boolean) =
|
|
|
|
|
data
|
|
|
|
|
.filter { predicate(it) }
|
2021-06-20 15:12:25 +02:00
|
|
|
|
.mapNotNull { getEntityOrderId(account, it).takeIf { id -> !id.isDefault } }
|
2020-12-11 00:25:55 +01:00
|
|
|
|
.reduceOrNull { a, b -> maxComparable(a, b) }
|
|
|
|
|
|
|
|
|
|
fun inject(account: SavedAccount, list: List<TootNotification>) {
|
|
|
|
|
try {
|
|
|
|
|
val jsonList = list.map { it.json }
|
|
|
|
|
jsonList.forEach { item ->
|
|
|
|
|
item[KEY_TIME_CREATED_AT] = parseNotificationTime(account, item)
|
|
|
|
|
}
|
|
|
|
|
data.addAll(jsonList)
|
|
|
|
|
normalize(account)
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.trace(ex, "inject failed.")
|
|
|
|
|
} finally {
|
|
|
|
|
save()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
// fun updatePost(post_id : EntityId, post_time : Long) {
|
|
|
|
|
// this.post_id = post_id
|
|
|
|
|
// this.post_time = post_time
|
|
|
|
|
// try {
|
|
|
|
|
// val cv = ContentValues()
|
|
|
|
|
// post_id.putTo(cv, COL_POST_ID)
|
|
|
|
|
// cv.put(COL_POST_TIME, post_time)
|
|
|
|
|
// val rows = App1.database.update(table, cv, WHERE_AID, arrayOf(account_db_id.toString()))
|
|
|
|
|
// log.d(
|
|
|
|
|
// "updatePost account_db_id=%s,post=%s,%s last_data=%s,update_rows=%s"
|
|
|
|
|
// , account_db_id
|
|
|
|
|
// , post_id
|
|
|
|
|
// , post_time
|
|
|
|
|
// , last_data?.length
|
|
|
|
|
// , rows
|
|
|
|
|
// )
|
|
|
|
|
//
|
|
|
|
|
// } catch(ex : Throwable) {
|
|
|
|
|
// log.e(ex, "updatePost failed.")
|
|
|
|
|
// }
|
|
|
|
|
//
|
|
|
|
|
// }
|
2019-10-22 20:21:03 +02:00
|
|
|
|
}
|