* Requires authentication-confirmation to change biometric lock settings * Prevent double authentications on older APIs when confirming settings changes * Use new AuthPrompt API for app lock With this commit, the app lock will only explicitly require Class 2 biometrics or screen lock credential. Class 3 biometrics are guaranteed to meet Class 2 requirements thus will also be used when available. * Use extension toast (cherry picked from commit 90ab04e81d6ff26656488d3ab4dc3eca4976c5a9) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt # app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSecurityController.kt
293 lines
11 KiB
Kotlin
Executable File
293 lines
11 KiB
Kotlin
Executable File
package eu.kanade.tachiyomi
|
|
|
|
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.graphics.Color
|
|
import android.os.Build
|
|
import android.os.Environment
|
|
import android.webkit.WebView
|
|
import androidx.appcompat.app.AppCompatDelegate
|
|
import androidx.core.app.NotificationManagerCompat
|
|
import androidx.core.content.getSystemService
|
|
import androidx.lifecycle.Lifecycle
|
|
import androidx.lifecycle.LifecycleObserver
|
|
import androidx.lifecycle.OnLifecycleEvent
|
|
import androidx.lifecycle.ProcessLifecycleOwner
|
|
import androidx.lifecycle.lifecycleScope
|
|
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.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.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.data.preference.asImmediateFlow
|
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
|
import eu.kanade.tachiyomi.util.system.notification
|
|
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 exh.util.days
|
|
import io.realm.Realm
|
|
import kotlinx.coroutines.flow.launchIn
|
|
import kotlinx.coroutines.flow.onEach
|
|
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.Security
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Locale
|
|
|
|
open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|
|
|
private val preferences: PreferencesHelper by injectLazy()
|
|
|
|
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
|
|
|
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()
|
|
|
|
// 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))
|
|
|
|
setupNotificationChannels()
|
|
Realm.init(this)
|
|
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
|
setupDebugOverlay()
|
|
}
|
|
|
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
|
|
|
// Show notification to disable Incognito Mode when it's enabled
|
|
preferences.incognitoMode().asFlow()
|
|
.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
|
|
)
|
|
setContentIntent(pendingIntent)
|
|
}
|
|
notificationManager.notify(Notifications.ID_INCOGNITO_MODE, notification)
|
|
} else {
|
|
disableIncognitoReceiver.unregister()
|
|
notificationManager.cancel(Notifications.ID_INCOGNITO_MODE)
|
|
}
|
|
}
|
|
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
|
|
|
preferences.themeMode()
|
|
.asImmediateFlow {
|
|
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)
|
|
}
|
|
|
|
override fun newImageLoader(): ImageLoader {
|
|
return ImageLoader.Builder(this).apply {
|
|
componentRegistry {
|
|
add(TachiyomiImageDecoder(this@App.resources))
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
add(ImageDecoderDecoder(this@App))
|
|
} else {
|
|
add(GifDecoder())
|
|
}
|
|
add(ByteBufferFetcher())
|
|
add(MangaCoverFetcher())
|
|
}
|
|
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
|
crossfade(300)
|
|
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
|
}.build()
|
|
}
|
|
|
|
private fun addAnalytics() {
|
|
if (syDebugVersion != "0") {
|
|
Firebase.analytics.setUserProperty("preview_version", syDebugVersion)
|
|
}
|
|
}
|
|
|
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
|
@Suppress("unused")
|
|
fun onAppBackgrounded() {
|
|
if (!AuthenticatorUtil.isAuthenticating && 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<Printer>(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())
|
|
|
|
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.inWholeMilliseconds)
|
|
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})
|
|
Preview build: $syDebugVersion
|
|
Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
|
|
Android build ID: ${Build.DISPLAY}
|
|
Device brand: ${Build.BRAND}
|
|
Device manufacturer: ${Build.MANUFACTURER}
|
|
Device name: ${Build.DEVICE}
|
|
Device model: ${Build.MODEL}
|
|
Device product name: ${Build.PRODUCT}
|
|
""".trimIndent()
|
|
)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
|
}
|
|
}
|