SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt

307 lines
7.8 KiB
Kotlin

package jp.juggler.subwaytooter.util
import android.content.Context
import android.os.Handler
import android.os.SystemClock
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.api.entity.CustomEmoji
import jp.juggler.subwaytooter.api.entity.parseList
import jp.juggler.util.LogCategory
import jp.juggler.util.toJsonArray
import jp.juggler.util.toRequestBody
import org.json.JSONObject
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
class CustomEmojiLister(internal val context : Context) {
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 instance : String,
var list : ArrayList<CustomEmoji>? = null,
var listWithAliases : ArrayList<CustomEmoji>? = null,
// ロードした時刻
var time_update : Long = elapsedTime,
// 参照された時刻
var time_used : Long = time_update
)
internal class Request(
val instance : String,
val isMisskey : Boolean,
val reportWithAliases : Boolean = false,
val onListLoaded : (list : ArrayList<CustomEmoji>) -> Unit?
)
// 成功キャッシュ
internal val cache = ConcurrentHashMap<String, CacheItem>()
// エラーキャッシュ
internal val cache_error = ConcurrentHashMap<String, Long>()
private val cache_error_item = CacheItem("error")
// ロード要求
internal val queue = ConcurrentLinkedQueue<Request>()
private val handler : Handler
private val worker : Worker
init {
this.handler = Handler(context.mainLooper)
this.worker = Worker()
worker.start()
}
private fun getCached(now : Long, instance : String) : CacheItem? {
// 成功キャッシュ
val item = cache[instance]
if(item != null && now - item.time_update <= ERROR_EXPIRE) {
item.time_used = now
return item
}
// エラーキャッシュ
val time_error = cache_error[instance]
if(time_error != null && now < time_error + ERROR_EXPIRE) {
return cache_error_item
}
return null
}
fun getList(
_instance : String,
isMisskey : Boolean,
onListLoaded : (list : ArrayList<CustomEmoji>) -> Unit
) : ArrayList<CustomEmoji>? {
try {
if(_instance.isEmpty()) return null
val instance = _instance.toLowerCase(Locale.JAPAN)
synchronized(cache) {
val item = getCached(elapsedTime, instance)
if(item != null) return item.list
}
queue.add(Request(instance, isMisskey, onListLoaded = onListLoaded))
worker.notifyEx()
} catch(ex : Throwable) {
log.trace(ex)
}
return null
}
fun getListWithAliases(
_instance : String,
isMisskey : Boolean,
onListLoaded : (list : ArrayList<CustomEmoji>) -> Unit
) : ArrayList<CustomEmoji>? {
try {
if(_instance.isEmpty()) return null
val instance = _instance.toLowerCase(Locale.JAPAN)
synchronized(cache) {
val item = getCached(elapsedTime, instance)
if(item != null) return item.listWithAliases
}
queue.add(
Request(
instance,
isMisskey,
reportWithAliases = true,
onListLoaded = onListLoaded
)
)
worker.notifyEx()
} catch(ex : Throwable) {
log.trace(ex)
}
return null
}
fun getMap(host : String, isMisskey : Boolean) : HashMap<String, CustomEmoji>? {
val list = getList(host, isMisskey) {
// 遅延ロード非対応
} ?: return null
//
val dst = HashMap<String, CustomEmoji>()
for(e in list) {
dst[e.shortcode] = e
}
return dst
}
private inner class Worker : WorkerBase() {
override fun cancel() {
// このスレッドはキャンセルされない。プロセスが生きている限り動き続ける。
}
override fun run() {
while(true) {
try {
// リクエストを取得する
val request = queue.poll()
if(request == null) {
// なければ待機
waitEx(86400000L)
continue
}
val cached = synchronized(cache) {
val item = getCached(elapsedTime, request.instance)
return@synchronized if(item != null) {
val list = item.list
val listWithAliases = item.listWithAliases
if(list != null && listWithAliases != null) {
fireCallback(request, list, listWithAliases)
}
true
} else {
// キャッシュにはなかった
sweep_cache()
false
}
}
if(cached) continue
var list : ArrayList<CustomEmoji>? = null
var listWithAlias : ArrayList<CustomEmoji>? = null
try {
val data = if(request.isMisskey) {
App1.getHttpCachedString("https://" + request.instance + "/api/meta") { builder ->
builder.post(JSONObject().toRequestBody())
}
} else {
App1.getHttpCachedString("https://" + request.instance + "/api/v1/custom_emojis")
}
if(data != null) {
val a = decodeEmojiList(data, request.instance, request.isMisskey)
list = a
listWithAlias = makeListWithAlias(a)
}
} catch(ex : Throwable) {
log.trace(ex)
}
synchronized(cache) {
val now = elapsedTime
if(list == null || listWithAlias == null) {
cache_error.put(request.instance, now)
} else {
var item : CacheItem? = cache[request.instance]
if(item == null) {
item = CacheItem(request.instance, list, listWithAlias)
cache[request.instance] = item
} else {
item.list = list
item.listWithAliases = listWithAlias
item.time_update = now
}
fireCallback(request, list, listWithAlias)
}
}
} catch(ex : Throwable) {
log.trace(ex)
waitEx(3000L)
}
}
}
private fun fireCallback(
request : Request,
list : ArrayList<CustomEmoji>,
listWithAliases : ArrayList<CustomEmoji>
) {
handler.post {
request.onListLoaded(
if(request.reportWithAliases) {
listWithAliases
} else {
list
}
)
}
}
// キャッシュの掃除
private fun sweep_cache() {
// 超過してる数
val over = cache.size - CACHE_MAX
if(over <= 0) return
// 古い要素を一時リストに集める
val now = elapsedTime
val list = ArrayList<CacheItem>(over)
for(item in cache.values) {
if(now - item.time_used > 1000L) list.add(item)
}
// 昇順ソート
list.sortBy { it.time_used }
// 古い物から順に捨てる
var removed = 0
for(item in list) {
cache.remove(item.instance)
if(++ removed >= over) break
}
}
private fun decodeEmojiList(
data : String,
instance : String,
isMisskey : Boolean
) : ArrayList<CustomEmoji>? {
return try {
val list = if(isMisskey) {
parseList(CustomEmoji.decodeMisskey, JSONObject(data).optJSONArray("emojis"))
} else {
parseList(CustomEmoji.decode, data.toJsonArray())
}
list.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode })
list
} catch(ex : Throwable) {
log.e(ex, "decodeEmojiList failed. instance=%s", instance)
null
}
}
private fun makeListWithAlias(list : ArrayList<CustomEmoji>?) : ArrayList<CustomEmoji> {
val dst = ArrayList<CustomEmoji>()
if(list != null) {
dst.addAll(list)
for(item in list) {
val aliases = item.aliases ?: continue
for(alias in aliases) {
if(alias.equals(item.shortcode, ignoreCase = true)) continue
dst.add(item.makeAlias(alias))
}
}
dst.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode })
}
return dst
}
}
}