Skip to content

Commit 2f4fb2f

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

19 files changed

Lines changed: 765 additions & 210 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: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import com.philkes.notallyx.data.model.LabelsInBaseNote
1919
import com.philkes.notallyx.data.model.ListItem
2020
import com.philkes.notallyx.data.model.Reminder
2121
import com.philkes.notallyx.data.model.Type
22+
import com.philkes.notallyx.presentation.getQuantityString
2223
import com.philkes.notallyx.presentation.showToast
2324
import com.philkes.notallyx.utils.charLimit
2425
import com.philkes.notallyx.utils.log
25-
import kotlin.text.compareTo
2626

2727
data class NoteIdReminder(val id: Long, val reminders: List<Reminder>)
2828

@@ -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 {
@@ -45,7 +45,13 @@ interface BaseNoteDao {
4545

4646
private fun BaseNote.truncated(): Pair<Boolean, BaseNote> {
4747
return if (body.length > MAX_BODY_CHAR_LENGTH) {
48-
return Pair(true, copy(body = body.take(MAX_BODY_CHAR_LENGTH)))
48+
return Pair(
49+
true,
50+
copy(
51+
body = body.take(MAX_BODY_CHAR_LENGTH),
52+
spans = spans.filter { it.isInsideBounds() },
53+
),
54+
)
4955
} else Pair(false, this)
5056
}
5157

@@ -79,17 +85,19 @@ interface BaseNoteDao {
7985
}
8086
note
8187
}
82-
context.log(
83-
TAG,
84-
"${truncatedNotes.size} Notes are too big to save, they were truncated to $truncatedCharacterSize characters",
85-
)
86-
context.showToast(
87-
context.getString(
88-
R.string.notes_too_big_truncating,
89-
truncatedNotes.size,
90-
truncatedCharacterSize,
88+
if (truncatedNotes.isNotEmpty()) {
89+
context.log(
90+
TAG,
91+
"${truncatedNotes.size} Notes are too big to save, they were truncated to $truncatedCharacterSize characters",
92+
)
93+
context.showToast(
94+
context.getQuantityString(
95+
R.plurals.notes_too_big_truncating,
96+
truncatedNotes.size,
97+
truncatedCharacterSize,
98+
)
9199
)
92-
)
100+
}
93101
return insert(notes)
94102
}
95103

@@ -194,6 +202,10 @@ interface BaseNoteDao {
194202
spans: List<com.philkes.notallyx.data.model.SpanRepresentation>,
195203
)
196204

205+
// Truncate body at DB level without loading the row, to resolve oversized rows safely
206+
@Query("UPDATE BaseNote SET body = substr(body, 1, :limit) WHERE id = :id")
207+
suspend fun truncateBody(id: Long, limit: Int)
208+
197209
/**
198210
* Both id and position can be invalid.
199211
*

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

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package com.philkes.notallyx.data.dao
22

3-
import android.content.ContextWrapper
43
import androidx.room.Dao
54
import androidx.room.Transaction
65
import com.philkes.notallyx.data.NotallyDatabase
6+
import com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
77
import com.philkes.notallyx.data.model.BaseNote
88
import com.philkes.notallyx.data.model.Label
99
import com.philkes.notallyx.data.model.LabelsInBaseNote
10+
import com.philkes.notallyx.data.model.SpanRepresentation
11+
import com.philkes.notallyx.data.model.Type
1012
import com.philkes.notallyx.data.model.createNoteUrl
1113
import com.philkes.notallyx.data.model.getNoteIdFromUrl
1214
import com.philkes.notallyx.data.model.getNoteTypeFromUrl
1315
import com.philkes.notallyx.data.model.isNoteUrl
16+
import com.philkes.notallyx.utils.NoteSplitUtils
1417

1518
@Dao
1619
abstract class CommonDao(private val database: NotallyDatabase) {
@@ -41,12 +44,16 @@ abstract class CommonDao(private val database: NotallyDatabase) {
4144
}
4245

4346
@Transaction
44-
open suspend fun importBackup(
45-
context: ContextWrapper,
46-
baseNotes: List<BaseNote>,
47-
labels: List<Label>,
48-
) {
49-
database.getBaseNoteDao().insertSafe(context, baseNotes)
47+
open suspend fun importBackup(baseNotes: List<BaseNote>, labels: List<Label>) {
48+
val dao = database.getBaseNoteDao()
49+
// Insert notes, splitting oversized text notes instead of truncating
50+
baseNotes.forEach { note ->
51+
if (note.type == Type.NOTE && note.body.length > MAX_BODY_CHAR_LENGTH) {
52+
NoteSplitUtils.splitAndInsertForImport(note, dao)
53+
} else {
54+
dao.insert(note.copy(id = 0))
55+
}
56+
}
5057
database.getLabelDao().insert(labels)
5158
}
5259

@@ -62,21 +69,31 @@ abstract class CommonDao(private val database: NotallyDatabase) {
6269
labels: List<Label>,
6370
) {
6471
val baseNoteDao = database.getBaseNoteDao()
65-
val newIds = baseNoteDao.insert(baseNotes)
66-
// Build old->new mapping using positional correspondence
72+
73+
// 1) Insert notes with splitting; build mapping from original id -> first-part new id
6774
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-
}
75+
// Keep all inserted note ids with their spans for remapping pass
76+
val insertedParts = ArrayList<Pair<Long, List<SpanRepresentation>>>()
7277

73-
// Remap note links in spans where necessary
7478
for (i in baseNotes.indices) {
75-
val note = baseNotes[i]
76-
val newId = newIds.getOrNull(i) ?: continue
79+
val original = baseNotes[i]
80+
val (firstId, parts) =
81+
if (original.type == Type.NOTE && original.body.length > MAX_BODY_CHAR_LENGTH) {
82+
NoteSplitUtils.splitAndInsertForImport(original, baseNoteDao)
83+
} else {
84+
val newId = baseNoteDao.insert(original.copy(id = 0))
85+
Pair(newId, listOf(Pair(newId, original.spans)))
86+
}
87+
val oldId = originalIds.getOrNull(i)
88+
if (oldId != null) idMap[oldId] = firstId
89+
insertedParts.addAll(parts)
90+
}
91+
92+
// 2) Remap note links in spans for all inserted notes
93+
for ((noteId, spans) in insertedParts) {
7794
var changed = false
78-
val updatedSpans =
79-
note.spans.map { span ->
95+
val updated =
96+
spans.map { span ->
8097
if (span.link && span.linkData?.isNoteUrl() == true) {
8198
val url = span.linkData!!
8299
val oldTargetId = url.getNoteIdFromUrl()
@@ -85,13 +102,11 @@ abstract class CommonDao(private val database: NotallyDatabase) {
85102
if (newTargetId != null) {
86103
changed = true
87104
span.copy(linkData = newTargetId.createNoteUrl(type))
88-
} else {
89-
span
90-
}
105+
} else span
91106
} else span
92107
}
93108
if (changed) {
94-
baseNoteDao.updateSpans(newId, updatedSpans)
109+
baseNoteDao.updateSpans(noteId, updated)
95110
}
96111
}
97112

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/data/model/SpanRepresentation.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.philkes.notallyx.data.model
22

3+
import com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
4+
35
data class SpanRepresentation(
46
var start: Int,
57
var end: Int,
@@ -18,4 +20,6 @@ data class SpanRepresentation(
1820
fun isEqualInSize(representation: SpanRepresentation): Boolean {
1921
return start == representation.start && end == representation.end
2022
}
23+
24+
fun isInsideBounds(): Boolean = start < MAX_BODY_CHAR_LENGTH && end <= MAX_BODY_CHAR_LENGTH
2125
}

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

Lines changed: 37 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,38 @@ 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+
if (preferences.dataSchemaId.value < LATEST_DATA_SCHEMA) {
145+
val migrationProgress = MutableLiveData<MigrationProgress>()
146+
migrationProgress.setupProgressDialog(this)
147+
lifecycleScope.launch {
148+
// Initial title
149+
migrationProgress.postValue(
150+
MigrationProgress(R.string.migrating_data, indeterminate = true)
151+
)
152+
application.runMigrations { titleId ->
153+
migrationProgress.postValue(MigrationProgress(titleId, indeterminate = true))
154+
}
155+
// Dismiss
156+
migrationProgress.postValue(
157+
MigrationProgress(R.string.migrating_data, inProgress = false)
158+
)
159+
proceed()
160+
}
161+
} else {
162+
proceed()
163+
}
164+
}
165+
135166
private fun configureEdgeToEdgeInsets() {
136167
WindowCompat.setDecorFitsSystemWindows(window, false)
137168
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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
422422
{ "InputStream for '$uri' is null" },
423423
)
424424
val (baseNotes, labels) = stream.readAsBackup()
425-
commonDao.importBackup(app, baseNotes, labels)
425+
commonDao.importBackup(baseNotes, labels)
426426
baseNotes.size
427427
}
428428
val message = app.getQuantityString(R.plurals.imported_notes, importedNotes)
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)