mihon/app/src/main/java/eu/kanade/tachiyomi/App.kt
Andreas 0086743a53
Use 1.x preference abstraction (#8020)
* Use 1.x preference abstraction

- Uses SharedPreferences compared to 1.x impl which uses DataStore but it breaks all settings screens currently
- Move PreferencesHelper to new PreferenceStore
  - PreferencesHelper should be split into smaller preference stores and be in core or domain
- Remove flow preferences as new PreferenceStore handles changes for us

Co-authored-by: inorichi <3521738+inorichi@users.noreply.github.com>

* Fix PreferenceMutableState not updating

* Fix changes not emitting on first subscription

Co-authored-by: inorichi <3521738+inorichi@users.noreply.github.com>
2022-09-17 11:48:24 -04:00

274 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.appcompat.app.AppCompatDelegate
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.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.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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.preference.asHotFlow
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 preferences: PreferencesHelper by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver()
@SuppressLint("LaunchActivityFromNotification")
override fun onCreate() {
super<Application>.onCreate()
// 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
preferences.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)
preferences.themeMode()
.asHotFlow {
AppCompatDelegate.setDefaultNightMode(
when (it) {
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
},
)
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
// Updates widget update
Injekt.get<DatabaseHandler>()
.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<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.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<ActivityManager>()!!.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) {
preferences.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 }
}
}
}