2018-01-04 19:52:25 +01:00
|
|
|
|
package jp.juggler.subwaytooter.util
|
|
|
|
|
|
2020-02-05 09:18:28 +01:00
|
|
|
|
import android.content.ContentValues
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import android.content.Context
|
2020-02-05 09:18:28 +01:00
|
|
|
|
import android.database.sqlite.SQLiteDatabase
|
|
|
|
|
import android.database.sqlite.SQLiteDatabaseCorruptException
|
|
|
|
|
import android.database.sqlite.SQLiteOpenHelper
|
2019-08-12 03:28:13 +02:00
|
|
|
|
import android.graphics.Bitmap
|
|
|
|
|
import android.graphics.BitmapFactory
|
|
|
|
|
import android.graphics.Canvas
|
|
|
|
|
import android.graphics.RectF
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import android.os.Handler
|
|
|
|
|
import android.os.SystemClock
|
2020-02-05 09:18:28 +01:00
|
|
|
|
import android.provider.BaseColumns
|
2019-03-20 05:35:07 +01:00
|
|
|
|
import com.caverock.androidsvg.SVG
|
2019-08-12 03:28:13 +02:00
|
|
|
|
import jp.juggler.apng.ApngFrames
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import jp.juggler.subwaytooter.App1
|
2018-12-01 00:02:18 +01:00
|
|
|
|
import jp.juggler.util.LogCategory
|
2021-06-24 06:49:27 +02:00
|
|
|
|
import jp.juggler.util.TableCompanion
|
2021-10-28 01:37:39 +02:00
|
|
|
|
import jp.juggler.util.getBlobOrNull
|
|
|
|
|
import jp.juggler.util.getLong
|
2020-12-08 21:24:42 +01:00
|
|
|
|
import kotlinx.coroutines.channels.Channel
|
2019-08-12 03:28:13 +02:00
|
|
|
|
import java.io.ByteArrayInputStream
|
|
|
|
|
import java.lang.ref.WeakReference
|
|
|
|
|
import java.util.*
|
|
|
|
|
import java.util.concurrent.ConcurrentHashMap
|
2020-02-05 09:18:28 +01:00
|
|
|
|
import java.util.concurrent.TimeUnit
|
2019-03-20 05:35:07 +01:00
|
|
|
|
import kotlin.math.ceil
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2020-09-09 20:13:11 +02:00
|
|
|
|
class CustomEmojiCache(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
val context: Context,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
private val handler: Handler,
|
2020-09-09 20:13:11 +02:00
|
|
|
|
) {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
// カスタム絵文字のキャッシュ専用のデータベースファイルを作る
|
|
|
|
|
// (DB破損などの際に削除してしまえるようにする)
|
|
|
|
|
private const val CACHE_DB_NAME = "emoji_cache_db"
|
|
|
|
|
private const val CACHE_DB_VERSION = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class DbCache(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
val id: Long,
|
|
|
|
|
val timeUsed: Long,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
val data: ByteArray,
|
2021-06-13 13:48:48 +02:00
|
|
|
|
) {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
|
|
|
|
companion object : TableCompanion {
|
|
|
|
|
|
2021-10-28 01:37:39 +02:00
|
|
|
|
override val table = "custom_emoji_cache"
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
|
|
|
|
const val COL_ID = BaseColumns._ID
|
|
|
|
|
const val COL_TIME_SAVE = "time_save"
|
|
|
|
|
const val COL_TIME_USED = "time_used"
|
|
|
|
|
const val COL_URL = "url"
|
|
|
|
|
const val COL_DATA = "data"
|
|
|
|
|
|
|
|
|
|
override fun onDBCreate(db: SQLiteDatabase) {
|
|
|
|
|
db.execSQL(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
"""create table if not exists $table
|
2020-02-05 09:18:28 +01:00
|
|
|
|
($COL_ID INTEGER PRIMARY KEY
|
|
|
|
|
,$COL_TIME_SAVE integer not null
|
|
|
|
|
,$COL_TIME_USED integer not null
|
|
|
|
|
,$COL_URL text not null
|
|
|
|
|
,$COL_DATA blob not null
|
|
|
|
|
)""".trimIndent()
|
2021-06-13 13:48:48 +02:00
|
|
|
|
)
|
2021-06-20 15:12:25 +02:00
|
|
|
|
db.execSQL("create unique index if not exists ${table}_url on $table($COL_URL)")
|
|
|
|
|
db.execSQL("create index if not exists ${table}_old on $table($COL_TIME_USED)")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onDBUpgrade(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
db: SQLiteDatabase,
|
|
|
|
|
oldVersion: Int,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
newVersion: Int,
|
2021-06-13 13:48:48 +02:00
|
|
|
|
) {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun load(db: SQLiteDatabase, url: String, now: Long) =
|
|
|
|
|
db.rawQuery(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
"select $COL_ID,$COL_TIME_USED,$COL_DATA from $table where $COL_URL=?",
|
|
|
|
|
arrayOf(url)
|
|
|
|
|
)?.use { cursor ->
|
2021-06-20 15:12:25 +02:00
|
|
|
|
if (cursor.moveToNext()) {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
DbCache(
|
2021-10-28 01:37:39 +02:00
|
|
|
|
id = cursor.getLong(COL_ID),
|
|
|
|
|
timeUsed = cursor.getLong(COL_TIME_USED),
|
|
|
|
|
data = cursor.getBlobOrNull(COL_DATA)!!
|
2021-06-13 13:48:48 +02:00
|
|
|
|
).apply {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
if (now - timeUsed >= 5 * 3600000L) {
|
|
|
|
|
db.update(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
table,
|
|
|
|
|
ContentValues().apply {
|
|
|
|
|
put(COL_TIME_USED, now)
|
|
|
|
|
},
|
|
|
|
|
"$COL_ID=?",
|
|
|
|
|
arrayOf(id.toString())
|
|
|
|
|
)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-06-20 15:12:25 +02:00
|
|
|
|
} else {
|
|
|
|
|
null
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun sweep(db: SQLiteDatabase, now: Long) {
|
|
|
|
|
val expire = now - TimeUnit.DAYS.toMillis(30)
|
|
|
|
|
db.delete(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
table,
|
|
|
|
|
"$COL_TIME_USED < ?",
|
|
|
|
|
arrayOf(expire.toString())
|
|
|
|
|
)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun update(db: SQLiteDatabase, url: String, data: ByteArray) {
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
db.replace(table,
|
2021-06-13 13:48:48 +02:00
|
|
|
|
null,
|
|
|
|
|
ContentValues().apply {
|
|
|
|
|
put(COL_URL, url)
|
|
|
|
|
put(COL_DATA, data)
|
|
|
|
|
put(COL_TIME_USED, now)
|
|
|
|
|
put(COL_TIME_SAVE, now)
|
|
|
|
|
}
|
|
|
|
|
)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class DbOpenHelper(val context: Context) :
|
|
|
|
|
SQLiteOpenHelper(context, CACHE_DB_NAME, null, CACHE_DB_VERSION) {
|
|
|
|
|
|
|
|
|
|
private val tables = arrayOf(DbCache)
|
|
|
|
|
override fun onCreate(db: SQLiteDatabase) =
|
|
|
|
|
tables.forEach { it.onDBCreate(db) }
|
|
|
|
|
|
|
|
|
|
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) =
|
|
|
|
|
tables.forEach { it.onDBUpgrade(db, oldVersion, newVersion) }
|
|
|
|
|
|
|
|
|
|
fun deleteDatabase() {
|
|
|
|
|
try {
|
|
|
|
|
close()
|
|
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
log.e(ex, "deleteDatabase: close() failed.")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
SQLiteDatabase.deleteDatabase(context.getDatabasePath(databaseName))
|
|
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
log.e(ex, "deleteDatabase failed.")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class CacheItem(val url: String, var frames: ApngFrames?) {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
var timeUsed: Long = elapsedTime
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class Request(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
val refTarget: WeakReference<Any>,
|
|
|
|
|
val url: String,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
val onLoadComplete: () -> Unit,
|
2021-06-13 13:48:48 +02:00
|
|
|
|
)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
|
|
|
|
// APNGデコード済のキャッシュデータ
|
|
|
|
|
private val cache = ConcurrentHashMap<String, CacheItem>()
|
|
|
|
|
|
|
|
|
|
// エラーキャッシュ
|
2021-06-20 15:12:25 +02:00
|
|
|
|
private val cacheError = ConcurrentHashMap<String, Long>()
|
|
|
|
|
private val cacheErrorItem = CacheItem("error", null)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
|
|
|
|
// リクエストキュー
|
|
|
|
|
// キャンセル操作の都合上、アクセス時に排他が必要
|
|
|
|
|
private val queue = LinkedList<Request>()
|
|
|
|
|
|
|
|
|
|
private val dbOpenHelper = DbOpenHelper(context)
|
|
|
|
|
|
|
|
|
|
private var lastSweepDbCache = 0L
|
|
|
|
|
|
|
|
|
|
private val workerLock = Channel<Unit>(capacity = 8)
|
|
|
|
|
|
|
|
|
|
// 他の変数より後に初期化すること
|
|
|
|
|
private val workers =
|
|
|
|
|
(1..4).map { Worker(workerLock) }.toList()
|
|
|
|
|
|
|
|
|
|
// DB処理を行い、SQLiteDatabaseCorruptExceptionを検出したらDBを削除してリトライする
|
|
|
|
|
private fun <T : Any> useDbCache(block: (SQLiteDatabase) -> T?): T? {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
for (nTry in 0 until 3) {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
try {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
val db = dbOpenHelper.writableDatabase
|
|
|
|
|
if (db == null) {
|
|
|
|
|
log.e("useDbCache[$nTry]: writableDatabase returns null.")
|
|
|
|
|
break
|
|
|
|
|
}
|
2020-12-08 21:24:42 +01:00
|
|
|
|
return block(db)
|
|
|
|
|
} catch (ex: SQLiteDatabaseCorruptException) {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
log.e(ex, "useDbCache[$nTry]: db corrupt!")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
dbOpenHelper.deleteDatabase()
|
|
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
log.e(ex, "useDbCache[$nTry]: failed.")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ネットワーク接続が切り替わったタイミングでエラーキャッシュをクリアする
|
|
|
|
|
fun onNetworkChanged() {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
cacheError.clear()
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// カラムのリロードボタンを押したタイミングでエラーキャッシュをクリアする
|
|
|
|
|
fun clearErrorCache() {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
cacheError.clear()
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
item.timeUsed = now
|
2020-12-08 21:24:42 +01:00
|
|
|
|
return item
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// エラーキャッシュ
|
2021-06-20 15:12:25 +02:00
|
|
|
|
val timeError = cacheError[url]
|
|
|
|
|
if (timeError != null && now < timeError + ERROR_EXPIRE) {
|
|
|
|
|
return cacheErrorItem
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getFrames(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
refDrawTarget: WeakReference<Any>?,
|
|
|
|
|
url: String,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
onLoadComplete: () -> Unit,
|
2021-06-13 13:48:48 +02:00
|
|
|
|
): ApngFrames? {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
try {
|
|
|
|
|
if (refDrawTarget?.get() == null) {
|
|
|
|
|
log.e("draw: DrawTarget is null ")
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cancelRequest(refDrawTarget)
|
|
|
|
|
|
|
|
|
|
// APNG frame cache
|
|
|
|
|
synchronized(cache) {
|
|
|
|
|
getCached(elapsedTime, url)?.let { return it.frames }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// キャンセル操作の都合上、排他が必要
|
|
|
|
|
synchronized(queue) {
|
|
|
|
|
queue.addLast(Request(refDrawTarget, url, onLoadComplete))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
workers.first().notifyEx()
|
|
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
log.e(ex, "getFrames failed.")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
// たまにcache変数がなぜかnullになる端末があるらしい
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun delete() {
|
|
|
|
|
synchronized(cache) {
|
|
|
|
|
for (entry in cache.entries) {
|
|
|
|
|
entry.value.frames?.dispose()
|
|
|
|
|
}
|
|
|
|
|
cache.clear()
|
2021-06-20 15:12:25 +02:00
|
|
|
|
cacheError.clear()
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
dbOpenHelper.deleteDatabase()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private inner class Worker(waiter: Channel<Unit>) : WorkerBase(waiter) {
|
|
|
|
|
|
|
|
|
|
override fun cancel() {
|
|
|
|
|
// このスレッドはプロセスが生きてる限りキャンセルされない
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override suspend fun run() {
|
|
|
|
|
var ts: Long
|
|
|
|
|
var te: Long
|
|
|
|
|
while (true) {
|
|
|
|
|
try {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
var queueSize: Int
|
2020-12-08 21:24:42 +01:00
|
|
|
|
val request = synchronized(queue) {
|
|
|
|
|
val x = if (queue.isNotEmpty()) queue.removeFirst() else null
|
2021-06-20 15:12:25 +02:00
|
|
|
|
queueSize = queue.size
|
2020-12-08 21:24:42 +01:00
|
|
|
|
x
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (request == null) {
|
|
|
|
|
if (DEBUG) log.d("wait")
|
|
|
|
|
|
|
|
|
|
synchronized(cache) {
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
if (now - lastSweepDbCache >= TimeUnit.DAYS.toMillis(1)) {
|
|
|
|
|
lastSweepDbCache = now
|
|
|
|
|
useDbCache { DbCache.sweep(it, now) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ts = elapsedTime
|
|
|
|
|
waitEx(86400000L)
|
|
|
|
|
te = elapsedTime
|
|
|
|
|
if (te - ts >= 200L) log.d("sleep ${te - ts}ms")
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 描画先がGCされたなら何もしない
|
|
|
|
|
request.refTarget.get() ?: continue
|
|
|
|
|
|
|
|
|
|
ts = elapsedTime
|
2021-06-20 15:12:25 +02:00
|
|
|
|
var cacheSize: Int = -1
|
|
|
|
|
val cacheUsed = synchronized(cache) {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
val now = elapsedTime
|
|
|
|
|
val item = getCached(now, request.url)
|
|
|
|
|
if (item != null) {
|
|
|
|
|
if (item.frames != null) {
|
|
|
|
|
fireCallback(request)
|
|
|
|
|
}
|
|
|
|
|
return@synchronized true
|
|
|
|
|
}
|
2021-06-20 15:12:25 +02:00
|
|
|
|
sweepCache(now)
|
|
|
|
|
cacheSize = cache.size
|
2020-12-08 21:24:42 +01:00
|
|
|
|
return@synchronized false
|
|
|
|
|
}
|
|
|
|
|
te = elapsedTime
|
|
|
|
|
if (te - ts >= 200L) log.d("cache_used? ${te - ts}ms")
|
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
if (cacheUsed) continue
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
if (DEBUG) log.d("start get image. queue_size=$queueSize, cache_size=$cacheSize url=${request.url}")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
|
|
|
|
|
// データベースからロードしてみる
|
|
|
|
|
ts = elapsedTime
|
|
|
|
|
val dbCache = useDbCache { DbCache.load(it, request.url, now) }
|
|
|
|
|
te = elapsedTime
|
|
|
|
|
if (te - ts >= 200L) log.d("DbCache.load ${te - ts}ms")
|
|
|
|
|
|
|
|
|
|
var data = dbCache?.data
|
|
|
|
|
|
|
|
|
|
// データベースにblobがなければHTTPリクエスト
|
|
|
|
|
if (data == null) {
|
|
|
|
|
ts = elapsedTime
|
|
|
|
|
data = try {
|
|
|
|
|
App1.getHttpCached(request.url)
|
|
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
log.w(ex, "get failed. url=${request.url}")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
null
|
|
|
|
|
}
|
|
|
|
|
te = elapsedTime
|
|
|
|
|
if (te - ts >= 200L) log.d("image get? ${te - ts}ms")
|
|
|
|
|
|
|
|
|
|
if (data != null) {
|
|
|
|
|
useDbCache { db ->
|
|
|
|
|
DbCache.update(db, request.url, data)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ts = elapsedTime
|
|
|
|
|
val frames = try {
|
|
|
|
|
data?.let { decodeAPNG(it, request.url) }
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.e(ex, "decode failed.")
|
|
|
|
|
null
|
|
|
|
|
}
|
|
|
|
|
te = elapsedTime
|
|
|
|
|
if (te - ts >= 200L) log.d("image decode? ${te - ts}ms")
|
|
|
|
|
|
|
|
|
|
ts = elapsedTime
|
|
|
|
|
synchronized(cache) {
|
|
|
|
|
if (frames == null) {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
cacheError[request.url] = elapsedTime
|
2020-12-08 21:24:42 +01:00
|
|
|
|
} 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
te = elapsedTime
|
|
|
|
|
if (te - ts >= 200L) log.d("update_cache ${te - ts}ms")
|
|
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
log.e(ex, "can't load custom emojis.")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
|
|
|
|
// 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() }
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
private fun sweepCache(now: Long) {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
|
|
|
|
// キャッシュ限界を超過した数
|
|
|
|
|
val over = cache.size - CACHE_MAX
|
|
|
|
|
|
|
|
|
|
// 超過した数がある程度大きくなるまで掃除しない
|
|
|
|
|
if (over <= 64) return
|
|
|
|
|
|
|
|
|
|
// 掃除する候補
|
|
|
|
|
val list = ArrayList<CacheItem>()
|
|
|
|
|
for (item in cache.values) {
|
|
|
|
|
// 最近使われていないものが掃除対象
|
2021-06-20 15:12:25 +02:00
|
|
|
|
if (now - item.timeUsed > 1000L) list.add(item)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 昇順ソート
|
2021-06-20 15:12:25 +02:00
|
|
|
|
list.sortBy { it.timeUsed }
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
|
|
|
|
// 古い物から順に捨てる
|
|
|
|
|
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? {
|
2021-11-06 01:36:00 +01:00
|
|
|
|
val errors = ArrayList<Throwable>()
|
|
|
|
|
|
2020-12-08 21:24:42 +01:00
|
|
|
|
try {
|
|
|
|
|
// APNGをデコード
|
|
|
|
|
val x = ApngFrames.parse(64) { ByteArrayInputStream(data) }
|
|
|
|
|
if (x != null) return x
|
2021-11-06 01:36:00 +01:00
|
|
|
|
error("ApngFrames.parse returns null.")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
if (DEBUG) log.e(ex, "decodeAPNG failed.")
|
2021-11-06 01:36:00 +01:00
|
|
|
|
errors.add(ex)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 通常のビットマップでのロードを試みる
|
|
|
|
|
try {
|
|
|
|
|
val b = decodeBitmap(data, 128)
|
2021-11-06 01:36:00 +01:00
|
|
|
|
if (b != null) return ApngFrames(b)
|
|
|
|
|
error("decodeBitmap returns null.")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
if (DEBUG) log.e(ex, "decodeBitmap failed.")
|
2021-11-06 01:36:00 +01:00
|
|
|
|
errors.add(ex)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SVGのロードを試みる
|
|
|
|
|
try {
|
|
|
|
|
val b = decodeSVG(url, data, 128.toFloat())
|
2021-11-06 01:36:00 +01:00
|
|
|
|
if (b != null) return ApngFrames(b)
|
|
|
|
|
error("decodeSVG returns null.")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
} catch (ex: Throwable) {
|
2022-12-27 03:54:52 +01:00
|
|
|
|
if (DEBUG) log.e(ex, "decodeSVG failed.")
|
2021-11-06 01:36:00 +01:00
|
|
|
|
errors.add(ex)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-11-06 01:36:00 +01:00
|
|
|
|
// 全部ダメだった
|
|
|
|
|
log.e("decode failed. url=$url, errors=${
|
|
|
|
|
errors.joinToString(", ") { "${it.javaClass} ${it.message}" }
|
|
|
|
|
}")
|
2020-12-08 21:24:42 +01:00
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val options = BitmapFactory.Options()
|
|
|
|
|
|
|
|
|
|
private fun decodeBitmap(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
data: ByteArray,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
@Suppress("SameParameterValue") pixelMax: Int,
|
2021-06-13 13:48:48 +02:00
|
|
|
|
): Bitmap? {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
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
|
2021-11-06 01:36:00 +01:00
|
|
|
|
if (w <= 0 || h <= 0) error("decodeBitmap: can't decode bounds.")
|
|
|
|
|
|
2020-12-08 21:24:42 +01:00
|
|
|
|
var bits = 0
|
2021-06-20 15:12:25 +02:00
|
|
|
|
while (w > pixelMax || h > pixelMax) {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
++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(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
url: String,
|
|
|
|
|
data: ByteArray,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
@Suppress("SameParameterValue") pixelMax: Float,
|
2021-06-13 13:48:48 +02:00
|
|
|
|
): Bitmap? {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
try {
|
|
|
|
|
val svg = SVG.getFromInputStream(ByteArrayInputStream(data))
|
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
// the width in pixels, or -1 if there is no width available.
|
|
|
|
|
// the height in pixels, or -1 if there is no height available.
|
|
|
|
|
val srcW = svg.documentWidth
|
|
|
|
|
val srcH = svg.documentHeight
|
|
|
|
|
val aspect = if (srcW <= 0f || srcH <= 0f) {
|
2020-12-08 21:24:42 +01:00
|
|
|
|
// widthやheightの情報がない
|
|
|
|
|
1f
|
|
|
|
|
} else {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
srcW / srcH
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
val dstW: Float
|
|
|
|
|
val dstH: Float
|
2020-12-08 21:24:42 +01:00
|
|
|
|
if (aspect >= 1f) {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
dstW = pixelMax
|
|
|
|
|
dstH = pixelMax / aspect
|
2020-12-08 21:24:42 +01:00
|
|
|
|
} else {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
dstH = pixelMax
|
|
|
|
|
dstW = pixelMax * aspect
|
2020-12-08 21:24:42 +01:00
|
|
|
|
}
|
2021-06-20 15:12:25 +02:00
|
|
|
|
val wCeil = ceil(dstW)
|
|
|
|
|
val hCeil = ceil(dstH)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
|
|
|
|
|
// Create a Bitmap to render our SVG to
|
2021-06-20 15:12:25 +02:00
|
|
|
|
val b = Bitmap.createBitmap(wCeil.toInt(), hCeil.toInt(), Bitmap.Config.ARGB_8888)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
// Create a Canvas to use for rendering
|
|
|
|
|
val canvas = Canvas(b)
|
|
|
|
|
|
|
|
|
|
svg.renderToCanvas(
|
2021-06-13 13:48:48 +02:00
|
|
|
|
canvas,
|
|
|
|
|
if (aspect >= 1f) {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
RectF(0f, hCeil - dstH, dstW, dstH) // 後半はw,hを指定する
|
2021-06-13 13:48:48 +02:00
|
|
|
|
} else {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
RectF(wCeil - dstW, 0f, dstW, dstH) // 後半はw,hを指定する
|
2021-06-13 13:48:48 +02:00
|
|
|
|
}
|
|
|
|
|
)
|
2020-12-08 21:24:42 +01:00
|
|
|
|
return b
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.e(ex, "decodeSVG failed. $url")
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|