Compare commits

...

11 Commits

Author SHA1 Message Date
AntsyLich
f3a2f566c8 Pass uncaught exception to default handler in GlobalExceptionHandler
Fixes #1347
2024-10-19 22:51:01 +06:00
AntsyLich
15e3f28aa3 Rework Firebase setup
Fixes #1332
Closes #1339
2024-10-19 21:22:04 +06:00
AntsyLich
3bf70b230f Address deprecation, suggestion and spotless 2024-10-19 20:19:06 +06:00
AntsyLich
eb3bea8150 Revert "Tweak Preference.collectAsState"
This reverts commit 3bddb55385.

Fixes #1341
2024-10-19 20:02:15 +06:00
Mend Renovate
5612ae0149 Update dependency androidx.compose:compose-bom to v2024.10.00 (#1338) 2024-10-19 20:00:56 +06:00
Mend Renovate
dbf6ad2ca7 Update xml.serialization.version to v0.90.2 (#1331)
* Update xml.serialization.version to v0.90.2

* Fix build

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-19 20:00:30 +06:00
AntsyLich
d2afbfe4ed Change "Invalidate downloads index" to "Reindex downloads" 2024-10-19 17:06:29 +06:00
Mend Renovate
337806d9e1 Update dependency androidx.annotation:annotation to v1.9.0 (#1336) 2024-10-19 16:19:39 +06:00
Mend Renovate
443f6e0ae5 Update dependency androidx.glance:glance-appwidget to v1.1.1 (#1335) 2024-10-19 16:19:07 +06:00
Mend Renovate
572ee2f02a Update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.3 (#1334) 2024-10-19 16:18:38 +06:00
Mend Renovate
ba1343bed8 Update dependency androidx.activity:activity-compose to v1.9.3 (#1333) 2024-10-19 16:17:55 +06:00
14 changed files with 104 additions and 94 deletions

View File

@@ -1,9 +0,0 @@
package mihon.core.firebase
import android.content.Context
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import kotlinx.coroutines.CoroutineScope
object Firebase {
fun setup(context: Context, preference: PrivacyPreferences, scope: CoroutineScope) = Unit
}

View File

@@ -0,0 +1,11 @@
package mihon.core.firebase
import android.content.Context
object FirebaseConfig {
fun init(context: Context) = Unit
fun setAnalyticsEnabled(enabled: Boolean) = Unit
fun setCrashlyticsEnabled(enabled: Boolean) = Unit
}

View File

@@ -51,7 +51,7 @@ import kotlinx.coroutines.flow.onEach
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import mihon.core.firebase.Firebase
import mihon.core.firebase.FirebaseConfig
import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations
import org.conscrypt.Conscrypt
@@ -78,6 +78,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
override fun onCreate() {
super<Application>.onCreate()
patchInjekt()
FirebaseConfig.init(applicationContext)
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
@@ -96,12 +97,12 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
Injekt.importModule(AppModule(this))
Injekt.importModule(DomainModule())
Firebase.setup(applicationContext, privacyPreferences, ProcessLifecycleOwner.get().lifecycleScope)
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 ->
@@ -129,14 +130,22 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
cancelNotification(Notifications.ID_INCOGNITO_MODE)
}
}
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
.launchIn(scope)
privacyPreferences.analytics()
.changes()
.onEach(FirebaseConfig::setAnalyticsEnabled)
.launchIn(scope)
privacyPreferences.crashlytics()
.changes()
.onEach(FirebaseConfig::setCrashlyticsEnabled)
.launchIn(scope)
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
// Updates widget update
with(WidgetManager(Injekt.get(), Injekt.get())) {
init(ProcessLifecycleOwner.get().lifecycleScope)
}
WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) }
if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))

View File

@@ -11,7 +11,6 @@ import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat
import kotlin.system.exitProcess
class GlobalExceptionHandler private constructor(
private val applicationContext: Context,
@@ -31,13 +30,9 @@ class GlobalExceptionHandler private constructor(
}
override fun uncaughtException(thread: Thread, exception: Throwable) {
try {
logcat(priority = LogPriority.ERROR, throwable = exception)
launchActivity(applicationContext, activityToBeLaunched, exception)
exitProcess(0)
} catch (_: Exception) {
defaultHandler.uncaughtException(thread, exception)
}
logcat(priority = LogPriority.ERROR, throwable = exception)
launchActivity(applicationContext, activityToBeLaunched, exception)
defaultHandler.uncaughtException(thread, exception)
}
private fun launchActivity(

View File

@@ -96,13 +96,13 @@ class DownloadCache(
private val diskCacheFile: File
get() = File(context.cacheDir, "dl_index_cache_v3")
private val rootDownloadsDirLock = Mutex()
private val rootDownloadsDirMutex = Mutex()
private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
init {
// Attempt to read cache file
scope.launch {
rootDownloadsDirLock.withLock {
rootDownloadsDirMutex.withLock {
try {
if (diskCacheFile.exists()) {
val diskCache = diskCacheFile.inputStream().use {
@@ -112,7 +112,7 @@ class DownloadCache(
lastRenew = System.currentTimeMillis()
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Failed to initialize disk cache" }
logcat(LogPriority.ERROR, e) { "Failed to initialize from disk cache" }
diskCacheFile.delete()
}
}
@@ -198,7 +198,7 @@ class DownloadCache(
* @param manga the manga of the chapter.
*/
suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
rootDownloadsDirLock.withLock {
rootDownloadsDirMutex.withLock {
// Retrieve the cached source directory or cache a new one
var sourceDir = rootDownloadsDir.sourceDirs[manga.source]
if (sourceDir == null) {
@@ -230,7 +230,7 @@ class DownloadCache(
* @param manga the manga of the chapter.
*/
suspend fun removeChapter(chapter: Chapter, manga: Manga) {
rootDownloadsDirLock.withLock {
rootDownloadsDirMutex.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
@@ -250,7 +250,7 @@ class DownloadCache(
* @param manga the manga of the chapter.
*/
suspend fun removeChapters(chapters: List<Chapter>, manga: Manga) {
rootDownloadsDirLock.withLock {
rootDownloadsDirMutex.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
chapters.forEach { chapter ->
@@ -271,7 +271,7 @@ class DownloadCache(
* @param manga the manga to remove.
*/
suspend fun removeManga(manga: Manga) {
rootDownloadsDirLock.withLock {
rootDownloadsDirMutex.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDirName = provider.getMangaDirName(manga.title)
if (sourceDir.mangaDirs.containsKey(mangaDirName)) {
@@ -283,7 +283,7 @@ class DownloadCache(
}
suspend fun removeSource(source: Source) {
rootDownloadsDirLock.withLock {
rootDownloadsDirMutex.withLock {
rootDownloadsDir.sourceDirs -= source.id
}
@@ -322,10 +322,10 @@ class DownloadCache(
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
rootDownloadsDirLock.withLock {
rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
rootDownloadsDirMutex.withLock {
val updatedRootDir = RootDirectory(storageManager.getDownloadsDirectory())
val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
updatedRootDir.sourceDirs = updatedRootDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() }
.mapNotNull { dir ->
val sourceId = sourceMap[dir.name!!.lowercase()]
@@ -333,36 +333,35 @@ class DownloadCache(
}
.toMap()
rootDownloadsDir.sourceDirs = sourceDirs
updatedRootDir.sourceDirs.values.map { sourceDir ->
async {
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() }
.associate { it.name!! to MangaDirectory(it) }
sourceDirs.values
.map { sourceDir ->
async {
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() }
.associate { it.name!! to MangaDirectory(it) }
sourceDir.mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
.mapNotNull {
when {
// Ignore incomplete downloads
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
// Folder of images
it.isDirectory -> it.name
// CBZ files
it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
// Anything else is irrelevant
else -> null
}
sourceDir.mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
.mapNotNull {
when {
// Ignore incomplete downloads
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
// Folder of images
it.isDirectory -> it.name
// CBZ files
it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
// Anything else is irrelevant
else -> null
}
.toMutableSet()
}
.toMutableSet()
mangaDir.chapterDirs = chapterDirs
}
mangaDir.chapterDirs = chapterDirs
}
}
}
.awaitAll()
rootDownloadsDir = updatedRootDir
}
_isInitializing.emit(false)

View File

@@ -278,12 +278,13 @@ class MainActivity : BaseActivity() {
@Composable
private fun HandleOnNewIntent(context: Context, navigator: Navigator) {
LaunchedEffect(Unit) {
callbackFlow<Intent> {
callbackFlow {
val componentActivity = context as ComponentActivity
val consumer = Consumer<Intent> { trySend(it) }
componentActivity.addOnNewIntentListener(consumer)
awaitClose { componentActivity.removeOnNewIntentListener(consumer) }
}.collectLatest { handleIntentAction(it, navigator) }
}
.collectLatest { handleIntentAction(it, navigator) }
}
}
@@ -339,6 +340,7 @@ class MainActivity : BaseActivity() {
* When custom animation is used, status and navigation bar color will be set to transparent and will be restored
* after the animation is finished.
*/
@Suppress("Deprecation")
private fun setSplashScreenExitAnimation(splashScreen: SplashScreen?) {
val root = findViewById<View>(android.R.id.content)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && splashScreen != null) {

View File

@@ -1,20 +0,0 @@
package mihon.core.firebase
import android.content.Context
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
object Firebase {
fun setup(context: Context, preference: PrivacyPreferences, scope: CoroutineScope) {
preference.analytics().changes().onEach { enabled ->
FirebaseAnalytics.getInstance(context).setAnalyticsCollectionEnabled(enabled)
}.launchIn(scope)
preference.crashlytics().changes().onEach { enabled ->
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enabled)
}.launchIn(scope)
}
}

View File

@@ -0,0 +1,25 @@
package mihon.core.firebase
import android.content.Context
import com.google.firebase.FirebaseApp
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
object FirebaseConfig {
private lateinit var analytics: FirebaseAnalytics
private lateinit var crashlytics: FirebaseCrashlytics
fun init(context: Context) {
analytics = FirebaseAnalytics.getInstance(context)
FirebaseApp.initializeApp(context)
crashlytics = FirebaseCrashlytics.getInstance()
}
fun setAnalyticsEnabled(enabled: Boolean) {
analytics.setAnalyticsCollectionEnabled(enabled)
}
fun setCrashlyticsEnabled(enabled: Boolean) {
crashlytics.isCrashlyticsCollectionEnabled = enabled
}
}

View File

@@ -7,7 +7,7 @@ interpolator_version = "1.0.0"
[libraries]
gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" }
annotation = "androidx.annotation:annotation:1.8.2"
annotation = "androidx.annotation:annotation:1.9.0"
appcompat = "androidx.appcompat:appcompat:1.7.0"
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
@@ -28,7 +28,7 @@ paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pag
interpolator = { group = "androidx.interpolator", name = "interpolator", version.ref = "interpolator_version" }
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.3.2"
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.3.3"
test-ext = "androidx.test.ext:junit-ktx:1.2.1"
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.1"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0"

View File

@@ -1,8 +1,8 @@
[versions]
compose-bom = "2024.09.03"
compose-bom = "2024.10.00"
[libraries]
activity = "androidx.activity:activity-compose:1.9.2"
activity = "androidx.activity:activity-compose:1.9.3"
bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
foundation = { module = "androidx.compose.foundation:foundation" }
animation = { module = "androidx.compose.animation:animation" }
@@ -15,4 +15,4 @@ ui-util = { module = "androidx.compose.ui:ui-util" }
material3-core = { module = "androidx.compose.material3:material3" }
material-icons = { module = "androidx.compose.material:material-icons-extended" }
glance = "androidx.glance:glance-appwidget:1.1.0"
glance = "androidx.glance:glance-appwidget:1.1.1"

View File

@@ -1,7 +1,7 @@
[versions]
kotlin_version = "2.0.21"
serialization_version = "1.7.3"
xml_serialization_version = "0.86.3"
xml_serialization_version = "0.90.2"
[libraries]
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }

View File

@@ -582,7 +582,7 @@
<string name="pref_reset_user_agent_string">Reset default user agent string</string>
<string name="requires_app_restart">Requires app restart to take effect</string>
<string name="cookies_cleared">Cookies cleared</string>
<string name="pref_invalidate_download_cache">Invalidate downloads index</string>
<string name="pref_invalidate_download_cache">Reindex downloads</string>
<string name="pref_invalidate_download_cache_summary">Force app to recheck downloaded chapters</string>
<string name="download_cache_invalidated">Downloads index invalidated</string>
<string name="pref_clear_database">Clear database</string>

View File

@@ -4,12 +4,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.CoroutineScope
import tachiyomi.core.common.preference.Preference
@Composable
fun <T> Preference<T>.collectAsState(scope: CoroutineScope = rememberCoroutineScope()): State<T> {
val flow = remember(this) { stateIn(scope) }
return flow.collectAsState()
fun <T> Preference<T>.collectAsState(): State<T> {
val flow = remember(this) { changes() }
return flow.collectAsState(initial = get())
}

View File

@@ -18,7 +18,7 @@ import kotlinx.serialization.json.decodeFromStream
import logcat.LogPriority
import mihon.core.archive.archiveReader
import mihon.core.archive.epubReader
import nl.adaptivity.xmlutil.AndroidXmlReader
import nl.adaptivity.xmlutil.core.AndroidXmlReader
import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.extension