Skip to content

Commit c9f1ba6

Browse files
committed
Split notes during import to enforce max text length
1 parent e96c228 commit c9f1ba6

16 files changed

Lines changed: 724 additions & 166 deletions

File tree

app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import com.philkes.notallyx.utils.backup.isEqualTo
3030
import com.philkes.notallyx.utils.backup.modifiedNoteBackupExists
3131
import com.philkes.notallyx.utils.backup.scheduleAutoBackup
3232
import com.philkes.notallyx.utils.backup.updateAutoBackup
33-
import com.philkes.notallyx.utils.checkForMigrations
3433
import com.philkes.notallyx.utils.observeOnce
3534
import com.philkes.notallyx.utils.security.UnlockReceiver
3635
import java.util.concurrent.TimeUnit
@@ -53,7 +52,6 @@ class NotallyXApplication : Application(), Application.ActivityLifecycleCallback
5352
registerActivityLifecycleCallbacks(this)
5453
if (isTestRunner()) return
5554
preferences = NotallyXPreferences.getInstance(this)
56-
checkForMigrations()
5755
if (preferences.useDynamicColors.value) {
5856
if (DynamicColors.isDynamicColorAvailable()) {
5957
DynamicColors.applyToActivitiesIfAvailable(this)

app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ data class NoteReminder(
3434
)
3535

3636
/** Maximum allowed size of a note body in MB (~340,000 characters) */
37-
const val MAX_BODY_SIZE_MB = 0.001
37+
const val MAX_BODY_SIZE_MB = 1.5
3838

3939
@Dao
4040
interface BaseNoteDao {
@@ -194,6 +194,10 @@ interface BaseNoteDao {
194194
spans: List<com.philkes.notallyx.data.model.SpanRepresentation>,
195195
)
196196

197+
// Truncate body at DB level without loading the row, to resolve oversized rows safely
198+
@Query("UPDATE BaseNote SET body = substr(body, 1, :limit) WHERE id = :id")
199+
suspend fun truncateBody(id: Long, limit: Int)
200+
197201
/**
198202
* Both id and position can be invalid.
199203
*

app/src/main/java/com/philkes/notallyx/data/dao/CommonDao.kt

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import android.content.ContextWrapper
44
import androidx.room.Dao
55
import androidx.room.Transaction
66
import com.philkes.notallyx.data.NotallyDatabase
7+
import com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
78
import com.philkes.notallyx.data.model.BaseNote
89
import com.philkes.notallyx.data.model.Label
910
import com.philkes.notallyx.data.model.LabelsInBaseNote
11+
import com.philkes.notallyx.data.model.SpanRepresentation
12+
import com.philkes.notallyx.data.model.Type
1013
import com.philkes.notallyx.data.model.createNoteUrl
1114
import com.philkes.notallyx.data.model.getNoteIdFromUrl
1215
import com.philkes.notallyx.data.model.getNoteTypeFromUrl
1316
import com.philkes.notallyx.data.model.isNoteUrl
17+
import com.philkes.notallyx.utils.NoteSplitUtils
1418

1519
@Dao
1620
abstract class CommonDao(private val database: NotallyDatabase) {
@@ -46,7 +50,15 @@ abstract class CommonDao(private val database: NotallyDatabase) {
4650
baseNotes: List<BaseNote>,
4751
labels: List<Label>,
4852
) {
49-
database.getBaseNoteDao().insertSafe(context, baseNotes)
53+
val dao = database.getBaseNoteDao()
54+
// Insert notes, splitting oversized text notes instead of truncating
55+
baseNotes.forEach { note ->
56+
if (note.type == Type.NOTE && note.body.length > MAX_BODY_CHAR_LENGTH) {
57+
NoteSplitUtils.splitAndInsertForImport(note, dao)
58+
} else {
59+
dao.insert(note.copy(id = 0))
60+
}
61+
}
5062
database.getLabelDao().insert(labels)
5163
}
5264

@@ -62,21 +74,31 @@ abstract class CommonDao(private val database: NotallyDatabase) {
6274
labels: List<Label>,
6375
) {
6476
val baseNoteDao = database.getBaseNoteDao()
65-
val newIds = baseNoteDao.insert(baseNotes)
66-
// Build old->new mapping using positional correspondence
77+
78+
// 1) Insert notes with splitting; build mapping from original id -> first-part new id
6779
val idMap = HashMap<Long, Long>(originalIds.size)
68-
val count = minOf(originalIds.size, newIds.size)
69-
for (i in 0 until count) {
70-
idMap[originalIds[i]] = newIds[i]
71-
}
80+
// Keep all inserted note ids with their spans for remapping pass
81+
val insertedParts = ArrayList<Pair<Long, List<SpanRepresentation>>>()
7282

73-
// Remap note links in spans where necessary
7483
for (i in baseNotes.indices) {
75-
val note = baseNotes[i]
76-
val newId = newIds.getOrNull(i) ?: continue
84+
val original = baseNotes[i]
85+
val (firstId, parts) =
86+
if (original.type == Type.NOTE && original.body.length > MAX_BODY_CHAR_LENGTH) {
87+
NoteSplitUtils.splitAndInsertForImport(original, baseNoteDao)
88+
} else {
89+
val newId = baseNoteDao.insert(original.copy(id = 0))
90+
Pair(newId, listOf(Pair(newId, original.spans)))
91+
}
92+
val oldId = originalIds.getOrNull(i)
93+
if (oldId != null) idMap[oldId] = firstId
94+
insertedParts.addAll(parts)
95+
}
96+
97+
// 2) Remap note links in spans for all inserted notes
98+
for ((noteId, spans) in insertedParts) {
7799
var changed = false
78-
val updatedSpans =
79-
note.spans.map { span ->
100+
val updated =
101+
spans.map { span ->
80102
if (span.link && span.linkData?.isNoteUrl() == true) {
81103
val url = span.linkData!!
82104
val oldTargetId = url.getNoteIdFromUrl()
@@ -85,13 +107,11 @@ abstract class CommonDao(private val database: NotallyDatabase) {
85107
if (newTargetId != null) {
86108
changed = true
87109
span.copy(linkData = newTargetId.createNoteUrl(type))
88-
} else {
89-
span
90-
}
110+
} else span
91111
} else span
92112
}
93113
if (changed) {
94-
baseNoteDao.updateSpans(newId, updatedSpans)
114+
baseNoteDao.updateSpans(noteId, updated)
95115
}
96116
}
97117

app/src/main/java/com/philkes/notallyx/data/imports/NotesImporter.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ import androidx.core.net.toUri
77
import androidx.lifecycle.MutableLiveData
88
import com.philkes.notallyx.R
99
import com.philkes.notallyx.data.NotallyDatabase
10+
import com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
1011
import com.philkes.notallyx.data.imports.evernote.EvernoteImporter
1112
import com.philkes.notallyx.data.imports.google.GoogleKeepImporter
1213
import com.philkes.notallyx.data.imports.txt.JsonImporter
1314
import com.philkes.notallyx.data.imports.txt.PlainTextImporter
1415
import com.philkes.notallyx.data.model.Audio
1516
import com.philkes.notallyx.data.model.FileAttachment
1617
import com.philkes.notallyx.data.model.Label
18+
import com.philkes.notallyx.data.model.Type
1719
import com.philkes.notallyx.presentation.viewmodel.NotallyModel
1820
import com.philkes.notallyx.utils.MIME_TYPE_ZIP
21+
import com.philkes.notallyx.utils.NoteSplitUtils
1922
import com.philkes.notallyx.utils.backup.importAudio
2023
import com.philkes.notallyx.utils.backup.importFile
2124
import com.philkes.notallyx.utils.backup.importImage
@@ -61,7 +64,17 @@ class NotesImporter(private val app: Application, private val database: NotallyD
6164
importFiles(images, it, NotallyModel.FileType.IMAGE, progress, totalFiles, counter)
6265
importAudios(audios, it, progress, totalFiles, counter)
6366
}
64-
database.getBaseNoteDao().insertSafe(app, notes)
67+
// Insert notes with split handling for oversized text notes
68+
val dao = database.getBaseNoteDao()
69+
notes.forEach { note ->
70+
if (note.type == Type.NOTE && note.body.length > MAX_BODY_CHAR_LENGTH) {
71+
// Split into parts, preserving spans and adding navigation links
72+
NoteSplitUtils.splitAndInsertForImport(note, dao)
73+
} else {
74+
// Regular insert; ensure id is auto-generated
75+
dao.insert(note.copy(id = 0))
76+
}
77+
}
6578
progress?.postValue(ImportProgress(inProgress = false))
6679
return notes.size
6780
} finally {

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

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import androidx.core.view.children
2020
import androidx.drawerlayout.widget.DrawerLayout
2121
import androidx.lifecycle.LifecycleOwner
2222
import androidx.lifecycle.LiveData
23+
import androidx.lifecycle.MutableLiveData
2324
import androidx.lifecycle.Observer
2425
import androidx.lifecycle.lifecycleScope
2526
import androidx.navigation.NavController
@@ -57,7 +58,10 @@ import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel.Companion.CURRE
5758
import com.philkes.notallyx.presentation.viewmodel.ExportMimeType
5859
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences.Companion.START_VIEW_DEFAULT
5960
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences.Companion.START_VIEW_UNLABELED
61+
import com.philkes.notallyx.presentation.viewmodel.progress.MigrationProgress
62+
import com.philkes.notallyx.utils.LATEST_DATA_SCHEMA
6063
import com.philkes.notallyx.utils.backup.exportNotes
64+
import com.philkes.notallyx.utils.runMigrations
6165
import com.philkes.notallyx.utils.shareNote
6266
import com.philkes.notallyx.utils.showColorSelectDialog
6367
import kotlinx.coroutines.Dispatchers
@@ -102,12 +106,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
102106

103107
preferences.alwaysShowSearchBar.observe(this) { invalidateOptionsMenu() }
104108

105-
val fragmentIdToLoad = intent.getIntExtra(EXTRA_FRAGMENT_TO_OPEN, -1)
106-
if (fragmentIdToLoad != -1) {
107-
navController.navigate(fragmentIdToLoad, intent.extras)
108-
} else if (savedInstanceState == null) {
109-
navigateToStartView()
110-
}
109+
checkForMigrations(savedInstanceState)
111110

112111
onBackPressedDispatcher.addCallback(
113112
this,
@@ -132,6 +131,39 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
132131
baseModel.progress.setupProgressDialog(this)
133132
}
134133

134+
private fun checkForMigrations(savedInstanceState: Bundle?) {
135+
// Run migrations first (blocking dialog), then proceed with initial navigation
136+
val proceed: () -> Unit = {
137+
val fragmentIdToLoad = intent.getIntExtra(EXTRA_FRAGMENT_TO_OPEN, -1)
138+
if (fragmentIdToLoad != -1) {
139+
navController.navigate(fragmentIdToLoad, intent.extras)
140+
} else if (savedInstanceState == null) {
141+
navigateToStartView()
142+
}
143+
}
144+
preferences.setDataSchemaId(1)
145+
if (preferences.dataSchemaId.value < LATEST_DATA_SCHEMA) {
146+
val migrationProgress = MutableLiveData<MigrationProgress>()
147+
migrationProgress.setupProgressDialog(this)
148+
lifecycleScope.launch {
149+
// Initial title
150+
migrationProgress.postValue(
151+
MigrationProgress(R.string.migrating_data, indeterminate = true)
152+
)
153+
application.runMigrations { titleId ->
154+
migrationProgress.postValue(MigrationProgress(titleId, indeterminate = true))
155+
}
156+
// Dismiss
157+
migrationProgress.postValue(
158+
MigrationProgress(R.string.migrating_data, inProgress = false)
159+
)
160+
proceed()
161+
}
162+
} else {
163+
proceed()
164+
}
165+
}
166+
135167
private fun configureEdgeToEdgeInsets() {
136168
WindowCompat.setDecorFitsSystemWindows(window, false)
137169
val navHostFragment = binding.NavHostFragment

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import androidx.recyclerview.widget.RecyclerView
4141
import com.google.android.material.dialog.MaterialAlertDialogBuilder
4242
import com.philkes.notallyx.R
4343
import com.philkes.notallyx.data.NotallyDatabase
44+
import com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
4445
import com.philkes.notallyx.data.model.Audio
4546
import com.philkes.notallyx.data.model.FileAttachment
4647
import com.philkes.notallyx.data.model.Folder
@@ -796,7 +797,11 @@ abstract class EditActivity(private val type: Type) :
796797
?: IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)
797798
?.let { listOf(it) }
798799
if (string != null) {
799-
notallyModel.body = Editable.Factory.getInstance().newEditable(string)
800+
if (string.length > MAX_BODY_CHAR_LENGTH) {
801+
showToast(getString(R.string.note_text_too_long_truncated, MAX_BODY_CHAR_LENGTH))
802+
}
803+
notallyModel.body =
804+
Editable.Factory.getInstance().newEditable(string.take(MAX_BODY_CHAR_LENGTH))
800805
}
801806
if (title != null) {
802807
notallyModel.title = title
@@ -835,7 +840,11 @@ abstract class EditActivity(private val type: Type) :
835840
val title =
836841
intent.getStringExtra(Intent.EXTRA_SUBJECT) ?: intent.data?.let { getFileName(it) }
837842
if (text != null) {
838-
notallyModel.body = Editable.Factory.getInstance().newEditable(text)
843+
if (text.length > MAX_BODY_CHAR_LENGTH) {
844+
showToast(getString(R.string.note_text_too_long_truncated, MAX_BODY_CHAR_LENGTH))
845+
}
846+
notallyModel.body =
847+
Editable.Factory.getInstance().newEditable(text.take(MAX_BODY_CHAR_LENGTH))
839848
}
840849
if (title != null) {
841850
notallyModel.title = title
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.philkes.notallyx.presentation.viewmodel.progress
2+
3+
import com.philkes.notallyx.R
4+
import com.philkes.notallyx.presentation.view.misc.Progress
5+
6+
/**
7+
* Simple progress model for startup data migrations. We use only the title and inProgress flags
8+
* with an indeterminate progress bar.
9+
*/
10+
open class MigrationProgress(
11+
titleId: Int = R.string.migrating_data,
12+
current: Int = 0,
13+
total: Int = 0,
14+
inProgress: Boolean = true,
15+
indeterminate: Boolean = true,
16+
) : Progress(titleId, current, total, inProgress, indeterminate)

0 commit comments

Comments
 (0)