Skip to content

Commit

Permalink
fix: Resolved database migration crashes and improved App Core Manager
Browse files Browse the repository at this point in the history
- Addressed issues causing crashes during database migrations.
- Enhanced the App Core Manager for improved stability and performance.
  • Loading branch information
Mihai-Cristian Condrea committed Dec 27, 2024
1 parent 0a3c101 commit 7b44b5f
Show file tree
Hide file tree
Showing 15 changed files with 292 additions and 188 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ android {
applicationId = "com.d4rk.androidtutorials"
minSdk = 23
targetSdk = 35
versionCode = 98
versionCode = 103
versionName = "1.1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ package com.d4rk.androidtutorials.data.core
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.database.sqlite.SQLiteException
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import androidx.room.Room
import androidx.room.migration.Migration
import com.d4rk.androidtutorials.data.client.KtorClient
import com.d4rk.androidtutorials.data.core.ads.AdsCoreManager
import com.d4rk.androidtutorials.data.core.datastore.DataStoreCoreManager
import com.d4rk.androidtutorials.data.database.AppDatabase
import com.d4rk.androidtutorials.data.database.MIGRATION_1_2
import com.d4rk.androidtutorials.data.database.migrations.MIGRATION_1_2
import com.d4rk.androidtutorials.data.database.migrations.MIGRATION_2_3
import com.d4rk.androidtutorials.data.datastore.DataStore
import com.d4rk.androidtutorials.utils.error.ErrorHandler.handleInitializationFailure
import io.ktor.client.HttpClient
Expand All @@ -27,6 +31,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope

class AppCoreManager : MultiDexApplication() , Application.ActivityLifecycleCallbacks ,
LifecycleObserver {
Expand All @@ -51,7 +56,7 @@ class AppCoreManager : MultiDexApplication() , Application.ActivityLifecycleCall
}
}

private suspend fun initializeApp() = coroutineScope {
private suspend fun initializeApp() = supervisorScope {
val ktor = async { initializeKtorClient() }
val dataBase = async { initializeDatabase() }
val dataStore = async { initializeDataStore() }
Expand Down Expand Up @@ -87,15 +92,28 @@ class AppCoreManager : MultiDexApplication() , Application.ActivityLifecycleCall
private suspend fun initializeDatabase() {
runCatching {
database = Room.databaseBuilder(
context = this@AppCoreManager ,
klass = AppDatabase::class.java ,
context = this@AppCoreManager,
klass = AppDatabase::class.java,
name = "Android Studio Tutorials"
).addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().fallbackToDestructiveMigrationOnDowngrade().build()
)
.addMigrations(migrations = getMigrations())
.fallbackToDestructiveMigration()
.fallbackToDestructiveMigrationOnDowngrade()
.build()

database.openHelper.writableDatabase
}.onFailure {
handleDatabaseError(exception = it as Exception)
}
}

private fun getMigrations() : Array<Migration> {
return arrayOf(
MIGRATION_1_2 ,
MIGRATION_2_3 ,
)
}

private suspend fun initializeDataStore() {
runCatching {
dataStore = DataStore.getInstance(context = this@AppCoreManager)
Expand Down Expand Up @@ -131,19 +149,25 @@ class AppCoreManager : MultiDexApplication() , Application.ActivityLifecycleCall
}

private suspend fun handleDatabaseError(exception : Exception) {
(exception as? IllegalStateException)
?.takeIf { it.message?.contains("Migration failed") == true }
?.let { eraseDatabase() }
if (exception is SQLiteException || (exception is IllegalStateException && exception.message?.contains(other = "Migration failed") == true)) {
eraseDatabase()
}
}

private suspend fun eraseDatabase() {
runCatching {
deleteDatabase("Android Studio Tutorials")
}.onSuccess {
initializeDatabase()
}.onFailure {
logDatabaseError(exception = it as Exception)
}
}

private fun logDatabaseError(exception : Exception) {
Log.e("AppCoreManager" , "Database error: ${exception.message}" , exception)
}

private fun markAppAsLoaded() {
isAppLoaded = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,18 @@ open class AdsCoreManager(protected val context : Context) {
}
isLoadingAd = true
val request : AdRequest = AdRequest.Builder().build()
@Suppress("DEPRECATION") AppOpenAd.load(context ,
AdsConstants.APP_OPEN_UNIT_ID ,
request ,
AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT ,
object : AppOpenAd.AppOpenAdLoadCallback() {
override fun onAdLoaded(ad : AppOpenAd) {
appOpenAd = ad
isLoadingAd = false
loadTime = Date().time
}
@Suppress("DEPRECATION")
AppOpenAd.load(context , AdsConstants.APP_OPEN_UNIT_ID , request , AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT , object : AppOpenAd.AppOpenAdLoadCallback() {
override fun onAdLoaded(ad : AppOpenAd) {
appOpenAd = ad
isLoadingAd = false
loadTime = Date().time
}

override fun onAdFailedToLoad(loadAdError : LoadAdError) {
isLoadingAd = false
}
})
override fun onAdFailedToLoad(loadAdError : LoadAdError) {
isLoadingAd = false
}
})
}

private fun wasLoadTimeLessThanNHoursAgo() : Boolean {
Expand All @@ -77,7 +74,8 @@ open class AdsCoreManager(protected val context : Context) {
onShowAdCompleteListener = object : OnShowAdCompleteListener {
override fun onShowAdComplete() {
}
})
}
)
}

