mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
* Update Simple Icons to 16.34.0 The brand paths are unchanged; only the version in the headers moves. * Use the current Material Symbols names Several symbols were renamed after the Valkyrie migration. people_alt, new_releases, screen_lock_*, screen_rotation and stay_current_* are only aliases in the font now, for group, verified and the mobile_* symbols. done, push_pin and expand_less/expand_more become check, keep and keyboard_arrow_up/down, which draw the same or an almost identical glyph. * Use filled icons where the Material Symbols migration dropped the fill The migration mapped Filled and Default references to the outline symbols, but the classic glyph was often solid, and Outlined Warning, Error and Pause were solid as well. Each call site was compared against the rendered classic glyph, and the solid ones now use RoundedFilled. view_list needs an auto-mirrored filled pack. The outline SVGs this leaves unused are removed. * Show a compact grid icon for the compact display modes Compact and cover-only grids showed the same view_module icon as the comfortable grid. * Generate notification and layout drawables from Material Symbols Notifications and XML layouts can't use ImageVectors, so they relied on hand-kept copies of classic Material drawables. A build-logic task now converts an opt-in list of the module's SVGs with AGP's Svg2Vector into generated resources. sdk-common is taken from AGP's own dependency metadata, so its version always matches AGP. Compose call sites that only needed a drawable (tab close, crop) use the ImageVectors instead. The icons module's R class must be imported as MaterialSymbolsR, enforced by spotless, so it isn't mistaken for the app's R. * Redraw custom icons in the Material Symbols style The reading mode, crop off, mark previous as read and chapter download icons were drawn in the classic style. They are rebuilt from Material Symbols shapes (mobile_gear, mobile_arrow_right/down, crop, arrow_circle_down, downloading) in a new custom-material-symbols module; each SVG header lists where its parts come from. The default reading mode and incognito use mobile_gear and eyeglasses_2 as they are. ReadingMode holds an ImageVector now, and the module's R class follows the same alias rule as MaterialSymbolsR. * Use Material Symbols for the launcher shortcuts Updates uses release_alert because the symbol named new_releases is now a check badge. * Rename the animated icon drawables anim_*_enter no longer fits now that the icons play in both directions. This is a pure rename so the following commits show only content changes. * Use Material Symbols for the bottom navigation icons The library, updates, history and browse animations are rebuilt from the outline and filled symbols but keep their original motion: the white inside is pushed into the ribbon, the "!" and the needle, and the history ring still spins. Updates uses release_alert, like the shortcut. More already matches more_horiz and is unchanged. * Use rounded bars for the manga description caret The animation is unchanged; the bars get rounded ends to match the Material Symbols stroke. * Settle the browse tab icon when selecting and deselecting The compass turns a full turn and settles with overshoot both ways, instead of deselecting by playing the select animation backwards. A rememberAnimatedVectorPainter overload takes separate enter and exit animations and doesn't animate on the first composition.
301 lines
11 KiB
Kotlin
301 lines
11 KiB
Kotlin
package eu.kanade.tachiyomi
|
|
|
|
import android.annotation.SuppressLint
|
|
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.webkit.WebView
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.lifecycle.DefaultLifecycleObserver
|
|
import androidx.lifecycle.LifecycleOwner
|
|
import androidx.lifecycle.ProcessLifecycleOwner
|
|
import androidx.lifecycle.lifecycleScope
|
|
import coil3.ImageLoader
|
|
import coil3.SingletonImageLoader
|
|
import coil3.memory.MemoryCache
|
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
|
import coil3.request.allowRgb565
|
|
import coil3.request.crossfade
|
|
import coil3.util.DebugLogger
|
|
import dev.zacsweers.metro.Inject
|
|
import dev.zacsweers.metro.createGraphFactory
|
|
import eu.kanade.domain.base.BasePreferences
|
|
import eu.kanade.domain.ui.UiPreferences
|
|
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
|
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
|
import eu.kanade.tachiyomi.crash.CrashActivity
|
|
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
|
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
|
|
import eu.kanade.tachiyomi.data.coil.CoverRequestInterceptor
|
|
import eu.kanade.tachiyomi.data.coil.ImageDecoder
|
|
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.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.DeviceUtil
|
|
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.isDebugBuildType
|
|
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 mihon.app.di.AppGraph
|
|
import mihon.app.di.injekt.MetroInjektRegistrar
|
|
import mihon.core.metro.GraphProvider
|
|
import mihon.core.migration.Migration
|
|
import mihon.core.migration.Migrator
|
|
import mihon.telemetry.TelemetryConfig
|
|
import org.conscrypt.Conscrypt
|
|
import tachiyomi.core.common.i18n.stringResource
|
|
import tachiyomi.core.common.preference.Preference
|
|
import tachiyomi.core.common.preference.PreferenceStore
|
|
import tachiyomi.core.common.util.system.logcat
|
|
import tachiyomi.domain.source.service.SourceManager
|
|
import tachiyomi.i18n.MR
|
|
import tachiyomi.presentation.widget.WidgetManager
|
|
import uy.kohesive.injekt.Injekt
|
|
import uy.kohesive.injekt.api.InjektScope
|
|
import java.security.Security
|
|
import mihon.icons.materialsymbols.R as MaterialSymbolsR
|
|
|
|
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory, GraphProvider<AppGraph> {
|
|
|
|
override val graph: AppGraph by lazy {
|
|
createGraphFactory<AppGraph.Factory>().create(context = this, isDebugBuild = isDebugBuildType)
|
|
}
|
|
|
|
@Inject private lateinit var preferenceStore: PreferenceStore
|
|
|
|
@Inject private lateinit var basePreferences: BasePreferences
|
|
|
|
@Inject private lateinit var privacyPreferences: PrivacyPreferences
|
|
|
|
@Inject private lateinit var networkPreferences: NetworkPreferences
|
|
|
|
@Inject private lateinit var uiPreferences: UiPreferences
|
|
|
|
@Inject private lateinit var coverCache: CoverCache
|
|
|
|
@Inject private lateinit var networkHelper: NetworkHelper
|
|
|
|
@Inject private lateinit var sourceManager: SourceManager
|
|
|
|
@Inject private lateinit var widgetManager: WidgetManager
|
|
|
|
@Inject private lateinit var migrations: Set<Migration>
|
|
|
|
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
|
|
|
@SuppressLint("LaunchActivityFromNotification")
|
|
override fun onCreate() {
|
|
super<Application>.onCreate()
|
|
|
|
// Must run before the graph is built, since injecting dependencies initializes WebView and the
|
|
// suffix can't be set once a provider exists in the process. Secondary processes die otherwise.
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
val process = getProcessName()
|
|
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
|
}
|
|
|
|
Injekt = InjektScope(MetroInjektRegistrar(application = this, graphProvider = this))
|
|
graph.inject(this)
|
|
|
|
TelemetryConfig.init(applicationContext)
|
|
|
|
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)
|
|
}
|
|
|
|
setupNotificationChannels()
|
|
|
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
|
|
|
val scope = ProcessLifecycleOwner.get().lifecycleScope
|
|
|
|
// 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(stringResource(MR.strings.pref_incognito_mode))
|
|
setContentText(stringResource(MR.strings.notification_incognito_text))
|
|
setSmallIcon(MaterialSymbolsR.drawable.rounded_eyeglasses_2)
|
|
setOngoing(true)
|
|
|
|
val pendingIntent = PendingIntent.getBroadcast(
|
|
this@App,
|
|
0,
|
|
Intent(ACTION_DISABLE_INCOGNITO_MODE).setPackage(BuildConfig.APPLICATION_ID),
|
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
|
)
|
|
setContentIntent(pendingIntent)
|
|
}
|
|
} else {
|
|
disableIncognitoReceiver.unregister()
|
|
cancelNotification(Notifications.ID_INCOGNITO_MODE)
|
|
}
|
|
}
|
|
.launchIn(scope)
|
|
|
|
privacyPreferences.analytics
|
|
.changes()
|
|
.onEach(TelemetryConfig::setAnalyticsEnabled)
|
|
.launchIn(scope)
|
|
|
|
privacyPreferences.crashlytics
|
|
.changes()
|
|
.onEach(TelemetryConfig::setCrashlyticsEnabled)
|
|
.launchIn(scope)
|
|
|
|
setAppCompatDelegateThemeMode(uiPreferences.themeMode.get())
|
|
|
|
// Updates widget update
|
|
widgetManager.init(scope)
|
|
|
|
if (!LogcatLogger.isInstalled) {
|
|
val minLogPriority = when {
|
|
networkPreferences.verboseLogging.get() -> LogPriority.VERBOSE
|
|
BuildConfig.DEBUG -> LogPriority.DEBUG
|
|
else -> LogPriority.INFO
|
|
}
|
|
LogcatLogger.install()
|
|
LogcatLogger.loggers += AndroidLogcatLogger(minLogPriority)
|
|
}
|
|
|
|
initializeMigrator()
|
|
}
|
|
|
|
private fun initializeMigrator() {
|
|
val preference = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
|
|
logcat {
|
|
"Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE} with ${migrations.size} migration(s)"
|
|
}
|
|
Migrator.initialize(
|
|
old = preference.get(),
|
|
new = BuildConfig.VERSION_CODE,
|
|
migrations = migrations.toList(),
|
|
onMigrationComplete = {
|
|
logcat { "Updating last version to ${BuildConfig.VERSION_CODE}" }
|
|
preference.set(BuildConfig.VERSION_CODE)
|
|
},
|
|
)
|
|
}
|
|
|
|
override fun newImageLoader(context: Context): ImageLoader {
|
|
return ImageLoader.Builder(this).apply {
|
|
val callFactoryLazy = lazy { networkHelper.client }
|
|
components {
|
|
// NetworkFetcher.Factory
|
|
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
|
|
// Decoder.Factory
|
|
add(ImageDecoder.Factory())
|
|
// Fetcher.Factory
|
|
add(BufferedSourceFetcher.Factory())
|
|
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy, coverCache, sourceManager))
|
|
add(MangaCoverFetcher.MangaFactory(callFactoryLazy, coverCache, sourceManager))
|
|
// Keyer
|
|
add(MangaCoverKeyer(coverCache))
|
|
add(MangaKeyer())
|
|
// Interceptor
|
|
add(CoverRequestInterceptor())
|
|
}
|
|
|
|
memoryCache(
|
|
MemoryCache.Builder()
|
|
.maxSizePercent(context)
|
|
.build(),
|
|
)
|
|
|
|
crossfade((300 * this@App.animatorDurationScale).toInt())
|
|
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
|
if (networkPreferences.verboseLogging.get()) logger(DebugLogger())
|
|
|
|
// Coil spawns a new thread for every image load by default
|
|
fetcherCoroutineContext(Dispatchers.IO.limitedParallelism(8))
|
|
decoderCoroutineContext(Dispatchers.IO.limitedParallelism(3))
|
|
}
|
|
.build()
|
|
}
|
|
|
|
override fun onStart(owner: LifecycleOwner) {
|
|
SecureActivityDelegate.onApplicationStart(this)
|
|
}
|
|
|
|
override fun onStop(owner: LifecycleOwner) {
|
|
SecureActivityDelegate.onApplicationStopped(this)
|
|
}
|
|
|
|
override fun getPackageName(): String {
|
|
try {
|
|
// Override the value passed as X-Requested-With in WebView requests
|
|
val stackTrace = Thread.currentThread().stackTrace
|
|
val isChromiumCall = stackTrace.any { trace ->
|
|
trace.className.lowercase() in setOf("org.chromium.base.buildinfo", "org.chromium.base.apkinfo") &&
|
|
trace.methodName.lowercase() in setOf("getall", "getpackagename", "<init>")
|
|
}
|
|
|
|
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
|
|
} catch (_: Exception) {
|
|
}
|
|
|
|
return super.getPackageName()
|
|
}
|
|
|
|
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) {
|
|
ContextCompat.registerReceiver(
|
|
this@App,
|
|
this,
|
|
IntentFilter(ACTION_DISABLE_INCOGNITO_MODE),
|
|
ContextCompat.RECEIVER_NOT_EXPORTED,
|
|
)
|
|
registered = true
|
|
}
|
|
}
|
|
|
|
fun unregister() {
|
|
if (registered) {
|
|
unregisterReceiver(this)
|
|
registered = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|