Show a screen while the app migrates after an update

This commit is contained in:
AntsyLich
2026-10-07 22:22:32 +06:00
parent a6ee4bc596
commit fd4f73c908
7 changed files with 114 additions and 5 deletions
@@ -0,0 +1,48 @@
package eu.kanade.presentation.more
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
/**
* Shown instead of the app while migrations from the previous version run, so a slow first launch
* after an update doesn't look like the app hanging.
*/
@Composable
fun AppMigratingScreen() {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.safeDrawingPadding()
.padding(horizontal = 32.dp),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
) {
CircularProgressIndicator()
Text(
text = stringResource(MR.strings.app_migrating_title),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
)
Text(
text = stringResource(MR.strings.app_migrating_summary),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
}
}
@@ -72,6 +72,7 @@ import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
import eu.kanade.presentation.components.IndexingBannerBackgroundColor
import eu.kanade.presentation.more.AppMigratingScreen
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
import eu.kanade.presentation.util.AssistContentScreen
@@ -95,6 +96,7 @@ import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
import eu.kanade.tachiyomi.util.system.updaterEnabled
import eu.kanade.tachiyomi.util.view.setComposeContent
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
@@ -107,6 +109,7 @@ import mihon.app.di.AppGraph
import mihon.app.di.appGraph
import mihon.core.metro.metroGraph
import mihon.core.migration.Migrator
import mihon.domain.database.repository.DatabaseRepository
import mihon.feature.support.SupportUsScreen
import mihon.icons.materialsymbols.MaterialSymbols
import mihon.icons.materialsymbols.automirroredrounded.OpenInNew
@@ -138,6 +141,10 @@ class MainActivity : BaseActivity() {
@Inject private lateinit var chapterCache: ChapterCache
@Inject private lateinit var databaseRepository: DatabaseRepository
private val isAppMigrating = MutableStateFlow(Migrator.isRunning)
@Inject private lateinit var getIncognitoState: GetIncognitoState
@Inject private lateinit var extensionManager: ExtensionManager
@@ -160,7 +167,10 @@ class MainActivity : BaseActivity() {
super.onCreate(savedInstanceState)
Migrator.awaitAndRelease()
lifecycleScope.launch {
Migrator.awaitAndRelease()
isAppMigrating.value = false
}
// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
if (!isTaskRoot) {
@@ -169,6 +179,15 @@ class MainActivity : BaseActivity() {
}
setComposeContent {
val appMigrating by isAppMigrating.collectAsState()
val databaseMigrating by databaseRepository.isMigrating.collectAsState()
if (appMigrating || databaseMigrating) {
AppMigratingScreen()
// Release the splash screen so the reason for the wait shows
LaunchedEffect(Unit) { ready = true }
return@setComposeContent
}
val context = LocalContext.current
var incognito by remember { mutableStateOf(false) }
@@ -5,7 +5,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
object Migrator {
@@ -35,7 +34,10 @@ object Migrator {
result = null
}
fun awaitAndRelease(): Boolean = runBlocking {
await().also { release() }
val isRunning: Boolean
get() = result?.isCompleted == false
suspend fun awaitAndRelease(): Boolean {
return await().also { release() }
}
}
@@ -0,0 +1,16 @@
package mihon.data.database
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import kotlinx.coroutines.flow.MutableStateFlow
import mihon.domain.database.repository.DatabaseRepository
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DatabaseRepositoryImpl : DatabaseRepository {
override val isMigrating = MutableStateFlow(false)
}
@@ -7,6 +7,7 @@ import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.sqlite.execSQL
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlDriver
import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter
import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteConfiguration
@@ -19,6 +20,7 @@ import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import mihon.data.database.DatabaseRepositoryImpl
@ContributesTo(AppScope::class)
@BindingContainer
@@ -30,7 +32,7 @@ object DatabaseBindings {
*/
@Provides
@SingleIn(AppScope::class)
fun providesSqlDriver(context: Context): SqlDriver {
fun providesSqlDriver(context: Context, databaseRepository: DatabaseRepositoryImpl): SqlDriver {
val isLowRam = context.getSystemService<ActivityManager>()?.isLowRamDevice == true
return AndroidxSqliteDriver(
connectionFactory = object : AndroidxSqliteConnectionFactory {
@@ -48,6 +50,14 @@ object DatabaseBindings {
isForeignKeyConstraintsEnabled = true,
concurrencyModel = MultipleReadersSingleWriter(isWal = true, walCount = if (isLowRam) 1 else 4),
),
// Runs before the driver compares the schema version, and onOpen once the schema is ready
onConfigure = {
val version = executePragmaQuery("user_version", { QueryResult.Value(it.apply { next() }.getLong(0)) })
databaseRepository.isMigrating.value = version in 1..<Database.Schema.version
},
onOpen = {
databaseRepository.isMigrating.value = false
},
)
}
@@ -0,0 +1,12 @@
package mihon.domain.database.repository
import kotlinx.coroutines.flow.StateFlow
interface DatabaseRepository {
/**
* True while the database is being migrated from an older schema, which happens on its first use after an
* app update.
*/
val isMigrating: StateFlow<Boolean>
}
@@ -188,6 +188,8 @@
<string name="onboarding_storage_selection_required">A folder must be selected</string>
<string name="onboarding_storage_help_info">Updating from an older version and not sure what to select? Refer to the storage guide for more information.</string>
<string name="onboarding_storage_help_action">Storage guide</string>
<string name="app_migrating_title">Finishing the update</string>
<string name="app_migrating_summary">Getting things ready after the update. This can take a few minutes, so keep the app open.</string>
<string name="onboarding_permission_install_apps">Install apps permission</string>
<string name="onboarding_permission_install_apps_description">To install source extensions.</string>
<string name="onboarding_permission_notifications">Notification permission</string>