fun showAdIfAvailable(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import kotlinx.coroutines.flow.firstOrNull

open class DataStoreCoreManager(protected val context : Context) {

var isDataStoreLoaded : Boolean = false
var dataStore : DataStore = AppCoreManager.dataStore

suspend fun initializeDataStore() : Boolean = coroutineScope {
suspend fun initializeDataStore() = coroutineScope {

listOf(async {
dataStore.getStartupPage().firstOrNull() ?: BottomBarRoutes.HOME
Expand All @@ -40,8 +39,5 @@ open class DataStoreCoreManager(protected val context : Context) {
} , async {
dataStore.usageAndDiagnostics.firstOrNull() ?: ! BuildConfig.DEBUG
}).awaitAll()

isDataStoreLoaded = true
return@coroutineScope this@DataStoreCoreManager.isDataStoreLoaded
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,10 @@ package com.d4rk.androidtutorials.data.database

import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.d4rk.androidtutorials.data.database.converters.Converters
import com.d4rk.androidtutorials.data.database.dao.FavoriteLessonsDao
import com.d4rk.androidtutorials.data.database.table.FavoriteLessonTable

@Database(entities = [FavoriteLessonTable::class] , version = 2 , exportSchema = false)
@TypeConverters(Converters::class)
@Database(entities = [FavoriteLessonTable::class] , version = 3 , exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun favoriteLessonsDao() : FavoriteLessonsDao
}

val MIGRATION_1_2 = object : Migration(1 , 2) {
override fun migrate(db : SupportSQLiteDatabase) {
db.execSQL(
sql = """
CREATE TABLE IF NOT EXISTS `Favorite Lessons_new` (
`lessonId` TEXT PRIMARY KEY NOT NULL,
`lessonTitle` TEXT NOT NULL,
`lessonDescription` TEXT NOT NULL,
`lessonType` TEXT NOT NULL,
`lessonTags` TEXT NOT NULL,
`thumbnailImageUrl` TEXT NOT NULL,
`squareImageUrl` TEXT NOT NULL,
`deepLinkPath` TEXT NOT NULL,
`isFavorite` INTEGER NOT NULL
)
"""
)

db.execSQL(
sql = """
INSERT INTO `Favorite Lessons_new` (`lessonId`, `lessonTitle`, `lessonDescription`, `lessonType`,
`lessonTags`, `thumbnailImageUrl`, `squareImageUrl`, `deepLinkPath`, `isFavorite`)
SELECT `id`, `title`, `description`, `type`, `tags`, `bannerImageUrl`, `squareImageUrl`, `deepLinkPath`, `isFavorite`
FROM `Favorite Lessons`
"""
)

db.execSQL(sql = "DROP TABLE `Favorite Lessons`")

db.execSQL(sql = "ALTER TABLE `Favorite Lessons_new` RENAME TO `Favorite Lessons`")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.d4rk.androidtutorials.data.database.migrations

import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

val MIGRATION_1_2 : Migration = object : Migration(1 , 2) {
override fun migrate(db : SupportSQLiteDatabase) {
db.execSQL(
sql = """
CREATE TABLE IF NOT EXISTS `Favorite Lessons_new` (
`lessonId` TEXT PRIMARY KEY NOT NULL,
`lessonTitle` TEXT NOT NULL,
`lessonDescription` TEXT NOT NULL,
`lessonType` TEXT NOT NULL,
`lessonTags` TEXT NOT NULL,
`thumbnailImageUrl` TEXT NOT NULL,
`squareImageUrl` TEXT NOT NULL,
`deepLinkPath` TEXT NOT NULL,
`isFavorite` INTEGER NOT NULL
)
""".trimIndent()
)

db.execSQL(
sql = """
INSERT INTO `Favorite Lessons_new` (
`lessonId`, `lessonTitle`, `lessonDescription`, `lessonType`,
`lessonTags`, `thumbnailImageUrl`, `squareImageUrl`, `deepLinkPath`, `isFavorite`
)
SELECT
`lessonId`, -- or whatever your original PK column was
`title`,
`description`,
`type`,
`tags`,
`bannerImageUrl`, -- this becomes thumbnailImageUrl
`squareImageUrl`,
`deepLinkPath`,
`isFavorite`
FROM `Favorite Lessons`
""".trimIndent()
)

db.execSQL(sql = "DROP TABLE `Favorite Lessons`")

db.execSQL(sql = "ALTER TABLE `Favorite Lessons_new` RENAME TO `Favorite Lessons`")
}
}

val MIGRATION_2_3 : Migration = object : Migration(2 , 3) {
override fun migrate(db : SupportSQLiteDatabase) {
db.execSQL(
sql = """
CREATE TABLE IF NOT EXISTS `Favorite Lessons_new` (
`lessonId` TEXT PRIMARY KEY NOT NULL,
`lessonTitle` TEXT NOT NULL,
`lessonDescription` TEXT NOT NULL,
`lessonType` TEXT NOT NULL,
`lessonTags` TEXT NOT NULL,
`thumbnailImageUrl` TEXT NOT NULL,
`squareImageUrl` TEXT NOT NULL,
`deepLinkPath` TEXT NOT NULL,
`isFavorite` INTEGER NOT NULL
)
""".trimIndent()
)

db.execSQL(
sql = """
INSERT INTO `Favorite Lessons_new` (
`lessonId`, `lessonTitle`, `lessonDescription`, `lessonType`,
`lessonTags`, `thumbnailImageUrl`, `squareImageUrl`, `deepLinkPath`, `isFavorite`
)
SELECT
`lessonId`, `lessonTitle`, `lessonDescription`, `lessonType`,
`lessonTags`, `thumbnailImageUrl`, `squareImageUrl`, `deepLinkPath`, `isFavorite`
FROM `Favorite Lessons`
""".trimIndent()
)

db.execSQL(sql = "DROP TABLE `Favorite Lessons`")

db.execSQL(sql = "ALTER TABLE `Favorite Lessons_new` RENAME TO `Favorite Lessons`")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.d4rk.androidtutorials.R

@Composable
fun NoLessonsScreen(
text : Int = R.string.lesson_not_found ,
text : Int = R.string.no_lessons_found ,
icon : ImageVector = Icons.Default.Info ,
iconDescription : String = "No lessons icon"
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.icons.outlined.HeartBroken
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
Expand Down Expand Up @@ -41,21 +42,29 @@ fun FavoritesScreen() {
onDismiss = { viewModel.dismissErrorDialog() })
}

if (isLoading) {
LoadingScreen(progressAlpha)
}
else {
favoriteLessons.firstOrNull()?.lessons?.let { lessonList ->
if (lessonList.isEmpty()) {
NoLessonsScreen(
text = R.string.no_favorite_lessons_found , icon = Icons.Outlined.HeartBroken
when {
isLoading -> {
LoadingScreen(progressAlpha)
}

else -> {
when (val lessons = favoriteLessons.firstOrNull()?.lessons) {
null -> NoLessonsScreen(
text = R.string.error_loading_favorites,
icon = Icons.Outlined.ErrorOutline
)
}
else {
LessonListLayout(
lessons = lessonList , context = context , visibilityStates = visibilityStates

emptyList<UiHomeScreen>() -> NoLessonsScreen(
text = R.string.no_favorite_lessons_found,
icon = Icons.Outlined.HeartBroken
)

else -> LessonListLayout(
lessons = lessons,
context = context,
visibilityStates = visibilityStates
)
}
} ?: NoLessonsScreen()
}
}
}
Loading

0 comments on commit 7b44b5f

Please sign in to comment.