package eu.kanade.tachiyomi import android.annotation.SuppressLint 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.os.Build import android.webkit.WebView import androidx.core.content.ContextCompat import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope import coil3.ImageLoader import coil3.SingletonImageLoader import coil3.memory.MemoryCache import coil3.network.okhttp.OkHttpNetworkFetcherFactory import coil3.request.allowRgb565 import coil3.request.crossfade import coil3.util.DebugLogger import dev.zacsweers.metro.Inject import dev.zacsweers.metro.createGraphFactory import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.tachiyomi.core.security.PrivacyPreferences import eu.kanade.tachiyomi.crash.CrashActivity import eu.kanade.tachiyomi.crash.GlobalExceptionHandler import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher import eu.kanade.tachiyomi.data.coil.ImageDecoder 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.notification.Notifications import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.isDebugBuildType import eu.kanade.tachiyomi.util.system.notify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import logcat.AndroidLogcatLogger import logcat.LogPriority import logcat.LogcatLogger import mihon.app.di.AppGraph import mihon.app.di.injekt.MetroInjektRegistrar import mihon.core.metro.GraphProvider import mihon.core.migration.Migration import mihon.core.migration.Migrator import mihon.telemetry.TelemetryConfig import org.conscrypt.Conscrypt import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.source.service.SourceManager import tachiyomi.i18n.MR import tachiyomi.presentation.widget.WidgetManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.InjektScope import java.security.Security class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory, GraphProvider { override val graph: AppGraph by lazy { createGraphFactory().create(context = this, isDebugBuild = isDebugBuildType) } @Inject private lateinit var preferenceStore: PreferenceStore @Inject private lateinit var basePreferences: BasePreferences @Inject private lateinit var privacyPreferences: PrivacyPreferences @Inject private lateinit var networkPreferences: NetworkPreferences @Inject private lateinit var uiPreferences: UiPreferences @Inject private lateinit var coverCache: CoverCache @Inject private lateinit var networkHelper: NetworkHelper @Inject private lateinit var sourceManager: SourceManager @Inject private lateinit var widgetManager: WidgetManager @Inject private lateinit var migrations: Set private val disableIncognitoReceiver = DisableIncognitoReceiver() @SuppressLint("LaunchActivityFromNotification") override fun onCreate() { super.onCreate() // Must run before the graph is built, since injecting dependencies initializes WebView and the // suffix can't be set once a provider exists in the process. Secondary processes die otherwise. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val process = getProcessName() if (packageName != process) WebView.setDataDirectorySuffix(process) } Injekt = InjektScope(MetroInjektRegistrar(application = this, graphProvider = this)) graph.inject(this) TelemetryConfig.init(applicationContext) GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java) // TLS 1.3 support for Android < 10 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { Security.insertProviderAt(Conscrypt.newProvider(), 1) } setupNotificationChannels() ProcessLifecycleOwner.get().lifecycle.addObserver(this) val scope = ProcessLifecycleOwner.get().lifecycleScope // Show notification to disable Incognito Mode when it's enabled basePreferences.incognitoMode.changes() .onEach { enabled -> if (enabled) { disableIncognitoReceiver.register() notify( Notifications.ID_INCOGNITO_MODE, Notifications.CHANNEL_INCOGNITO_MODE, ) { setContentTitle(stringResource(MR.strings.pref_incognito_mode)) setContentText(stringResource(MR.strings.notification_incognito_text)) setSmallIcon(R.drawable.ic_glasses_24dp) setOngoing(true) val pendingIntent = PendingIntent.getBroadcast( this@App, 0, Intent(ACTION_DISABLE_INCOGNITO_MODE).setPackage(BuildConfig.APPLICATION_ID), PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, ) setContentIntent(pendingIntent) } } else { disableIncognitoReceiver.unregister() cancelNotification(Notifications.ID_INCOGNITO_MODE) } } .launchIn(scope) privacyPreferences.analytics .changes() .onEach(TelemetryConfig::setAnalyticsEnabled) .launchIn(scope) privacyPreferences.crashlytics .changes() .onEach(TelemetryConfig::setCrashlyticsEnabled) .launchIn(scope) setAppCompatDelegateThemeMode(uiPreferences.themeMode.get()) // Updates widget update widgetManager.init(scope) if (!LogcatLogger.isInstalled) { val minLogPriority = when { networkPreferences.verboseLogging.get() -> LogPriority.VERBOSE BuildConfig.DEBUG -> LogPriority.DEBUG else -> LogPriority.INFO } LogcatLogger.install() LogcatLogger.loggers += AndroidLogcatLogger(minLogPriority) } initializeMigrator() } private fun initializeMigrator() { val preference = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0) logcat { "Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE} with ${migrations.size} migration(s)" } Migrator.initialize( old = preference.get(), new = BuildConfig.VERSION_CODE, migrations = migrations.toList(), onMigrationComplete = { logcat { "Updating last version to ${BuildConfig.VERSION_CODE}" } preference.set(BuildConfig.VERSION_CODE) }, ) } override fun newImageLoader(context: Context): ImageLoader { return ImageLoader.Builder(this).apply { val callFactoryLazy = lazy { networkHelper.client } components { // NetworkFetcher.Factory add(OkHttpNetworkFetcherFactory(callFactoryLazy::value)) // Decoder.Factory add(ImageDecoder.Factory()) // Fetcher.Factory add(BufferedSourceFetcher.Factory()) add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy, coverCache, sourceManager)) add(MangaCoverFetcher.MangaFactory(callFactoryLazy, coverCache, sourceManager)) // Keyer add(MangaCoverKeyer(coverCache)) add(MangaKeyer()) } memoryCache( MemoryCache.Builder() .maxSizePercent(context) .build(), ) crossfade((300 * this@App.animatorDurationScale).toInt()) allowRgb565(DeviceUtil.isLowRamDevice(this@App)) if (networkPreferences.verboseLogging.get()) logger(DebugLogger()) // Coil spawns a new thread for every image load by default fetcherCoroutineContext(Dispatchers.IO.limitedParallelism(8)) decoderCoroutineContext(Dispatchers.IO.limitedParallelism(3)) } .build() } override fun onStart(owner: LifecycleOwner) { SecureActivityDelegate.onApplicationStart(this) } override fun onStop(owner: LifecycleOwner) { SecureActivityDelegate.onApplicationStopped(this) } override fun getPackageName(): String { try { // Override the value passed as X-Requested-With in WebView requests val stackTrace = Thread.currentThread().stackTrace val isChromiumCall = stackTrace.any { trace -> trace.className.lowercase() in setOf("org.chromium.base.buildinfo", "org.chromium.base.apkinfo") && trace.methodName.lowercase() in setOf("getall", "getpackagename", "") } if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext) } catch (_: Exception) { } return super.getPackageName() } private fun setupNotificationChannels() { try { Notifications.createChannels(this) } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" } } } private inner class DisableIncognitoReceiver : BroadcastReceiver() { private var registered = false override fun onReceive(context: Context, intent: Intent) { basePreferences.incognitoMode.set(false) } fun register() { if (!registered) { ContextCompat.registerReceiver( this@App, this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE), ContextCompat.RECEIVER_NOT_EXPORTED, ) registered = true } } fun unregister() { if (registered) { unregisterReceiver(this) registered = false } } } } private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"