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.subwaytooter.table.SavedAccount import jp.juggler.util.* import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue class CustomEmojiLister( val context : Context, private val handler : Handler ) { 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, var list : ArrayList? = null, var listWithAliases : ArrayList? = null, // ロードした時刻 var time_update : Long = elapsedTime, // 参照された時刻 var time_used : Long = time_update ) internal class Request( val accessInfo : SavedAccount, val reportWithAliases : Boolean = false, val onListLoaded : (list : ArrayList) -> Unit? ) // 成功キャッシュ internal val cache = ConcurrentHashMap() // エラーキャッシュ internal val cache_error = ConcurrentHashMap() private val cache_error_item = CacheItem("error") // ロード要求 internal val queue = ConcurrentLinkedQueue() private val worker : Worker init { this.worker = Worker() } // ネットワーク接続が変化したらエラーキャッシュをクリア fun onNetworkChanged() { cache_error.clear() } private fun getCached(now : Long, accessInfo : SavedAccount) : CacheItem? { val host = accessInfo.apiHost.ascii // 成功キャッシュ val item = cache[host] if(item != null && now - item.time_update <= ERROR_EXPIRE) { item.time_used = now return item } // エラーキャッシュ val time_error = cache_error[host] if(time_error != null && now < time_error + ERROR_EXPIRE) { return cache_error_item } return null } fun getList( accessInfo : SavedAccount, onListLoaded : (list : ArrayList) -> Unit ) : ArrayList? { try { synchronized(cache) { val item = getCached(elapsedTime, accessInfo) if(item != null) return item.list } queue.add(Request(accessInfo, onListLoaded = onListLoaded)) worker.notifyEx() } catch(ex : Throwable) { log.trace(ex) } return null } fun getListWithAliases( accessInfo : SavedAccount, onListLoaded : (list : ArrayList) -> Unit ) : ArrayList? { try { synchronized(cache) { val item = getCached(elapsedTime, accessInfo) if(item != null) return item.listWithAliases } queue.add( Request( accessInfo, reportWithAliases = true, onListLoaded = onListLoaded ) ) worker.notifyEx() } catch(ex : Throwable) { log.trace(ex) } return null } fun getMap(accessInfo : SavedAccount) : HashMap? { val list = getList(accessInfo) { // 遅延ロード非対応 } ?: return null // val dst = HashMap() for(e in list) { dst[e.shortcode] = e } return dst } 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 } val cached = synchronized(cache) { val item = getCached(elapsedTime, request.accessInfo) 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 val accessInfo = request.accessInfo val cacheKey = accessInfo.apiHost.ascii var list : ArrayList? = null var listWithAlias : ArrayList? = null try { val data = if(accessInfo.isMisskey) { App1.getHttpCachedString( "https://${cacheKey}/api/meta", accessInfo = accessInfo ) { builder -> builder.post(JsonObject().toRequestBody()) } } else { App1.getHttpCachedString( "https://${cacheKey}/api/v1/custom_emojis", accessInfo = accessInfo ) } if(data != null) { val a = decodeEmojiList(data, accessInfo) list = a listWithAlias = makeListWithAlias(a) } } catch(ex : Throwable) { log.trace(ex) } synchronized(cache) { val now = elapsedTime if(list == null || listWithAlias == null) { cache_error.put(cacheKey, now) } else { var item : CacheItem? = cache[cacheKey] if(item == null) { item = CacheItem(cacheKey, list, listWithAlias) cache[cacheKey] = 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, listWithAliases : ArrayList ) { 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(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.key) if(++ removed >= over) break } } private fun decodeEmojiList( data : String, accessInfo : SavedAccount ) : ArrayList? { return try { val list = if(accessInfo.isMisskey) { parseList( CustomEmoji.decodeMisskey, data.decodeJsonObject().jsonArray("emojis") ) } else { parseList(CustomEmoji.decode, data.decodeJsonArray()) } list.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode }) list } catch(ex : Throwable) { log.e(ex, "decodeEmojiList failed. instance=%s", accessInfo.apiHost.ascii) null } } private fun makeListWithAlias(list : ArrayList?) : ArrayList { val dst = ArrayList() 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 } } }