Files
EhViewer/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt
T
FooIbar 49eb3bc234 Separate thumbnail and gallery image caches
Prevent thumbnail cache from being evicted by gallery cache.
2026-06-08 17:04:49 +08:00

251 lines
8.7 KiB
Kotlin

/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer
import android.app.Application
import android.content.Context
import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composer
import androidx.compose.runtime.tooling.ComposeStackTraceMode
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.coroutineScope
import coil3.EventListener
import coil3.SingletonImageLoader
import coil3.asImage
import coil3.gif.AnimatedImageDecoder
import coil3.gif.GifDecoder
import coil3.network.ConnectivityChecker
import coil3.network.ktor3.KtorNetworkFetcherFactory
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.serviceLoaderEnabled
import coil3.util.DebugLogger
import com.ehviewer.core.database.SearchDatabase
import com.ehviewer.core.database.roomDb
import com.ehviewer.core.files.deleteContent
import com.ehviewer.core.ui.util.initSETConnection
import com.ehviewer.core.util.isAtLeastO
import com.ehviewer.core.util.isAtLeastP
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.launchIO
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withUIContext
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.coil.AnimatedWebPDecoder
import com.hippo.ehviewer.coil.CropBorderInterceptor
import com.hippo.ehviewer.coil.DetectBorderInterceptor
import com.hippo.ehviewer.coil.DownloadThumbInterceptor
import com.hippo.ehviewer.coil.HardwareBitmapInterceptor
import com.hippo.ehviewer.coil.MapExtraInfoInterceptor
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.coil.QrCodeInterceptor
import com.hippo.ehviewer.dailycheck.checkDawn
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.ktbuilder.imageLoader
import com.hippo.ehviewer.ktor.Cronet
import com.hippo.ehviewer.ktor.configureClient
import com.hippo.ehviewer.ktor.configureCommon
import com.hippo.ehviewer.ktor.isCronetAvailable
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.ui.screen.detailCache
import com.hippo.ehviewer.ui.tools.dataStateFlow
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.CrashHandler
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.OSUtils
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import logcat.asLog
import okio.Path.Companion.toOkioPath
import splitties.init.appCtx
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
private val lifecycleScope = lifecycle.coroutineScope
class EhApplication : Application(), SingletonImageLoader.Factory {
override fun onCreate() = with(lifecycleScope) {
initSETConnection()
// Initialize Settings on first access
launchIO {
val mode = Settings.theme.value
if (!isAtLeastS) {
withUIContext {
AppCompatDelegate.setDefaultNightMode(mode)
}
}
LogcatLogger.loggers += AndroidLogcatLogger(LogPriority.VERBOSE)
Settings.saveCrashLog.valueFlow().collect {
if (it) {
LogcatLogger.install()
} else {
LogcatLogger.uninstall()
}
}
}
CrashHandler.install()
super.onCreate()
System.loadLibrary("ehviewer")
launch {
FavouriteStatusRouter.collect { info ->
detailCache[info.gid]?.apply {
favoriteSlot = info.favoriteSlot
favoriteName = info.favoriteName
favoriteNote = info.favoriteNote
}
}
}
launchIO {
EhTagDatabase.launchUpdate()
@Suppress("UNUSED_EXPRESSION")
launch { EhDB }
launch { dataStateFlow.value }
launch { OSUtils.totalMemory }
launch {
if (DownloadManager.labelList.isNotEmpty() && Settings.downloadFilterMode !in Settings.snapshot()) {
Settings.downloadFilterMode.value = DownloadsFilterMode.CUSTOM.flag
}
initialized = true
DownloadManager.readMetadataFromLocal()
}
launch {
FileUtils.cleanupDirectory(AppConfig.externalCrashDir)
FileUtils.cleanupDirectory(AppConfig.externalParseErrorDir)
}
launch { cleanupDownload() }
if (Settings.requestNews.value) {
launch { checkDawn() }
}
}
if (BuildConfig.DEBUG) {
StrictMode.enableDefaults()
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.SourceInformation)
} else {
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.Auto)
}
}
private suspend fun cleanupDownload() {
runCatching {
keepNoMediaFileStatus()
}.onFailure {
logcat(it)
}
runCatching {
clearTempDir()
}.onFailure {
logcat(it)
}
}
private fun clearTempDir() {
AppConfig.tempDir.deleteContent()
AppConfig.externalTempDir?.deleteContent()
}
override fun newImageLoader(context: Context) = context.imageLoader {
interceptorCoroutineContext(Dispatchers.Default)
components {
serviceLoaderEnabled(false)
add(
KtorNetworkFetcherFactory(
httpClient = { ktorClient },
connectivityChecker = { ConnectivityChecker.ONLINE },
),
)
add(MergeInterceptor)
add(DownloadThumbInterceptor)
if (isAtLeastO) {
add(HardwareBitmapInterceptor)
} else {
allowRgb565(true)
}
add(CropBorderInterceptor)
add(DetectBorderInterceptor)
add(QrCodeInterceptor)
add(MapExtraInfoInterceptor)
add(AnimatedWebPDecoder.Factory)
if (isAtLeastP) {
add(AnimatedImageDecoder.Factory(false))
} else {
add(GifDecoder.Factory())
}
}
diskCache { thumbCache }
crossfade(300)
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
if (drawable != null) error(drawable.asImage(true))
if (BuildConfig.DEBUG) {
logger(DebugLogger())
} else {
eventListener(object : EventListener() {
override fun onError(request: ImageRequest, result: ErrorResult) {
logcat("ImageLoader", LogPriority.ERROR) {
"🚨 Failed - ${request.data}\n${result.throwable.asLog()}"
}
}
})
}
}
companion object {
@Volatile
var initialized = false
private set
val ktorClient by lazy {
if (isCronetAvailable && Settings.enableCronet.value) {
HttpClient(Cronet) {
engine { configureClient(Settings.enableQuic.value) }
configureCommon()
}
} else {
HttpClient(OkHttp) {
engine { configureClient() }
configureCommon()
}
}
}
val noRedirectKtorClient by lazy {
HttpClient(ktorClient.engine) {
configureCommon(redirect = false)
}
}
val thumbCache by lazy {
diskCache {
directory(appCtx.cacheDir.toOkioPath() / "thumb")
maxSizeBytes(80L * 1024 * 1024)
}
}
val searchDatabase by lazy { roomDb<SearchDatabase>("search_database.db") }
}
}