Skip to content

Commit 7a0f38d

Browse files
authored
Merge pull request #840 from Crustack/fix/data-migrations
Fix/data migrations
2 parents 39680b6 + 0fffc9c commit 7a0f38d

File tree

6 files changed

+115
-15
lines changed

6 files changed

+115
-15
lines changed

app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.philkes.notallyx.presentation.activity
33
import android.app.Activity
44
import android.app.KeyguardManager
55
import android.content.Intent
6+
import android.database.sqlite.SQLiteBlobTooBigException
67
import android.hardware.biometrics.BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT
78
import android.hardware.biometrics.BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS
89
import android.os.Build
@@ -15,18 +16,28 @@ import androidx.activity.result.contract.ActivityResultContracts
1516
import androidx.activity.viewModels
1617
import androidx.appcompat.app.AppCompatActivity
1718
import androidx.core.content.ContextCompat
19+
import androidx.lifecycle.MutableLiveData
1820
import androidx.lifecycle.lifecycleScope
1921
import androidx.viewbinding.ViewBinding
2022
import com.google.android.material.color.DynamicColors
2123
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2224
import com.philkes.notallyx.NotallyXApplication
2325
import com.philkes.notallyx.R
26+
import com.philkes.notallyx.presentation.setupProgressDialog
2427
import com.philkes.notallyx.presentation.showToast
2528
import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel
2629
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences
2730
import com.philkes.notallyx.presentation.viewmodel.preference.Theme
31+
import com.philkes.notallyx.presentation.viewmodel.progress.MigrationProgress
32+
import com.philkes.notallyx.utils.log
33+
import com.philkes.notallyx.utils.secondsBetween
2834
import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt
35+
import com.philkes.notallyx.utils.splitOversizedNotes
36+
import kotlinx.coroutines.Dispatchers
2937
import kotlinx.coroutines.launch
38+
import kotlinx.coroutines.sync.Mutex
39+
import kotlinx.coroutines.sync.withLock
40+
import kotlinx.coroutines.withContext
3041

3142
abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
3243

@@ -40,6 +51,8 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
4051

4152
override fun onCreate(savedInstanceState: Bundle?) {
4253
super.onCreate(savedInstanceState)
54+
setupGlobalExceptionHandler()
55+
initViewModel()
4356
notallyXApplication = (application as NotallyXApplication)
4457
preferences = NotallyXPreferences.getInstance(notallyXApplication)
4558
if (preferences.useDynamicColors.value) {
@@ -62,6 +75,53 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
6275
}
6376
}
6477

78+
open fun initViewModel() {
79+
baseModel.startObserving()
80+
}
81+
82+
private fun setupGlobalExceptionHandler() {
83+
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
84+
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
85+
if (
86+
throwable is SQLiteBlobTooBigException ||
87+
throwable.cause is SQLiteBlobTooBigException
88+
) {
89+
lifecycleScope.launch {
90+
EXCEPTION_HANDLER_MUTEX.withLock {
91+
val time = System.currentTimeMillis()
92+
if (!isExceptionAlreadyBeingHandled(time)) {
93+
EXCEPTION_HANDLER_MUTEX_LAST_TIMESTAMP = time
94+
val migrationProgress =
95+
MutableLiveData<MigrationProgress>().apply {
96+
setupProgressDialog(this@LockedActivity)
97+
postValue(
98+
MigrationProgress(
99+
R.string.migration_splitting_notes,
100+
indeterminate = true,
101+
)
102+
)
103+
}
104+
log(
105+
TAG,
106+
msg =
107+
"SQLiteBlobTooBigException occurred, trying to fix broken notes...",
108+
)
109+
withContext(Dispatchers.IO) { application.splitOversizedNotes() }
110+
migrationProgress.postValue(
111+
MigrationProgress(R.string.migrating_data, inProgress = false)
112+
)
113+
}
114+
}
115+
}
116+
} else {
117+
previousHandler?.uncaughtException(thread, throwable)
118+
}
119+
}
120+
}
121+
122+
private fun isExceptionAlreadyBeingHandled(time: Long): Boolean =
123+
EXCEPTION_HANDLER_MUTEX_LAST_TIMESTAMP?.let { it.secondsBetween(time) < 20 } ?: false
124+
65125
override fun onResume() {
66126
if (preferences.isLockEnabled) {
67127
if (hasToAuthenticateWithBiometric()) {
@@ -153,4 +213,10 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
153213
}
154214
} ?: false
155215
}
216+
217+
companion object {
218+
private const val TAG = "LockedActivity"
219+
private val EXCEPTION_HANDLER_MUTEX = Mutex()
220+
private var EXCEPTION_HANDLER_MUTEX_LAST_TIMESTAMP: Long? = null
221+
}
156222
}

app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,12 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
131131
baseModel.progress.setupProgressDialog(this)
132132
}
133133

