2018-01-04 19:52:25 +01:00
|
|
|
|
package jp.juggler.subwaytooter.util
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.graphics.Bitmap
|
|
|
|
|
import android.graphics.BitmapFactory
|
|
|
|
|
import android.os.Handler
|
|
|
|
|
import android.os.SystemClock
|
|
|
|
|
|
|
|
|
|
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
|
2018-01-28 20:03:04 +01:00
|
|
|
|
import jp.juggler.apng.ApngFrames
|
2018-12-01 00:02:18 +01:00
|
|
|
|
import jp.juggler.util.LogCategory
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
class CustomEmojiCache(internal val context : Context) {
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
|
|
|
|
|
private val log = LogCategory("CustomEmojiCache")
|
|
|
|
|
|
2018-01-20 07:51:14 +01:00
|
|
|
|
internal const val DEBUG = false
|
2018-01-04 19:52:25 +01:00
|
|
|
|
internal const val CACHE_MAX = 512 // 使用中のビットマップは掃除しないので、頻度によってはこれより多くなることもある
|
|
|
|
|
internal const val ERROR_EXPIRE = 60000L * 10
|
|
|
|
|
|
|
|
|
|
private val elapsedTime : Long
|
|
|
|
|
get() = SystemClock.elapsedRealtime()
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-27 12:00:44 +01:00
|
|
|
|
private class CacheItem(val url : String, var frames : ApngFrames?) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
var time_used : Long = elapsedTime
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class Request(
|
|
|
|
|
val refTarget : WeakReference<Any>,
|
|
|
|
|
val url : String,
|
2018-01-10 16:47:35 +01:00
|
|
|
|
val onLoadComplete : ()->Unit
|
2018-01-04 19:52:25 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
////////////////////////////////
|
|
|
|
|
|
|
|
|
|
// 成功キャッシュ
|
|
|
|
|
private val cache : ConcurrentHashMap<String, CacheItem>
|
|
|
|
|
|
|
|
|
|
// エラーキャッシュ
|
|
|
|
|
private val cache_error = ConcurrentHashMap<String, Long>()
|
|
|
|
|
private val cache_error_item = CacheItem("error", null)
|
|
|
|
|
|
|
|
|
|
// リクエストキュー
|
|
|
|
|
// キャンセル操作の都合上、アクセス時に排他が必要
|
|
|
|
|
private val queue = LinkedList<Request>()
|
|
|
|
|
|
|
|
|
|
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<Any>) {
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-27 12:00:44 +01:00
|
|
|
|
fun getFrames(refDrawTarget: WeakReference<Any>?, url : String, onLoadComplete : ()->Unit) : ApngFrames? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
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) {
|
2018-01-10 16:47:35 +01:00
|
|
|
|
queue.addLast(Request(refDrawTarget, url, onLoadComplete))
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
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 {
|
2018-12-03 09:07:07 +01:00
|
|
|
|
var queue_size : Int
|
2018-01-04 19:52:25 +01:00
|
|
|
|
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)
|
|
|
|
|
|
2018-01-27 12:00:44 +01:00
|
|
|
|
var frames : ApngFrames? = null
|
2018-01-04 19:52:25 +01:00
|
|
|
|
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)
|
2018-01-21 13:46:36 +01:00
|
|
|
|
cache[request.url] = item
|
2018-01-04 19:52:25 +01:00
|
|
|
|
} 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) {
|
2018-01-10 16:47:35 +01:00
|
|
|
|
handler.post { request.onLoadComplete() }
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun sweep_cache(now : Long) {
|
|
|
|
|
|
|
|
|
|
// キャッシュ限界を超過した数
|
|
|
|
|
val over = cache.size - CACHE_MAX
|
|
|
|
|
|
|
|
|
|
// 超過した数がある程度大きくなるまで掃除しない
|
|
|
|
|
if(over <= 64) return
|
|
|
|
|
|
|
|
|
|
// 掃除する候補
|
|
|
|
|
val list = ArrayList<CacheItem>()
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-27 12:00:44 +01:00
|
|
|
|
private fun decodeAPNG(data : ByteArray, url : String) : ApngFrames? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
try {
|
2018-01-20 07:51:14 +01:00
|
|
|
|
// PNGヘッダを確認
|
|
|
|
|
if( data.size >= 8
|
|
|
|
|
&& (data[0].toInt() and 0xff) == 0x89
|
|
|
|
|
&& (data[1].toInt() and 0xff) == 0x50
|
|
|
|
|
){
|
|
|
|
|
// APNGをデコード
|
2018-01-27 12:00:44 +01:00
|
|
|
|
return ApngFrames.parseApng(ByteArrayInputStream(data), 64)
|
2018-01-20 07:51:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
// fall thru
|
2018-01-04 19:52:25 +01:00
|
|
|
|
} catch(ex : Throwable) {
|
2018-01-11 10:31:25 +01:00
|
|
|
|
if(DEBUG) log.trace(ex)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
log.e(ex, "PNG decode failed. %s ", url)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 通常のビットマップでのロードを試みる
|
|
|
|
|
try {
|
|
|
|
|
val b = decodeBitmap(data, 128)
|
|
|
|
|
if(b != null) {
|
|
|
|
|
if(DEBUG) log.d("bitmap decoded.")
|
2018-01-27 12:00:44 +01:00
|
|
|
|
return ApngFrames(b)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
} else {
|
|
|
|
|
log.e("Bitmap decode returns null. %s", url)
|
|
|
|
|
}
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.e(ex, "Bitmap 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|