SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/notification/TaskRunner.kt

914 lines
38 KiB
Kotlin

package jp.juggler.subwaytooter.notification
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import jp.juggler.subwaytooter.ActCallback
import jp.juggler.subwaytooter.EventReceiver
import jp.juggler.subwaytooter.Pref
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.util.PushSubscriptionHelper
import jp.juggler.util.*
import kotlinx.coroutines.*
import okhttp3.Call
import java.lang.ref.WeakReference
import java.util.*
import kotlin.math.min
class TaskRunner(
private val pollingWorker: PollingWorker,
val job: JobItem,
private val taskId: TaskId,
private val taskData: JsonObject
) {
companion object {
private val log = LogCategory("TaskRunner")
private var workerStatus = ""
set(value) {
field = value
PollingWorker.workerStatus = value
}
}
val context = pollingWorker.context
val notification_manager = pollingWorker.notification_manager
val pref = pollingWorker.pref
val error_instance = ArrayList<String>()
suspend fun runTask() {
workerStatus = "start task $taskId"
coroutineScope {
try {
// タスクによってはポーリング前にすることがある
@Suppress("NON_EXHAUSTIVE_WHEN")
when (taskId) {
TaskId.BootCompleted ->
NotificationTracking.resetPostAll()
TaskId.PackageReplaced ->
NotificationTracking.resetPostAll()
TaskId.DataInjected ->
pollingWorker.processInjectedData(job.injectedAccounts)
TaskId.ResetTrackingState ->
NotificationTracking.resetTrackingState(taskData.long(PollingWorker.EXTRA_DB_ID))
// プッシュ通知が届いた
TaskId.FcmMessage -> {
var bDone = false
val tag = taskData.string(PollingWorker.EXTRA_TAG)
if (tag != null) {
if (tag.startsWith("acct<>")) {
val acct = tag.substring(6)
val sa = SavedAccount.loadAccountByAcct(context, acct)
if (sa != null) {
NotificationCache.resetLastLoad(sa.db_id)
job.injectedAccounts.add(sa.db_id)
bDone = true
}
}
if (!bDone) {
for (sa in SavedAccount.loadByTag(context, tag)) {
NotificationCache.resetLastLoad(sa.db_id)
job.injectedAccounts.add(sa.db_id)
bDone = true
}
}
}
if (!bDone) {
// タグにマッチする情報がなかった場合、全部読み直す
NotificationCache.resetLastLoad()
}
}
TaskId.Clear -> {
deleteCacheData(taskData.long(PollingWorker.EXTRA_DB_ID))
}
TaskId.NotificationDelete -> {
val db_id = taskData.long(PollingWorker.EXTRA_DB_ID)
val type =
TrackingType.parseStr(taskData.string(PollingWorker.EXTRA_NOTIFICATION_TYPE))
val typeName = type.typeName
val id = taskData.string(PollingWorker.EXTRA_NOTIFICATION_ID)
log.d("Notification deleted! db_id=$db_id,type=$type,id=$id")
if (db_id != null) {
NotificationTracking.updateRead(db_id, typeName)
}
return@coroutineScope
}
TaskId.NotificationClick -> {
val db_id = taskData.long(PollingWorker.EXTRA_DB_ID)
val type =
TrackingType.parseStr(taskData.string(PollingWorker.EXTRA_NOTIFICATION_TYPE))
val typeName = type.typeName
val id = taskData.string(PollingWorker.EXTRA_NOTIFICATION_ID).notEmpty()
log.d("Notification clicked! db_id=$db_id,type=$type,id=$id")
if (db_id != null) {
// 通知をキャンセル
val notification_tag = when (typeName) {
"" -> "${db_id}/_"
else -> "${db_id}/$typeName"
}
if (id != null) {
val itemTag = "$notification_tag/$id"
notification_manager.cancel(itemTag, PollingWorker.NOTIFICATION_ID)
} else {
notification_manager.cancel(
notification_tag,
PollingWorker.NOTIFICATION_ID
)
}
// DB更新処理
NotificationTracking.updateRead(db_id, typeName)
}
return@coroutineScope
}
}
// インストールIDを生成する
// インストールID生成時にSavedAccountテーブルを操作することがあるので
// アカウントリストの取得より先に行う
if (job.install_id == null) {
PollingWorker.workerStatus = "make install id"
job.install_id = PollingWorker.prepareInstallId(context, job)
}
// アカウント別に処理スレッドを作る
PollingWorker.workerStatus = "create account threads"
val thread_list = LinkedList<AccountRunner>()
suspend fun startForAccount(_a: SavedAccount) {
if (_a.isPseudo) return
thread_list.add(AccountRunner(_a).apply { start() })
}
if (job.injectedAccounts.isNotEmpty()) {
// 更新対象アカウントが限られているなら、そのdb_idだけ処理する
job.injectedAccounts.forEach { db_id ->
SavedAccount.loadAccount(context, db_id)?.let { startForAccount(it) }
}
} else {
// 全てのアカウントを処理する
SavedAccount.loadAccountList(context).forEach { startForAccount(it) }
}
while (true) {
// 同じホスト名が重複しないようにSetに集める
val liveSet = TreeSet<Host>()
for (t in thread_list) {
if (!t.isActive) continue
if (job.isJobCancelled) t.cancel()
liveSet.add(t.account.apiHost)
}
if (liveSet.isEmpty()) break
PollingWorker.workerStatus =
"waiting ${liveSet.joinToString(", ") { it.pretty }}"
delay(if (job.isJobCancelled) 100L else 1000L)
}
synchronized(error_instance) {
createErrorNotification(error_instance)
}
if (!job.isJobCancelled) job.bPollingComplete = true
} catch (ex: Throwable) {
log.trace(ex, "task execution failed.")
} finally {
log.d(")runTask: taskId=$taskId")
PollingWorker.workerStatus = "end task $taskId"
}
}
}
inner class AccountRunner(val account: SavedAccount) {
private var suspendJob: Job? = null
private lateinit var parser: TootParser
private lateinit var cache: NotificationCache
private var currentCall: WeakReference<Call>? = null
private var policyFilter: (TootNotification) -> Boolean = when (account.push_policy) {
"followed" -> { it ->
val who = it.account
when {
who == null -> true
account.isMe(who) -> true
else -> UserRelation.load(account.db_id, who.id).following
}
}
"follower" -> { it ->
val who = it.account
when {
it.type == TootNotification.TYPE_FOLLOW ||
it.type == TootNotification.TYPE_FOLLOW_REQUEST -> true
who == null -> true
account.isMe(who) -> true
else -> UserRelation.load(account.db_id, who.id).followed_by
}
}
"none" -> { _ -> false }
else -> { _ -> true }
}
///////////////////
val isActive: Boolean
get() = suspendJob?.isActive ?: true
private val onCallCreated: (Call) -> Unit =
{ currentCall = WeakReference(it) }
private val client = TootApiClient(context, callback = object : TootApiCallback {
override val isApiCancelled: Boolean
get() = job.isJobCancelled || (suspendJob?.isCancelled == true)
}).apply {
currentCallCallback = onCallCreated
}
private val favMuteSet: HashSet<Acct> get() = job.favMuteSet
fun cancel() {
try {
currentCall?.get()?.cancel()
} catch (ex: Throwable) {
log.trace(ex)
}
}
suspend fun start() {
coroutineScope {
this@AccountRunner.suspendJob = launch(Dispatchers.IO) {
runSuspend()
}
}
}
private val onError: (TootApiResult) -> Unit = { result ->
val sv = result.error
if (sv?.contains("Timeout") == true && !account.dont_show_timeout) {
synchronized(error_instance) {
if (!error_instance.any { it == sv }) error_instance.add(sv)
}
}
}
private suspend fun runSuspend() {
try {
// 疑似アカウントはチェック対象外
if (account.isPseudo) return
// 未確認アカウントはチェック対象外
if (!account.isConfirmed) return
log.d("${account.acct}: runSuspend start.")
client.account = account
val wps = PushSubscriptionHelper(context, account)
if (wps.flags != 0) {
job.bPollingRequired.set(true)
val (instance, instanceResult) = TootInstance.get(client)
if (instance == null) {
if (instanceResult != null) {
log.e("${instanceResult.error} ${instanceResult.requestInfo}".trim())
account.updateNotificationError("${instanceResult.error} ${instanceResult.requestInfo}".trim())
}
return
}
if (job.isJobCancelled) return
}
wps.updateSubscription(client) ?: return // cancelled.
val wps_log = wps.logString
if (wps_log.isNotEmpty())
log.d("PushSubscriptionHelper: ${account.acct.pretty} $wps_log")
if (job.isJobCancelled) return
if (wps.flags == 0) {
if (account.last_notification_error != null) {
account.updateNotificationError(null)
}
return
}
this.cache = NotificationCache(account.db_id).apply {
load()
requestAsync(
client,
account,
wps.flags,
onError = onError,
isCancelled = { job.isJobCancelled }
)
}
if (job.isJobCancelled) return
this.parser = TootParser(context, account)
if (Pref.bpSeparateReplyNotificationGroup(pref)) {
var tr = TrackingRunner(
trackingType = TrackingType.NotReply,
trackingName = NotificationHelper.TRACKING_NAME_DEFAULT
)
tr.checkAccount()
if (job.isJobCancelled) return
tr.updateNotification()
//
tr = TrackingRunner(
trackingType = TrackingType.Reply,
trackingName = NotificationHelper.TRACKING_NAME_REPLY
)
tr.checkAccount()
if (job.isJobCancelled) return
tr.updateNotification()
} else {
val tr = TrackingRunner(
trackingType = TrackingType.All,
trackingName = NotificationHelper.TRACKING_NAME_DEFAULT
)
tr.checkAccount()
if (job.isJobCancelled) return
tr.updateNotification()
}
log.i("runSuspend complete normally.")
} catch (ex: Throwable) {
log.trace(ex)
} finally {
pollingWorker.notifyWorker()
}
}
inner class TrackingRunner(
var trackingType: TrackingType = TrackingType.All,
var trackingName: String = ""
) {
private lateinit var nr: NotificationTracking
private val duplicate_check = HashSet<EntityId>()
private val dstListData = LinkedList<NotificationData>()
fun checkAccount() {
this.nr =
NotificationTracking.load(account.acct.pretty, account.db_id, trackingName)
fun JsonObject.isMention() =
when (NotificationCache.parseNotificationType(account, this)) {
TootNotification.TYPE_REPLY, TootNotification.TYPE_MENTION -> true
else -> false
}
val jsonList = when (trackingType) {
TrackingType.All -> cache.data
TrackingType.Reply -> cache.data.filter { it.isMention() }
TrackingType.NotReply -> cache.data.filter { !it.isMention() }
}
// 新しい順に並んでいる。先頭から10件までを処理する。ただし処理順序は古い方から
val size = min(10, jsonList.size)
for (i in (0 until size).reversed()) {
if (job.isJobCancelled) return
update_sub(jsonList[i])
}
if (job.isJobCancelled) return
// 種別チェックより先に、cache中の最新のIDを「最後に表示した通知」に指定する
// nid_show は通知タップ時に参照されるので、通知を表示する際は必ず更新・保存する必要がある
// 種別チェックより優先する
val latestId = cache.filterLatestId(account) {
when (trackingType) {
TrackingType.Reply -> it.isMention()
TrackingType.NotReply -> !it.isMention()
else -> true
}
}
if (latestId != null) nr.nid_show = latestId
nr.save(account.acct.pretty)
}
private fun update_sub(src: JsonObject) {
val id = NotificationCache.getEntityOrderId(account, src)
if (id.isDefault || duplicate_check.contains(id)) return
duplicate_check.add(id)
// タップ・削除した通知のIDと同じか古いなら対象外
if (!id.isNewerThan(nr.nid_read)) {
log.d("update_sub: ${account.acct} skip old notification $id")
return
}
log.d("update_sub: found data that id=${id}, > read id ${nr.nid_read}")
val notification = parser.notification(src) ?: return
// アプリミュートと単語ミュート
if (notification.status?.checkMuted() == true) return
// ふぁぼ魔ミュート
when (notification.type) {
TootNotification.TYPE_REBLOG,
TootNotification.TYPE_FAVOURITE,
TootNotification.TYPE_FOLLOW,
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY -> {
val who = notification.account
if (who != null && favMuteSet.contains(account.getFullAcct(who))) {
log.d("${account.getFullAcct(who)} is in favMuteSet.")
return
}
}
}
// Mastodon 3.4.0rc1 push policy
if (!policyFilter(notification)) return
// 後から処理したものが先頭に来る
dstListData.add(0, NotificationData(account, notification))
}
fun updateNotification() {
val notification_tag = when (trackingName) {
"" -> "${account.db_id}/_"
else -> "${account.db_id}/$trackingName"
}
val nt = NotificationTracking.load(account.acct.pretty, account.db_id, trackingName)
val dataList = dstListData
val first = dataList.firstOrNull()
if (first == null) {
log.d("showNotification[${account.acct.pretty}/$notification_tag] cancel notification.")
if (Build.VERSION.SDK_INT >= 23 && Pref.bpDivideNotification(pref)) {
notification_manager.activeNotifications?.forEach {
if (it != null &&
it.id == PollingWorker.NOTIFICATION_ID &&
it.tag.startsWith("$notification_tag/")
) {
log.d("cancel: ${it.tag} context=${account.acct.pretty} $notification_tag")
notification_manager.cancel(it.tag, PollingWorker.NOTIFICATION_ID)
}
}
} else {
notification_manager.cancel(notification_tag, PollingWorker.NOTIFICATION_ID)
}
return
}
val lastPostTime = nt.post_time
val lastPostId = nt.post_id
if (first.notification.time_created_at == lastPostTime
&& first.notification.id == lastPostId
) {
// 先頭にあるデータが同じなら、通知を更新しない
// このマーカーは端末再起動時にリセットされるので、再起動後は通知が出るはず
log.d("showNotification[${account.acct.pretty}] id=${first.notification.id} is already shown.")
return
}
if (Build.VERSION.SDK_INT >= 23 && Pref.bpDivideNotification(pref)) {
val activeNotificationMap = HashMap<String, StatusBarNotification>().apply {
notification_manager.activeNotifications?.forEach {
if (it != null &&
it.id == PollingWorker.NOTIFICATION_ID &&
it.tag.startsWith("$notification_tag/")
) {
put(it.tag, it)
}
}
}
for (item in dstListData.reversed()) {
val itemTag = "$notification_tag/${item.notification.id}"
if (lastPostId != null &&
item.notification.time_created_at <= lastPostTime &&
item.notification.id <= lastPostId
) {
// 掲載済みデータより古い通知は再表示しない
log.d("ignore $itemTag ${item.notification.time_created_at} <= $lastPostTime && ${item.notification.id} <= $lastPostId")
continue
}
// ignore if already showing
if (activeNotificationMap.remove(itemTag) != null) {
log.d("ignore $itemTag is in activeNotificationMap")
continue
}
createNotification(
itemTag,
notificationId = item.notification.id.toString()
) { builder ->
builder.setWhen(item.notification.time_created_at)
val summary = item.getNotificationLine()
builder.setContentTitle(summary)
val content = item.notification.status?.decoded_content?.notEmpty()
if (content != null) {
builder.setStyle(
NotificationCompat.BigTextStyle()
.setBigContentTitle(summary)
.setSummaryText(item.access_info.acct.pretty)
.bigText(content)
)
} else {
builder.setContentText(item.access_info.acct.pretty)
}
if (Build.VERSION.SDK_INT < 26) {
var iv = 0
if (Pref.bpNotificationSound(pref)) {
var sound_uri: Uri? = null
try {
val whoAcct =
account.getFullAcct(item.notification.account)
sound_uri =
AcctColor.getNotificationSound(whoAcct).mayUri()
} catch (ex: Throwable) {
log.trace(ex)
}
if (sound_uri == null) {
sound_uri = account.sound_uri.mayUri()
}
var bSoundSet = false
if (sound_uri != null) {
try {
builder.setSound(sound_uri)
bSoundSet = true
} catch (ex: Throwable) {
log.trace(ex)
}
}
if (!bSoundSet) {
iv = iv or NotificationCompat.DEFAULT_SOUND
}
}
if (Pref.bpNotificationVibration(pref)) {
iv = iv or NotificationCompat.DEFAULT_VIBRATE
}
if (Pref.bpNotificationLED(pref)) {
iv = iv or NotificationCompat.DEFAULT_LIGHTS
}
builder.setDefaults(iv)
}
}
}
// リストにない通知は消さない。ある通知をユーザが指で削除した際に他の通知が残ってほしい場合がある
} else {
log.d("showNotification[${account.acct.pretty}] creating notification(1)")
createNotification(notification_tag) { builder ->
builder.setWhen(first.notification.time_created_at)
var a = first.getNotificationLine()
if (dataList.size == 1) {
builder.setContentTitle(a)
builder.setContentText(account.acct.pretty)
} else {
val header =
context.getString(R.string.notification_count, dataList.size)
builder.setContentTitle(header)
.setContentText(a)
val style = NotificationCompat.InboxStyle()
.setBigContentTitle(header)
.setSummaryText(account.acct.pretty)
for (i in 0..4) {
if (i >= dataList.size) break
val item = dataList[i]
a = item.getNotificationLine()
style.addLine(a)
}
builder.setStyle(style)
}
if (Build.VERSION.SDK_INT < 26) {
var iv = 0
if (Pref.bpNotificationSound(pref)) {
var sound_uri: Uri? = null
try {
val whoAcct =
account.getFullAcct(first.notification.account)
sound_uri = AcctColor.getNotificationSound(whoAcct).mayUri()
} catch (ex: Throwable) {
log.trace(ex)
}
if (sound_uri == null) {
sound_uri = account.sound_uri.mayUri()
}
var bSoundSet = false
if (sound_uri != null) {
try {
builder.setSound(sound_uri)
bSoundSet = true
} catch (ex: Throwable) {
log.trace(ex)
}
}
if (!bSoundSet) {
iv = iv or NotificationCompat.DEFAULT_SOUND
}
}
if (Pref.bpNotificationVibration(pref)) {
iv = iv or NotificationCompat.DEFAULT_VIBRATE
}
if (Pref.bpNotificationLED(pref)) {
iv = iv or NotificationCompat.DEFAULT_LIGHTS
}
builder.setDefaults(iv)
}
}
}
nt.updatePost(first.notification.id, first.notification.time_created_at)
}
private fun createNotification(
notification_tag: String,
notificationId: String? = null,
setContent: (builder: NotificationCompat.Builder) -> Unit
) {
log.d("showNotification[${account.acct.pretty}] creating notification(1)")
val builder = if (Build.VERSION.SDK_INT >= 26) {
// Android 8 から、通知のスタイルはユーザが管理することになった
// NotificationChannel を端末に登録しておけば、チャネルごとに管理画面が作られる
val channel = NotificationHelper.createNotificationChannel(
context,
account,
trackingName
)
NotificationCompat.Builder(context, channel.id)
} else {
NotificationCompat.Builder(context, "not_used")
}
builder.apply {
val params = listOf(
"db_id" to account.db_id.toString(),
"type" to trackingType.str,
"notificationId" to notificationId
).mapNotNull {
val second = it.second
if (second == null) {
null
} else {
"${it.first.encodePercent()}=${second.encodePercent()}"
}
}.joinToString("&")
setContentIntent(
PendingIntent.getActivity(
context,
257,
Intent(context, ActCallback::class.java).apply {
data =
"subwaytooter://notification_click/?$params".toUri()
// FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
},
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0)
)
)
setDeleteIntent(
PendingIntent.getBroadcast(
context,
257,
Intent(context, EventReceiver::class.java).apply {
action = EventReceiver.ACTION_NOTIFICATION_DELETE
data =
"subwaytooter://notification_delete/?$params".toUri()
},
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0)
)
)
setAutoCancel(true)
// 常に白テーマのアイコンを使う
setSmallIcon(R.drawable.ic_notification)
// 常に白テーマの色を使う
builder.color = ContextCompat.getColor(context, R.color.Light_colorAccent)
// Android 7.0 ではグループを指定しないと勝手に通知が束ねられてしまう。
// 束ねられた通知をタップしても pi_click が実行されないので困るため、
// アカウント別にグループキーを設定する
setGroup(context.packageName + ":" + account.acct.ascii)
}
log.d("showNotification[${account.acct.pretty}] creating notification(3)")
setContent(builder)
log.d("showNotification[${account.acct.pretty}] set notification...")
notification_manager.notify(
notification_tag,
PollingWorker.NOTIFICATION_ID,
builder.build()
)
}
}
}
private fun createErrorNotification(error_instance: ArrayList<String>) {
if (error_instance.isEmpty())
return
// 通知タップ時のPendingIntent
val intent_click = Intent(context, ActCallback::class.java)
// FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない
intent_click.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pi_click = PendingIntent.getActivity(
context,
3,
intent_click,
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0)
)
val builder = if (Build.VERSION.SDK_INT >= 26) {
// Android 8 から、通知のスタイルはユーザが管理することになった
// NotificationChannel を端末に登録しておけば、チャネルごとに管理画面が作られる
val channel = NotificationHelper.createNotificationChannel(
context,
"ErrorNotification",
"Error",
null,
2 /* NotificationManager.IMPORTANCE_LOW */
)
NotificationCompat.Builder(context, channel.id)
} else {
NotificationCompat.Builder(context, "not_used")
}
builder
.setContentIntent(pi_click)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_notification) // ここは常に白テーマのアイコンを使う
.setColor(
ContextCompat.getColor(
context,
R.color.Light_colorAccent
)
) // ここは常に白テーマの色を使う
.setWhen(System.currentTimeMillis())
.setGroup(context.packageName + ":" + "Error")
val header = context.getString(R.string.error_notification_title)
val summary = context.getString(R.string.error_notification_summary)
builder
.setContentTitle(header)
.setContentText(summary + ": " + error_instance[0])
val style = NotificationCompat.InboxStyle()
.setBigContentTitle(header)
.setSummaryText(summary)
for (i in 0..4) {
if (i >= error_instance.size) break
style.addLine(error_instance[i])
}
builder.setStyle(style)
notification_manager.notify(PollingWorker.NOTIFICATION_ID_ERROR, builder.build())
}
private fun NotificationData.getNotificationLine(): String {
val name = when (Pref.bpShowAcctInSystemNotification(pref)) {
false -> notification.accountRef?.decoded_display_name
true -> {
val acctPretty = notification.accountRef?.get()?.acct?.pretty
if (acctPretty?.isNotEmpty() == true) {
"@$acctPretty"
} else {
null
}
}
} ?: "?"
return "- " + when (notification.type) {
TootNotification.TYPE_MENTION,
TootNotification.TYPE_REPLY ->
context.getString(R.string.display_name_replied_by, name)
TootNotification.TYPE_RENOTE,
TootNotification.TYPE_REBLOG ->
context.getString(R.string.display_name_boosted_by, name)
TootNotification.TYPE_QUOTE ->
context.getString(R.string.display_name_quoted_by, name)
TootNotification.TYPE_STATUS ->
context.getString(R.string.display_name_posted_by, name)
TootNotification.TYPE_FOLLOW ->
context.getString(R.string.display_name_followed_by, name)
TootNotification.TYPE_UNFOLLOW ->
context.getString(R.string.display_name_unfollowed_by, name)
TootNotification.TYPE_FAVOURITE ->
context.getString(R.string.display_name_favourited_by, name)
TootNotification.TYPE_REACTION ->
context.getString(R.string.display_name_reaction_by, name)
TootNotification.TYPE_VOTE,
TootNotification.TYPE_POLL_VOTE_MISSKEY ->
context.getString(R.string.display_name_voted_by, name)
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY ->
context.getString(R.string.display_name_follow_request_by, name)
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY ->
context.getString(R.string.display_name_follow_request_accepted_by, name)
TootNotification.TYPE_POLL ->
context.getString(R.string.end_of_polling_from, name)
else -> "?"
}
}
private fun deleteCacheData(db_id: Long?) {
if (db_id != null) {
log.d("Notification clear! db_id=$db_id")
SavedAccount.loadAccount(context, db_id) ?: return
NotificationCache.deleteCache(db_id)
}
}
}