mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			269 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Kotlin
		
	
	
	
	
	
			
		
		
	
	
			269 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Kotlin
		
	
	
	
	
	
| 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.content.getSystemService
 | |
| 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.domain.DomainModule
 | |
| import eu.kanade.domain.base.BasePreferences
 | |
| import eu.kanade.domain.ui.UiPreferences
 | |
| import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
 | |
| import eu.kanade.tachiyomi.crash.CrashActivity
 | |
| import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
 | |
| import eu.kanade.tachiyomi.data.cache.ChapterCache
 | |
| 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.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.cancelNotification
 | |
| import eu.kanade.tachiyomi.util.system.isPreviewBuildType
 | |
| import eu.kanade.tachiyomi.util.system.isReleaseBuildType
 | |
| 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 org.acra.config.httpSender
 | |
| import org.acra.ktx.initAcra
 | |
| import org.acra.sender.HttpSender
 | |
| import org.conscrypt.Conscrypt
 | |
| import tachiyomi.core.util.system.logcat
 | |
| import tachiyomi.domain.library.service.LibraryPreferences
 | |
| import tachiyomi.presentation.widget.TachiyomiWidgetManager
 | |
| 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 libraryPreferences: LibraryPreferences by injectLazy()
 | |
|     private val networkPreferences: NetworkPreferences by injectLazy()
 | |
| 
 | |
|     private val disableIncognitoReceiver = DisableIncognitoReceiver()
 | |
|     private val chapterCache: ChapterCache by injectLazy()
 | |
| 
 | |
|     @SuppressLint("LaunchActivityFromNotification")
 | |
|     override fun onCreate() {
 | |
|         super<Application>.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 ->
 | |
|                 if (enabled) {
 | |
|                     disableIncognitoReceiver.register()
 | |
|                     notify(
 | |
|                         Notifications.ID_INCOGNITO_MODE,
 | |
|                         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)
 | |
|                     }
 | |
|                 } else {
 | |
|                     disableIncognitoReceiver.unregister()
 | |
|                     cancelNotification(Notifications.ID_INCOGNITO_MODE)
 | |
|                 }
 | |
|             }
 | |
|             .launchIn(ProcessLifecycleOwner.get().lifecycleScope)
 | |
| 
 | |
|         setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
 | |
| 
 | |
|         // Updates widget update
 | |
|         with(TachiyomiWidgetManager(Injekt.get(), Injekt.get())) {
 | |
|             init(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<NetworkHelper>().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.MangaFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
 | |
|                 add(MangaCoverFetcher.MangaCoverFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
 | |
|                 add(MangaKeyer())
 | |
|                 add(MangaCoverKeyer())
 | |
|             }
 | |
|             callFactory(callFactoryInit)
 | |
|             diskCache(diskCacheInit)
 | |
|             crossfade((300 * this@App.animatorDurationScale).toInt())
 | |
|             allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
 | |
|             if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
 | |
| 
 | |
|             // Coil spawns a new thread for every image load by default
 | |
|             fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
 | |
|             decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
 | |
|             transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
 | |
|         }.build()
 | |
|     }
 | |
| 
 | |
|     override fun onStart(owner: LifecycleOwner) {
 | |
|         SecureActivityDelegate.onApplicationStart()
 | |
|     }
 | |
| 
 | |
|     override fun onStop(owner: LifecycleOwner) {
 | |
|         SecureActivityDelegate.onApplicationStopped()
 | |
| 
 | |
|         if (libraryPreferences.autoClearChapterCache().get()) {
 | |
|             chapterCache.clear()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     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 (isPreviewBuildType || isReleaseBuildType) {
 | |
|             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 }
 | |
|         }
 | |
|     }
 | |
| }
 |