package eu.kanade.tachiyomi import android.app.ActivityManager import android.app.Application import android.content.Context import android.content.res.Configuration import android.graphics.Color import android.os.Build import android.os.Environment import androidx.core.content.getSystemService import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDex import coil.ImageLoader import coil.ImageLoaderFactory import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import com.elvishew.xlog.LogConfiguration import com.elvishew.xlog.LogLevel import com.elvishew.xlog.XLog import com.elvishew.xlog.printer.AndroidPrinter import com.elvishew.xlog.printer.Printer import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.security.ProviderInstaller import com.google.firebase.analytics.ktx.analytics import com.google.firebase.ktx.Firebase import com.ms_square.debugoverlay.DebugOverlay import com.ms_square.debugoverlay.modules.FpsModule import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.util.system.LocaleHelper import exh.debug.DebugToggles import exh.log.CrashlyticsPrinter import exh.log.EHDebugModeOverlay import exh.log.EHLogLevel import exh.log.EnhancedFilePrinter import exh.log.XLogTree import exh.log.xLogD import exh.log.xLogE import exh.syDebugVersion import io.realm.Realm import org.conscrypt.Conscrypt import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import java.security.NoSuchAlgorithmException import java.security.Security import java.text.SimpleDateFormat import java.util.Locale import javax.net.ssl.SSLContext import kotlin.time.ExperimentalTime import kotlin.time.days open class App : Application(), LifecycleObserver, ImageLoaderFactory { private val preferences: PreferencesHelper by injectLazy() override fun onCreate() { super.onCreate() // if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) setupExhLogging() // EXH logging Timber.plant(XLogTree()) // SY Redirect Timber to XLog if (!BuildConfig.DEBUG) addAnalytics() workaroundAndroid7BrokenSSL() // TLS 1.3 support for Android < 10 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { Security.insertProviderAt(Conscrypt.newProvider(), 1) } Injekt.importModule(AppModule(this)) setupNotificationChannels() Realm.init(this) if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) { setupDebugOverlay() } LocaleHelper.updateConfiguration(this, resources.configuration) ProcessLifecycleOwner.get().lifecycle.addObserver(this) // Reset Incognito Mode on relaunch preferences.incognitoMode().set(false) } override fun attachBaseContext(base: Context) { super.attachBaseContext(base) MultiDex.install(this) } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) LocaleHelper.updateConfiguration(this, newConfig, true) } override fun newImageLoader(): ImageLoader { return ImageLoader.Builder(this).apply { componentRegistry { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { add(ImageDecoderDecoder(this@App)) } else { add(GifDecoder()) } add(ByteBufferFetcher()) add(MangaCoverFetcher()) } okHttpClient(Injekt.get().coilClient) crossfade(300) allowRgb565(getSystemService()!!.isLowRamDevice) }.build() } private fun workaroundAndroid7BrokenSSL() { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1 ) { try { SSLContext.getInstance("TLSv1.2") } catch (e: NoSuchAlgorithmException) { xLogE("Could not install Android 7 broken SSL workaround!", e) } try { ProviderInstaller.installIfNeeded(applicationContext) } catch (e: GooglePlayServicesRepairableException) { xLogE("Could not install Android 7 broken SSL workaround!", e) } catch (e: GooglePlayServicesNotAvailableException) { xLogE("Could not install Android 7 broken SSL workaround!", e) } } } private fun addAnalytics() { if (syDebugVersion != "0") { Firebase.analytics.setUserProperty("preview_version", syDebugVersion) } } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) @Suppress("unused") fun onAppBackgrounded() { if (preferences.lockAppAfter().get() >= 0) { SecureActivityDelegate.locked = true } } protected open fun setupNotificationChannels() { Notifications.createChannels(this) } // EXH private fun setupExhLogging() { EHLogLevel.init(this) val logLevel = when { EHLogLevel.shouldLog(EHLogLevel.EXTREME) -> LogLevel.ALL EHLogLevel.shouldLog(EHLogLevel.EXTRA) || BuildConfig.DEBUG -> LogLevel.DEBUG else -> LogLevel.WARN } val logConfig = LogConfiguration.Builder() .logLevel(logLevel) .disableStackTrace() .disableBorder() .build() val printers = mutableListOf(AndroidPrinter()) val logFolder = File( Environment.getExternalStorageDirectory().absolutePath + File.separator + getString(R.string.app_name), "logs" ) val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) @OptIn(ExperimentalTime::class) printers += EnhancedFilePrinter .Builder(logFolder.absolutePath) { fileNameGenerator = object : DateFileNameGenerator() { override fun generateFileName(logLevel: Int, timestamp: Long): String { return super.generateFileName( logLevel, timestamp ) + "-${BuildConfig.BUILD_TYPE}.log" } } flattener { timeMillis, level, tag, message -> "${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message" } cleanStrategy = FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()) backupStrategy = NeverBackupStrategy() } // Install Crashlytics in prod if (!BuildConfig.DEBUG) { printers += CrashlyticsPrinter(LogLevel.ERROR) } XLog.init( logConfig, *printers.toTypedArray() ) xLogD("Application booting...") xLogD( "App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" + "Preview build: $syDebugVersion\n" + "Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" + "Android build ID: ${Build.DISPLAY}\n" + "Device brand: ${Build.BRAND}\n" + "Device manufacturer: ${Build.MANUFACTURER}\n" + "Device name: ${Build.DEVICE}\n" + "Device model: ${Build.MODEL}\n" + "Device product name: ${Build.PRODUCT}" ) } // EXH private fun setupDebugOverlay() { try { DebugOverlay.Builder(this) .modules(FpsModule(), EHDebugModeOverlay(this)) .bgColor(Color.parseColor("#7F000000")) .notification(false) .allowSystemLayer(false) .build() .install() } catch (e: IllegalStateException) { // Crashes if app is in background xLogE("Failed to initialize debug overlay, app in background?", e) } } }