package jp.juggler.subwaytooter.util import android.content.Context import android.graphics.* import android.os.Handler import android.os.SystemClock import com.caverock.androidsvg.SVG import java.io.ByteArrayInputStream import java.lang.ref.WeakReference import java.util.ArrayList import java.util.LinkedList import java.util.concurrent.ConcurrentHashMap import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.span.NetworkEmojiSpan import jp.juggler.apng.ApngFrames import jp.juggler.util.LogCategory import kotlin.math.ceil class CustomEmojiCache(internal val context : Context) { companion object { private val log = LogCategory("CustomEmojiCache") internal const val DEBUG = false internal const val CACHE_MAX = 512 // 使用中のビットマップは掃除しないので、頻度によってはこれより多くなることもある internal const val ERROR_EXPIRE = 60000L * 10 private val elapsedTime : Long get() = SystemClock.elapsedRealtime() } private class CacheItem(val url : String, var frames : ApngFrames?) { var time_used : Long = elapsedTime } private class Request( val refTarget : WeakReference, val url : String, val onLoadComplete : () -> Unit ) //////////////////////////////// // 成功キャッシュ private val cache : ConcurrentHashMap // エラーキャッシュ private val cache_error = ConcurrentHashMap() private val cache_error_item = CacheItem("error", null) // リクエストキュー // キャンセル操作の都合上、アクセス時に排他が必要 private val queue = LinkedList() private val handler : Handler private val worker : Worker init { handler = Handler(context.mainLooper) cache = ConcurrentHashMap() worker = Worker() worker.start() } // カラムのリロードボタンを押したタイミングでエラーキャッシュをクリアする fun clearErrorCache() { cache_error.clear() } // tag_target を持つリクエストまたはtagがGCされたリクエストをキューから除去する fun cancelRequest(refTarget : WeakReference) { val targetTag = refTarget.get() ?: return synchronized(queue) { val it = queue.iterator() while(it.hasNext()) { val request = it.next() val tag = request.refTarget.get() if(tag === null || tag === targetTag) it.remove() } } } private fun getCached(now : Long, url : String) : CacheItem? { // 成功キャッシュ val item = cache[url] if(item != null) { item.time_used = now return item } // エラーキャッシュ val time_error = cache_error[url] if(time_error != null && now < time_error + ERROR_EXPIRE) { return cache_error_item } return null } fun getFrames( refDrawTarget : WeakReference?, url : String, onLoadComplete : () -> Unit ) : ApngFrames? { try { if(refDrawTarget?.get() == null) { NetworkEmojiSpan.log.e("draw: DrawTarget is null ") return null } cancelRequest(refDrawTarget) synchronized(cache) { val item = getCached(elapsedTime, url) if(item != null) return item.frames } // キャンセル操作の都合上、排他が必要 synchronized(queue) { queue.addLast(Request(refDrawTarget, url, onLoadComplete)) } worker.notifyEx() } catch(ex : Throwable) { log.trace(ex) // たまにcache変数がなぜかnullになる端末があるらしい } return null } private inner class Worker : WorkerBase() { override fun cancel() { // このスレッドはプロセスが生きてる限りキャンセルされない } override fun run() { while(true) { try { var queue_size : Int val request = synchronized(queue) { val x = if(queue.isNotEmpty()) queue.removeFirst() else null queue_size = queue.size return@synchronized x } if(request == null) { if(DEBUG) log.d("wait") waitEx(86400000L) continue } // 描画先がGCされたなら何もしない request.refTarget.get() ?: continue var cache_size : Int = - 1 if(synchronized(cache) { val now = elapsedTime val item = getCached(now, request.url) if(item != null) { if(item.frames != null) { fireCallback(request) } return@synchronized true } sweep_cache(now) cache_size = cache.size return@synchronized false }) continue if(DEBUG) log.d( "start get image. queue_size=%d, cache_size=%d url=%s", queue_size, cache_size, request.url ) var frames : ApngFrames? = null try { val data = App1.getHttpCached(request.url) if(data == null) { log.e("get failed. url=%s", request.url) } else { frames = decodeAPNG(data, request.url) } } catch(ex : Throwable) { log.trace(ex) } synchronized(cache) { if(frames == null) { cache_error.put(request.url, elapsedTime) } else { // 古いキャッシュがある var item : CacheItem? = cache[request.url] if(item == null) { item = CacheItem(request.url, frames) cache[request.url] = item } else { item.frames?.dispose() item.frames = frames } fireCallback(request) } } } catch(ex : Throwable) { log.trace(ex) // Fujitsu F-01H(F01H), 2048MB RAM, Android 6.0 // java.lang.NullPointerException: // at java.util.concurrent.ConcurrentHashMap.get (ConcurrentHashMap.java:772) // at jp.juggler.subwaytooter.util.CustomEmojiCache$Worker.run (CustomEmojiCache.java:183) waitEx(3000L) } } } private fun fireCallback(request : Request) { handler.post { request.onLoadComplete() } } private fun sweep_cache(now : Long) { // キャッシュ限界を超過した数 val over = cache.size - CACHE_MAX // 超過した数がある程度大きくなるまで掃除しない if(over <= 64) return // 掃除する候補 val list = ArrayList() 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.url) item.frames?.dispose() if(++ removed >= over) break } } private fun decodeAPNG(data : ByteArray, url : String) : ApngFrames? { try { // PNGヘッダを確認 if(data.size >= 8 && (data[0].toInt() and 0xff) == 0x89 && (data[1].toInt() and 0xff) == 0x50 ) { // APNGをデコード return ApngFrames.parseApng(ByteArrayInputStream(data), 64) } // fall thru } catch(ex : Throwable) { if(DEBUG) log.trace(ex) log.e(ex, "PNG decode failed. %s ", url) } // 通常のビットマップでのロードを試みる try { val b = decodeBitmap(data, 128) if(b != null) { if(DEBUG) log.d("bitmap decoded.") return ApngFrames(b) } else { log.e("Bitmap decode returns null. %s", url) } // fall thru } catch(ex : Throwable) { log.e(ex, "Bitmap decode failed. %s", url) } // SVGのロードを試みる try { val b = decodeSVG(data, 128.toFloat()) if(b != null) { if(DEBUG) log.d("SVG decoded.") return ApngFrames(b) } // fall thru } catch(ex : Throwable) { log.e(ex, "SVG decode failed. %s", url) } return null } private val options = BitmapFactory.Options() private fun decodeBitmap(data : ByteArray, pixel_max : Int) : Bitmap? { options.inJustDecodeBounds = true options.inScaled = false options.outWidth = 0 options.outHeight = 0 BitmapFactory.decodeByteArray(data, 0, data.size, options) var w = options.outWidth var h = options.outHeight if(w <= 0 || h <= 0) { log.e("can't decode bounds.") return null } var bits = 0 while(w > pixel_max || h > pixel_max) { ++ bits w = w shr 1 h = h shr 1 } options.inJustDecodeBounds = false options.inSampleSize = 1 shl bits return BitmapFactory.decodeByteArray(data, 0, data.size, options) } private fun decodeSVG(data : ByteArray, pixelMax : Float) : Bitmap? { try { val svg = SVG.getFromInputStream(ByteArrayInputStream(data)) val src_w = svg.documentWidth val src_h = svg.documentHeight val aspect = src_w / src_h val dst_w : Float val dst_h : Float if(aspect >= 1f) { dst_w = pixelMax dst_h = pixelMax / aspect } else { dst_h = pixelMax dst_w = pixelMax * aspect } val w_ceil = ceil(dst_w) val h_ceil = ceil(dst_h) // Create a Bitmap to render our SVG to val b = Bitmap.createBitmap(w_ceil.toInt(), h_ceil.toInt(), Bitmap.Config.ARGB_8888) // Create a Canvas to use for rendering val canvas = Canvas(b) svg.renderToCanvas( canvas, if(aspect >= 1f) { RectF(0f, h_ceil-dst_h, dst_w, dst_h) // 後半はw,hを指定する } else { RectF(w_ceil-dst_w, 0f, dst_w , dst_h) // 後半はw,hを指定する } ) return b } catch(ex : Throwable) { log.e(ex, "decodeSVG failed.") } return null } } }