134+
override fun initViewModel() {}
135+
134136
private fun checkForMigrations(savedInstanceState: Bundle?) {
135137
// Run migrations first (blocking dialog), then proceed with initial navigation
136138
val proceed: () -> Unit = {
139+
baseModel.startObserving()
137140
val fragmentIdToLoad = intent.getIntExtra(EXTRA_FRAGMENT_TO_OPEN, -1)
138141
if (fragmentIdToLoad != -1) {
139142
navController.navigate(fragmentIdToLoad, intent.extras)

app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,13 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
110110

111111
lateinit var selectedExportMimeType: ExportMimeType
112112

113-
lateinit var labels: LiveData<List<String>>
114-
lateinit var reminders: LiveData<List<NoteReminder>>
115-
private var allNotes: LiveData<List<BaseNote>>? = null
113+
var labels: LiveData<List<String>> = NotNullLiveData(mutableListOf())
114+
var reminders: LiveData<List<NoteReminder>> = NotNullLiveData(mutableListOf())
115+
private var allNotes: LiveData<List<BaseNote>>? = NotNullLiveData(mutableListOf())
116116
private var allNotesObserver: Observer<List<BaseNote>>? = null
117-
var baseNotes: Content? = null
118-
var deletedNotes: Content? = null
119-
var archivedNotes: Content? = null
117+
var baseNotes: Content? = Content(MutableLiveData(), ::transform)
118+
var deletedNotes: Content? = Content(MutableLiveData(), ::transform)
119+
var archivedNotes: Content? = Content(MutableLiveData(), ::transform)
120120

121121
val folder = NotNullLiveData(Folder.NOTES)
122122

@@ -149,7 +149,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
149149
internal var showRefreshBackupsFolderAfterThemeChange = false
150150
private var labelsHiddenObserver: Observer<Set<String>>? = null
151151

152-
init {
152+
fun startObserving() {
153153
NotallyDatabase.getDatabase(app).observeForever(::init)
154154
folder.observeForever { newFolder ->
155155
searchResults!!.fetch(keyword, newFolder, currentLabel)
@@ -166,7 +166,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
166166
// colors = baseNoteDao.getAllColorsAsync()
167167
reminders = baseNoteDao.getAllRemindersAsync()
168168

169-
allNotes?.removeObserver(allNotesObserver!!)
169+
allNotesObserver?.let { allNotes?.removeObserver(it) }
170170
allNotesObserver = Observer { list -> Cache.list = list }
171171
allNotes = baseNoteDao.getAllAsync()
172172
allNotes!!.observeForever(allNotesObserver!!)
@@ -897,7 +897,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
897897
askForUriPermissions(backupFolderUri)
898898
}
899899
.show()
900-
} catch (e: Exception) {
900+
} catch (_: Exception) {
901901
showRefreshBackupsFolderAfterThemeChange = false
902902
disableBackups()
903903
}

app/src/main/java/com/philkes/notallyx/utils/DataSchemaMigrations.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ private fun Application.moveAttachments(preferences: NotallyXPreferences) {
6161
* end of each truncated note that points to the next note. The link text is included in the body
6262
* and must also fit within the size limit.
6363
*/
64-
private suspend fun Application.splitOversizedNotes() {
64+
suspend fun Application.splitOversizedNotes() {
6565
log(
6666
TAG,
6767
"Running migration 2: Splitting notes exceeding the body size limit (limit: $MAX_BODY_CHAR_LENGTH characters)",
@@ -87,8 +87,14 @@ private suspend fun Application.splitOversizedNotes() {
8787
e,
8888
)
8989
repaired += 1
90-
truncateBodyAndFixSpans(dao, id)
91-
dao.get(id)
90+
try {
91+
truncateBodyAndFixSpans(dao, id)
92+
dao.get(id)
93+
} catch (e: SQLiteBlobTooBigException) {
94+
log(TAG, "Note (id: $id) could not be repaired. Deleting...", e)
95+
dao.delete(id)
96+
null
97+
}
9298
}
9399
if (original == null) return@forEach
94100
if (original.type != Type.NOTE) return@forEach

app/src/main/java/com/philkes/notallyx/utils/MiscExtensions.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.philkes.notallyx.utils
33
import android.util.Patterns
44
import java.util.Calendar
55
import java.util.Locale
6+
import kotlin.math.abs
67

78
fun CharSequence.truncate(limit: Int): CharSequence {
89
return if (length > limit) {
@@ -110,3 +111,7 @@ fun now(): Calendar =
110111
set(Calendar.SECOND, 0)
111112
set(Calendar.MILLISECOND, 0)
112113
}
114+
115+
typealias TimeMillis = Long
116+
117+
fun TimeMillis.secondsBetween(other: TimeMillis): Long = abs(this - other) / 1000

app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.documentfile.provider.DocumentFile
1515
import androidx.lifecycle.MutableLiveData
1616
import com.philkes.notallyx.R
1717
import com.philkes.notallyx.data.NotallyDatabase
18+
import com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
1819
import com.philkes.notallyx.data.imports.ImportProgress
1920
import com.philkes.notallyx.data.imports.ImportStage
2021
import com.philkes.notallyx.data.model.Audio
@@ -121,8 +122,28 @@ suspend fun ContextWrapper.importZip(
121122
SQLiteDatabase.openDatabase(dbFile.path, null, SQLiteDatabase.OPEN_READONLY)
122123

123124
val labelCursor = database.query("Label", null, null, null, null, null, null)
124-
val baseNoteCursor = database.query("BaseNote", null, null, null, null, null, null)
125-
125+
val columns =
126+
arrayOf(
127+
"id",
128+
"type",
129+
"folder",
130+
"color",
131+
"title",
132+
"pinned",
133+
"timestamp",
134+
"modifiedTimestamp",
135+
"labels",
136+
"SUBSTR(body, 1, ${MAX_BODY_CHAR_LENGTH}) AS body",
137+
"spans",
138+
"items",
139+
"images",
140+
"files",
141+
"audios",
142+
"reminders",
143+
"viewMode",
144+
)
145+
val baseNoteCursor =
146+
database.query("BaseNote", columns, null, null, null, null, null)
126147
val labels = labelCursor.toList { cursor -> cursor.toLabel() }
127148

128149
var total = baseNoteCursor.count
@@ -137,7 +158,6 @@ suspend fun ContextWrapper.importZip(
137158
importingBackup?.postValue(ImportProgress(counter++, total))
138159
baseNote
139160
}
140-
141161
delay(1000)
142162

143163
total =

0 commit comments

Comments
 (0)