package eu.kanade.tachiyomi 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.os.Build import android.os.Looper import android.webkit.WebView import androidx.core.app.NotificationManagerCompat import androidx.core.content.getSystemService import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope import coil.ImageLoader import coil.ImageLoaderFactory import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.disk.DiskCache import coil.util.DebugLogger import eu.kanade.data.DatabaseHandler import eu.kanade.domain.DomainModule import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.crash.CrashActivity import eu.kanade.tachiyomi.crash.GlobalExceptionHandler import eu.kanade.tachiyomi.data.coil.DomainMangaKeyer 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.glance.UpdatesGridGlanceWidget 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.WebViewUtil import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.notification import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import logcat.AndroidLogcatLogger import logcat.LogPriority import logcat.LogcatLogger import org.acra.config.httpSender import org.acra.ktx.initAcra import org.acra.sender.HttpSender import org.conscrypt.Conscrypt import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.security.Security class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { private val basePreferences: BasePreferences by injectLazy() private val networkPreferences: NetworkPreferences by injectLazy() private val disableIncognitoReceiver = DisableIncognitoReceiver() @SuppressLint("LaunchActivityFromNotification") override fun onCreate() { super.onCreate() 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) } // Avoid potential crashes if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val process = getProcessName() if (packageName != process) WebView.setDataDirectorySuffix(process) } Injekt.importModule(AppModule(this)) Injekt.importModule(PreferenceModule(this)) Injekt.importModule(DomainModule()) setupAcra() setupNotificationChannels() ProcessLifecycleOwner.get().lifecycle.addObserver(this) // Show notification to disable Incognito Mode when it's enabled basePreferences.incognitoMode().changes() .onEach { enabled -> val notificationManager = NotificationManagerCompat.from(this) if (enabled) { disableIncognitoReceiver.register() val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) { setContentTitle(getString(R.string.pref_incognito_mode)) setContentText(getString(R.string.notification_incognito_text)) setSmallIcon(R.drawable.ic_glasses_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) } notificationManager.notify(Notifications.ID_INCOGNITO_MODE, notification) } else { disableIncognitoReceiver.unregister() notificationManager.cancel(Notifications.ID_INCOGNITO_MODE) } } .launchIn(ProcessLifecycleOwner.get().lifecycleScope) // Updates widget update Injekt.get() .subscribeToList { updatesViewQueries.updates(after = UpdatesGridGlanceWidget.DateLimit.timeInMillis) } .drop(1) .distinctUntilChanged() .onEach { val manager = GlanceAppWidgetManager(this) if (manager.getGlanceIds(UpdatesGridGlanceWidget::class.java).isNotEmpty()) { UpdatesGridGlanceWidget().loadData(it) } } .launchIn(ProcessLifecycleOwner.get().lifecycleScope) if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) { LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) } } override fun newImageLoader(): ImageLoader { return ImageLoader.Builder(this).apply { val callFactoryInit = { Injekt.get().client } val diskCacheInit = { CoilDiskCache.get(this@App) } components { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { add(ImageDecoderDecoder.Factory()) } else { add(GifDecoder.Factory()) } add(TachiyomiImageDecoder.Factory()) add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit))) add(MangaCoverFetcher.DomainMangaFactory(lazy(callFactoryInit), lazy(diskCacheInit))) add(MangaCoverFetcher.MangaCoverFactory(lazy(callFactoryInit), lazy(diskCacheInit))) add(MangaKeyer()) add(DomainMangaKeyer()) add(MangaCoverKeyer()) } callFactory(callFactoryInit) diskCache(diskCacheInit) crossfade((300 * this@App.animatorDurationScale).toInt()) allowRgb565(getSystemService()!!.isLowRamDevice) if (networkPreferences.verboseLogging().get()) logger(DebugLogger()) }.build() } override fun onCreate(owner: LifecycleOwner) { SecureActivityDelegate.onApplicationCreated() } override fun onStop(owner: LifecycleOwner) { SecureActivityDelegate.onApplicationStopped() } override fun getPackageName(): String { // This causes freezes in Android 6/7 for some reason if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { // Override the value passed as X-Requested-With in WebView requests val stackTrace = Looper.getMainLooper().thread.stackTrace val chromiumElement = stackTrace.find { it.className.equals( "org.chromium.base.BuildInfo", ignoreCase = true, ) } if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) { return WebViewUtil.SPOOF_PACKAGE_NAME } } catch (e: Exception) { } } return super.getPackageName() } private fun setupAcra() { if (isDevFlavor.not()) { initAcra { buildConfigClass = BuildConfig::class.java excludeMatchingSharedPreferencesKeys = listOf(".*username.*", ".*password.*", ".*token.*") httpSender { uri = BuildConfig.ACRA_URI httpMethod = HttpSender.Method.PUT } } } } 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) { registerReceiver(this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE)) registered = true } } fun unregister() { if (registered) { unregisterReceiver(this) registered = false } } } } private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE" /** * Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it. */ internal object CoilDiskCache { private const val FOLDER_NAME = "image_cache" private var instance: DiskCache? = null @Synchronized fun get(context: Context): DiskCache { return instance ?: run { val safeCacheDir = context.cacheDir.apply { mkdirs() } // Create the singleton disk cache instance. DiskCache.Builder() .directory(safeCacheDir.resolve(FOLDER_NAME)) .build() .also { instance = it } } } }