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.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() 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().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()!!.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 { 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"