yokai/app/src/main/java/eu/kanade/tachiyomi/App.kt
Ahmad Ansori Palembani cae0332ef9
refactor(library): Store sectioned library instead of flatten version of it (#336)
* refactor(library): Store sectioned first before flattening it out

* fix: Fix build, also rename some variables and move stuff around

* chore: Replace findCurrentCategory with currentCategory getter

* fix: Empty category can't be collapsed

* chore: Disable file log for debug build

* fix: Entry always displayed on default category

* refactor: Specify id, source, and url directly from MangaImpl constructor

* refactor: Make LibraryManga not extend MangaImpl

* refactor: Separate placeholder from LibraryManga

* fix: Default category should always be at the very beginning

* fix: Accidentally made the entries invisible

* fix: Default category disappear everytime a new category is added
2025-01-05 18:15:34 +07:00

306 lines
11 KiB
Kotlin

package eu.kanade.tachiyomi
import android.Manifest
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.Application
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.multidex.MultiDex
import co.touchlab.kermit.LogWriter
import co.touchlab.kermit.Logger
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.allowHardware
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.util.DebugLogger
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager
import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.MangaKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.source.SourcePresenter
import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notification
import eu.kanade.tachiyomi.util.system.setToDefault
import java.security.Security
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.conscrypt.Conscrypt
import org.koin.core.context.startKoin
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.core.CrashlyticsLogWriter
import yokai.core.RollingUniFileLogWriter
import yokai.core.di.appModule
import yokai.core.di.domainModule
import yokai.core.di.initExpensiveComponents
import yokai.core.di.preferenceModule
import yokai.core.migration.Migrator
import yokai.core.migration.migrations.migrations
import yokai.domain.base.BasePreferences
import yokai.domain.storage.StorageManager
import yokai.i18n.MR
import yokai.util.lang.getString
open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
val preferences: PreferencesHelper by injectLazy()
val basePreferences: BasePreferences by injectLazy()
val networkPreferences: NetworkPreferences by injectLazy()
private val storageManager: StorageManager by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver()
@SuppressLint("LaunchActivityFromNotification")
override fun onCreate() {
super<Application>.onCreate()
// TLS 1.3 support for Android 10 and below
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
// Avoid potential crashes
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = getProcessName()
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
startKoin {
modules(preferenceModule(this@App), appModule(this@App), domainModule())
}
initExpensiveComponents(this)
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
val scope = ProcessLifecycleOwner.get().lifecycleScope
networkPreferences.verboseLogging().changes()
.onEach { enabled ->
// FlexibleAdapter.enableLogs(if (enabled) Level.VERBOSE else Level.SUPPRESS)
Logger.setToDefault(buildLogWritersToAdd(storageManager.getLogsDirectory(), enabled))
}
.launchIn(scope)
basePreferences.crashReport().changes()
.onEach {
try {
Firebase.crashlytics.setCrashlyticsCollectionEnabled(it)
} catch (e: Exception) {
// Probably already enabled/disabled
}
}
.launchIn(scope)
setupNotificationChannels()
MangaCoverMetadata.load()
preferences.nightMode().changes()
.onEach { AppCompatDelegate.setDefaultNightMode(it) }
.launchIn(scope)
basePreferences.hardwareBitmapThreshold().let { preference ->
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
}
basePreferences.hardwareBitmapThreshold().changes()
.onEach { ImageUtil.hardwareBitmapThreshold = it }
.launchIn(scope)
scope.launchIO {
with(TachiyomiWidgetManager()) { this@App.init() }
}
// Show notification to disable Incognito Mode when it's enabled
preferences.incognitoMode().changes()
.onEach { enabled ->
val notificationManager = NotificationManagerCompat.from(this)
if (enabled) {
disableIncognitoReceiver.register()
val nContext = localeContext
val notification = nContext.notification(Notifications.CHANNEL_INCOGNITO_MODE) {
val incogText = nContext.getString(MR.strings.incognito_mode)
setContentTitle(incogText)
setContentText(nContext.getString(MR.strings.turn_off_, incogText))
setSmallIcon(R.drawable.ic_incognito_24dp)
setOngoing(true)
val pendingIntent = PendingIntent.getBroadcast(
this@App,
0,
Intent(ACTION_DISABLE_INCOGNITO_MODE),
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
setContentIntent(pendingIntent)
}
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS,
) != PackageManager.PERMISSION_GRANTED
) {
return@onEach
}
notificationManager.notify(Notifications.ID_INCOGNITO_MODE, notification)
} else {
disableIncognitoReceiver.unregister()
notificationManager.cancel(Notifications.ID_INCOGNITO_MODE)
}
}
.launchIn(scope)
initializeMigrator()
}
private fun initializeMigrator() {
val preferenceStore = Injekt.get<PreferenceStore>()
val preference = preferenceStore.getInt(
Preference.appStateKey("last_version_code"),
0,
)
if (preference.get() < 141) preference.set(0)
Logger.i { "Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE}" }
Migrator.initialize(
old = preference.get(),
new = BuildConfig.VERSION_CODE,
migrations = migrations,
onMigrationComplete = {
Logger.i { "Updating last version to ${BuildConfig.VERSION_CODE}" }
preference.set(BuildConfig.VERSION_CODE)
},
)
}
override fun onPause(owner: LifecycleOwner) {
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAfter().get() >= 0) {
SecureActivityDelegate.locked = true
}
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
MultiDex.install(this)
}
override fun onLowMemory() {
super.onLowMemory()
LibraryPresenter.onLowMemory()
RecentsPresenter.onLowMemory()
SourcePresenter.onLowMemory()
}
protected open fun setupNotificationChannels() {
Notifications.createChannels(this)
}
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
private var registered = false
override fun onReceive(context: Context, intent: Intent) {
preferences.incognitoMode().set(false)
}
fun register() {
if (!registered) {
ContextCompat.registerReceiver(
this@App,
this,
IntentFilter(ACTION_DISABLE_INCOGNITO_MODE),
ContextCompat.RECEIVER_EXPORTED,
)
registered = true
}
}
fun unregister() {
if (registered) {
unregisterReceiver(this)
registered = false
}
}
}
override fun newImageLoader(context: PlatformContext): ImageLoader {
return ImageLoader.Builder(this@App).apply {
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
components {
// NetworkFetcher.Factory
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
// Decoder.Factory
add(TachiyomiImageDecoder.Factory())
// Fetcher.Factory
add(BufferedSourceFetcher.Factory())
add(MangaCoverFetcher.MangaFactory(callFactoryLazy))
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
// Keyer
add(MangaKeyer())
add(MangaCoverKeyer())
}
crossfade(true)
allowRgb565(this@App.getSystemService<ActivityManager>()!!.isLowRamDevice)
allowHardware(true)
if (networkPreferences.verboseLogging().get()) {
logger(DebugLogger())
}
fetcherCoroutineContext(Dispatchers.IO.limitedParallelism(8))
decoderCoroutineContext(Dispatchers.IO.limitedParallelism(3))
}
.build()
}
}
fun buildLogWritersToAdd(logPath: UniFile?): List<LogWriter> {
val networkPreferences: NetworkPreferences = Injekt.get()
return buildLogWritersToAdd(logPath, networkPreferences.verboseLogging().get())
}
fun buildLogWritersToAdd(
logPath: UniFile?,
isVerbose: Boolean,
) = buildList {
if (!BuildConfig.DEBUG) add(CrashlyticsLogWriter())
if (logPath != null && !BuildConfig.DEBUG) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose))
}
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"