2018-01-04 19:52:25 +01:00
|
|
|
package jp.juggler.subwaytooter.util
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
import android.os.Handler
|
|
|
|
import android.os.SystemClock
|
|
|
|
import jp.juggler.subwaytooter.App1
|
2023-02-07 13:49:45 +01:00
|
|
|
import jp.juggler.subwaytooter.api.entity.parseList
|
2021-05-19 22:14:30 +02:00
|
|
|
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
2020-05-16 16:59:18 +02:00
|
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
2023-01-13 13:22:25 +01:00
|
|
|
import jp.juggler.util.coroutine.launchMain
|
|
|
|
import jp.juggler.util.data.JsonObject
|
|
|
|
import jp.juggler.util.data.decodeJsonArray
|
|
|
|
import jp.juggler.util.data.decodeJsonObject
|
|
|
|
import jp.juggler.util.log.LogCategory
|
|
|
|
import jp.juggler.util.network.toRequestBody
|
2018-11-04 03:40:28 +01:00
|
|
|
import java.util.concurrent.ConcurrentHashMap
|
|
|
|
import java.util.concurrent.ConcurrentLinkedQueue
|
2022-05-29 15:38:21 +02:00
|
|
|
import kotlin.coroutines.Continuation
|
|
|
|
import kotlin.coroutines.resume
|
|
|
|
import kotlin.coroutines.resumeWithException
|
|
|
|
import kotlin.coroutines.suspendCoroutine
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2020-09-09 20:13:11 +02:00
|
|
|
class CustomEmojiLister(
|
2021-06-13 13:48:48 +02:00
|
|
|
val context: Context,
|
2021-06-20 15:12:25 +02:00
|
|
|
private val handler: Handler,
|
2020-09-09 20:13:11 +02:00
|
|
|
) {
|
2021-06-13 13:48:48 +02:00
|
|
|
companion object {
|
|
|
|
|
|
|
|
private val log = LogCategory("CustomEmojiLister")
|
|
|
|
|
|
|
|
internal const val CACHE_MAX = 50
|
|
|
|
|
|
|
|
internal const val ERROR_EXPIRE = 60000L * 5
|
|
|
|
|
|
|
|
private val elapsedTime: Long
|
|
|
|
get() = SystemClock.elapsedRealtime()
|
|
|
|
}
|
|
|
|
|
|
|
|
internal class CacheItem(
|
|
|
|
val key: String,
|
2022-05-29 15:38:21 +02:00
|
|
|
var list: List<CustomEmoji>,
|
|
|
|
var listWithAliases: List<CustomEmoji>,
|
2023-01-10 17:40:26 +01:00
|
|
|
var mapShortCode: Map<String, CustomEmoji>,
|
2021-06-13 13:48:48 +02:00
|
|
|
// ロードした時刻
|
2021-06-20 15:12:25 +02:00
|
|
|
var timeUpdate: Long = elapsedTime,
|
2021-06-13 13:48:48 +02:00
|
|
|
// 参照された時刻
|
2021-06-20 15:12:25 +02:00
|
|
|
var timeUsed: Long = timeUpdate,
|
2021-06-13 13:48:48 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
internal class Request(
|
2022-05-29 15:38:21 +02:00
|
|
|
val cont: Continuation<List<CustomEmoji>>,
|
2021-06-13 13:48:48 +02:00
|
|
|
val accessInfo: SavedAccount,
|
|
|
|
val reportWithAliases: Boolean = false,
|
2022-05-29 15:38:21 +02:00
|
|
|
) {
|
2022-06-12 04:49:54 +02:00
|
|
|
val caller = Throwable()
|
2022-05-29 15:38:21 +02:00
|
|
|
fun resume(item: CacheItem) {
|
|
|
|
cont.resume(
|
|
|
|
when (reportWithAliases) {
|
|
|
|
true -> item.listWithAliases
|
|
|
|
else -> item.list
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2021-06-13 13:48:48 +02:00
|
|
|
|
|
|
|
// 成功キャッシュ
|
|
|
|
internal val cache = ConcurrentHashMap<String, CacheItem>()
|
|
|
|
|
|
|
|
// エラーキャッシュ
|
2021-06-20 15:12:25 +02:00
|
|
|
internal val cacheError = ConcurrentHashMap<String, Long>()
|
2021-06-13 13:48:48 +02:00
|
|
|
|
2023-01-10 17:40:26 +01:00
|
|
|
private val cacheErrorItem = CacheItem(
|
|
|
|
key = "error",
|
|
|
|
list = emptyList(),
|
|
|
|
listWithAliases = emptyList(),
|
|
|
|
mapShortCode = emptyMap(),
|
|
|
|
)
|
2021-06-13 13:48:48 +02:00
|
|
|
|
|
|
|
// ロード要求
|
|
|
|
internal val queue = ConcurrentLinkedQueue<Request>()
|
|
|
|
|
2022-05-29 15:38:21 +02:00
|
|
|
private val worker = Worker()
|
2021-06-13 13:48:48 +02:00
|
|
|
|
|
|
|
// ネットワーク接続が変化したらエラーキャッシュをクリア
|
|
|
|
fun onNetworkChanged() {
|
2021-06-20 15:12:25 +02:00
|
|
|
cacheError.clear()
|
2021-06-13 13:48:48 +02:00
|
|
|
}
|
|
|
|
|
2023-01-10 17:40:26 +01:00
|
|
|
private fun getCached(now: Long, accessInfo: SavedAccount) =
|
|
|
|
getCached(now, accessInfo.apiHost.ascii)
|
|
|
|
|
|
|
|
private fun getCached(now: Long, apiHostAscii: String?): CacheItem? {
|
|
|
|
apiHostAscii ?: return null
|
2021-06-13 13:48:48 +02:00
|
|
|
|
|
|
|
// 成功キャッシュ
|
2023-01-10 17:40:26 +01:00
|
|
|
val item = cache[apiHostAscii]
|
2021-06-20 15:12:25 +02:00
|
|
|
if (item != null && now - item.timeUpdate <= ERROR_EXPIRE) {
|
|
|
|
item.timeUsed = now
|
2021-06-13 13:48:48 +02:00
|
|
|
return item
|
|
|
|
}
|
|
|
|
|
|
|
|
// エラーキャッシュ
|
2023-01-10 17:40:26 +01:00
|
|
|
val timeError = cacheError[apiHostAscii]
|
2021-06-20 15:12:25 +02:00
|
|
|
if (timeError != null && now < timeError + ERROR_EXPIRE) {
|
|
|
|
return cacheErrorItem
|
2021-06-13 13:48:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2022-05-29 15:38:21 +02:00
|
|
|
// インスタンス用のカスタム絵文字のリストを取得する
|
|
|
|
// または例外を投げる
|
|
|
|
suspend fun getList(
|
2021-06-13 13:48:48 +02:00
|
|
|
accessInfo: SavedAccount,
|
2022-05-29 15:38:21 +02:00
|
|
|
withAliases: Boolean = false,
|
|
|
|
): List<CustomEmoji> {
|
|
|
|
synchronized(cache) {
|
|
|
|
getCached(elapsedTime, accessInfo)
|
|
|
|
}?.let { return it.list }
|
|
|
|
return suspendCoroutine { cont ->
|
|
|
|
try {
|
|
|
|
queue.add(Request(cont, accessInfo, reportWithAliases = withAliases))
|
|
|
|
worker.notifyEx()
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
cont.resumeWithException(ex)
|
2021-06-13 13:48:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-12 04:49:54 +02:00
|
|
|
fun tryGetList(
|
2021-06-13 13:48:48 +02:00
|
|
|
accessInfo: SavedAccount,
|
2022-05-29 15:38:21 +02:00
|
|
|
withAliases: Boolean = false,
|
|
|
|
callback: ((List<CustomEmoji>) -> Unit)? = null,
|
|
|
|
): List<CustomEmoji>? {
|
|
|
|
synchronized(cache) {
|
|
|
|
getCached(elapsedTime, accessInfo)
|
|
|
|
}?.let { return it.list }
|
|
|
|
launchMain {
|
2022-06-12 04:49:54 +02:00
|
|
|
try {
|
|
|
|
getList(accessInfo, withAliases).let { callback?.invoke(it) }
|
2022-06-13 19:23:46 +02:00
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
log.e(ex, "getList failed.")
|
2022-06-12 04:49:54 +02:00
|
|
|
}
|
2021-06-13 13:48:48 +02:00
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2022-05-29 15:38:21 +02:00
|
|
|
// suspend fun getMap(accessInfo: SavedAccount) =
|
|
|
|
// HashMap<String, CustomEmoji>().apply {
|
|
|
|
// getList(accessInfo).forEach { put(it.shortcode, it) }
|
|
|
|
// }
|
|
|
|
|
2022-06-25 01:02:13 +02:00
|
|
|
fun getMapNonBlocking(accessInfo: SavedAccount): HashMap<String, CustomEmoji>? =
|
2022-06-12 04:49:54 +02:00
|
|
|
tryGetList(accessInfo)?.let {
|
2022-05-29 15:38:21 +02:00
|
|
|
HashMap<String, CustomEmoji>().apply {
|
|
|
|
it.forEach { put(it.shortcode, it) }
|
|
|
|
}
|
2021-06-13 13:48:48 +02:00
|
|
|
}
|
|
|
|
|
2023-01-18 05:59:35 +01:00
|
|
|
fun getCachedEmoji(apiHostAscii: String?, shortcode: String): CustomEmoji? {
|
|
|
|
val cache = getCached(elapsedTime, apiHostAscii)
|
|
|
|
if (cache == null) {
|
|
|
|
log.w("getCachedEmoji: missing cache for $apiHostAscii")
|
|
|
|
return null
|
|
|
|
}
|
2023-02-07 13:49:45 +01:00
|
|
|
val emoji = cache.mapShortCode[shortcode]
|
2023-01-18 05:59:35 +01:00
|
|
|
if (emoji == null) {
|
|
|
|
log.w("getCachedEmoji: missing emoji for $shortcode in $apiHostAscii")
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
return emoji
|
|
|
|
}
|
2023-01-10 17:40:26 +01:00
|
|
|
|
2021-06-13 13:48:48 +02:00
|
|
|
private inner class Worker : WorkerBase() {
|
|
|
|
|
|
|
|
override fun cancel() {
|
|
|
|
// このスレッドはキャンセルされない。プロセスが生きている限り動き続ける。
|
|
|
|
}
|
|
|
|
|
|
|
|
override suspend fun run() {
|
|
|
|
while (true) {
|
|
|
|
try {
|
|
|
|
// リクエストを取得する
|
|
|
|
val request = queue.poll()
|
|
|
|
if (request == null) {
|
|
|
|
// なければ待機
|
|
|
|
waitEx(86400000L)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
try {
|
2022-05-29 15:38:21 +02:00
|
|
|
request.resume(handleRequest(request))
|
2021-06-13 13:48:48 +02:00
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
log.e(request.caller, "caller's call stack")
|
2022-05-29 15:38:21 +02:00
|
|
|
request.cont.resumeWithException(ex)
|
2021-06-13 13:48:48 +02:00
|
|
|
}
|
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
log.e(ex, "can't load custom emoji list.")
|
2021-06-13 13:48:48 +02:00
|
|
|
waitEx(3000L)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-29 15:38:21 +02:00
|
|
|
private suspend fun handleRequest(request: Request): CacheItem {
|
|
|
|
synchronized(cache) {
|
|
|
|
(getCached(elapsedTime, request.accessInfo)
|
|
|
|
?.takeIf { it != cacheErrorItem })
|
|
|
|
.also {
|
|
|
|
if (it == null) {
|
|
|
|
// エラーキャッシュは一定時間で除去される
|
|
|
|
sweepCache()
|
|
|
|
}
|
2021-06-13 13:48:48 +02:00
|
|
|
}
|
2022-05-29 15:38:21 +02:00
|
|
|
}?.let { return it }
|
|
|
|
|
|
|
|
val accessInfo = request.accessInfo
|
|
|
|
val cacheKey = accessInfo.apiHost.ascii
|
2023-01-10 17:40:26 +01:00
|
|
|
|
|
|
|
// v12のmetaからemojisをパース
|
|
|
|
suspend fun misskeyEmojis12(): List<CustomEmoji>? =
|
2022-05-29 15:38:21 +02:00
|
|
|
App1.getHttpCachedString(
|
|
|
|
"https://$cacheKey/api/meta",
|
|
|
|
accessInfo = accessInfo
|
|
|
|
) { builder ->
|
|
|
|
builder.post(JsonObject().toRequestBody())
|
2023-01-10 17:40:26 +01:00
|
|
|
}?.decodeJsonObject()
|
|
|
|
?.jsonArray("emojis")
|
2023-02-08 10:55:49 +01:00
|
|
|
?.let { parseList(it, CustomEmoji::decodeMisskey) }
|
2023-01-10 17:40:26 +01:00
|
|
|
|
|
|
|
// v13のemojisを読む
|
|
|
|
suspend fun misskeyEmojis13(): List<CustomEmoji>? =
|
|
|
|
App1.getHttpCachedString(
|
|
|
|
"https://$cacheKey/api/emojis",
|
|
|
|
accessInfo = accessInfo,
|
|
|
|
misskeyPost = true,
|
|
|
|
) { builder ->
|
|
|
|
builder.post(JsonObject().toRequestBody())
|
2022-05-29 15:38:21 +02:00
|
|
|
}
|
2023-01-10 17:40:26 +01:00
|
|
|
?.decodeJsonObject()
|
|
|
|
?.jsonArray("emojis")
|
|
|
|
?.let { emojis13 ->
|
2023-02-07 13:49:45 +01:00
|
|
|
parseList(emojis13) {
|
2023-02-08 10:55:49 +01:00
|
|
|
CustomEmoji.decodeMisskey13(accessInfo.apiHost, it)
|
2023-02-07 13:49:45 +01:00
|
|
|
}
|
2023-01-10 17:40:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// マストドンのカスタム絵文字一覧を読む
|
|
|
|
suspend fun mastodonEmojis() =
|
2022-05-29 15:38:21 +02:00
|
|
|
App1.getHttpCachedString(
|
|
|
|
"https://$cacheKey/api/v1/custom_emojis",
|
|
|
|
accessInfo = accessInfo
|
2023-01-10 17:40:26 +01:00
|
|
|
)?.let { data ->
|
2023-02-08 10:55:49 +01:00
|
|
|
parseList(data.decodeJsonArray(), CustomEmoji::decodeMastodon)
|
2023-01-10 17:40:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
val list = when {
|
|
|
|
accessInfo.isMastodon -> mastodonEmojis()
|
|
|
|
else -> misskeyEmojis12() ?: misskeyEmojis13()
|
|
|
|
}?.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode })
|
|
|
|
|
|
|
|
val listWithAlias = list?.let { srcList ->
|
|
|
|
ArrayList<CustomEmoji>(srcList).apply {
|
|
|
|
for (item in srcList) {
|
|
|
|
item.aliases
|
|
|
|
?.filter { !it.equals(item.shortcode, ignoreCase = true) }
|
|
|
|
?.forEach { add(item.makeAlias(it)) }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}?.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode })
|
|
|
|
|
2022-05-29 15:38:21 +02:00
|
|
|
return synchronized(cache) {
|
|
|
|
val now = elapsedTime
|
|
|
|
if (list == null || listWithAlias == null) {
|
|
|
|
cacheError[cacheKey] = now
|
|
|
|
error("can't load custom emoji for ${accessInfo.apiHost}")
|
|
|
|
} else {
|
2023-01-10 17:40:26 +01:00
|
|
|
val mapShortCode = buildMap {
|
|
|
|
list.forEach { put(it.alias ?: it.shortcode, it) }
|
|
|
|
listWithAlias.forEach { put(it.alias ?: it.shortcode, it) }
|
|
|
|
}
|
2022-05-29 15:38:21 +02:00
|
|
|
var item = cache[cacheKey]
|
|
|
|
if (item == null) {
|
2023-01-10 17:40:26 +01:00
|
|
|
item = CacheItem(cacheKey, list, listWithAlias, mapShortCode)
|
2022-05-29 15:38:21 +02:00
|
|
|
cache[cacheKey] = item
|
|
|
|
} else {
|
|
|
|
item.list = list
|
|
|
|
item.listWithAliases = listWithAlias
|
2023-01-10 17:40:26 +01:00
|
|
|
item.mapShortCode = mapShortCode
|
2022-05-29 15:38:21 +02:00
|
|
|
item.timeUpdate = now
|
|
|
|
}
|
|
|
|
item
|
|
|
|
}
|
|
|
|
}
|
2021-06-13 13:48:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// キャッシュの掃除
|
2021-06-20 15:12:25 +02:00
|
|
|
private fun sweepCache() {
|
2021-06-13 13:48:48 +02:00
|
|
|
// 超過してる数
|
|
|
|
val over = cache.size - CACHE_MAX
|
|
|
|
if (over <= 0) return
|
|
|
|
|
|
|
|
// 古い要素を一時リストに集める
|
|
|
|
// 昇順ソート
|
|
|
|
// 古い物から順に捨てる
|
2023-01-18 05:59:35 +01:00
|
|
|
val now = elapsedTime
|
|
|
|
cache.entries
|
|
|
|
.filter { now - it.value.timeUsed > 1000L }
|
|
|
|
.sortedBy { it.value.timeUsed }
|
|
|
|
.take(over)
|
|
|
|
.forEach { cache.remove(it.key) }
|
2021-06-13 13:48:48 +02:00
|
|
|
}
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|