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

217 lines
5.3 KiB
Kotlin
Raw Normal View History

package jp.juggler.subwaytooter.util
import android.content.Context
import android.os.Handler
import android.os.SystemClock
import java.util.ArrayList
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.api.entity.CustomEmoji
import jp.juggler.subwaytooter.api.entity.parseList
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>?) {
// 参照された時刻
var time_used : Long = 0
// ロードした時刻
var time_update : Long = 0
init {
time_update = elapsedTime
time_used = time_update
}
}
2018-01-19 14:15:39 +01:00
internal class Request(
val instance : String,
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", null)
// ロード要求
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? {
2018-01-19 14:15:39 +01:00
// 成功キャッシュ
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
}
2018-01-19 14:15:39 +01:00
fun getList(
_instance : String,
onListLoaded : (list : ArrayList<CustomEmoji>) -> Unit
) : ArrayList<CustomEmoji>? {
try {
2018-01-19 14:15:39 +01:00
if(_instance.isEmpty()) return null
val instance = _instance.toLowerCase()
synchronized(cache) {
val item = getCached(elapsedTime, instance)
if(item != null) return item.list
}
queue.add(Request(instance, onListLoaded))
worker.notifyEx()
2018-01-19 14:15:39 +01:00
} catch(ex : Throwable) {
log.trace(ex)
}
return null
}
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
if(list != null) {
fireCallback(request, list)
}
true
} else {
// キャッシュにはなかった
sweep_cache()
false
}
}
if(cached) continue
var list : ArrayList<CustomEmoji>? = null
try {
2018-01-19 14:15:39 +01:00
val data =
App1.getHttpCachedString("https://" + request.instance + "/api/v1/custom_emojis")
if(data != null) {
list = decodeEmojiList(data, request.instance)
}
} catch(ex : Throwable) {
log.trace(ex)
}
synchronized(cache) {
val now = elapsedTime
if(list == null) {
cache_error.put(request.instance, now)
} else {
var item : CacheItem? = cache[request.instance]
if(item == null) {
item = CacheItem(request.instance, list)
cache[request.instance] = item
} else {
item.list = list
item.time_update = now
}
fireCallback(request, list)
}
}
2018-01-19 14:15:39 +01:00
} catch(ex : Throwable) {
log.trace(ex)
waitEx(3000L)
}
}
}
2018-01-19 14:15:39 +01:00
private fun fireCallback(request : Request, list : ArrayList<CustomEmoji>) {
handler.post { request.onListLoaded(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)
}
// 昇順ソート
2018-01-19 14:15:39 +01:00
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) : ArrayList<CustomEmoji>? {
return try {
val list = parseList(::CustomEmoji, data.toJsonArray() )
list.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.shortcode }))
list
} catch(ex : Throwable) {
log.e(ex, "decodeEmojiList failed. instance=%s", instance)
null
}
}
}
}