Skip to content

Commit 36a67bd

Browse files
authored
Merge pull request #799 from Crustack/fix/limit-note-size
Enforce max text length for notes
2 parents 633c51e + 2dbdda2 commit 36a67bd

File tree

23 files changed

+918
-75
lines changed

23 files changed

+918
-75
lines changed

TRANSLATIONS.md

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,34 @@ See [Android Translations Converter](https://github.com/Crustack/android-transla
1919
<!-- translations:start -->
2020
| Language | Coverage |
2121
|----------|----------|
22-
| 🇺🇸 English | 100% (320/320) |
23-
| 🇪🇸 Catalan | 20% (65/320) |
24-
| 🇨🇿 Czech | 97% (313/320) |
25-
| 🇩🇰 Danish | 21% (69/320) |
26-
| 🇩🇪 German | 97% (313/320) |
27-
| 🇬🇷 Greek | 22% (72/320) |
28-
| 🇪🇸 Spanish | 97% (313/320) |
29-
| 🇫🇷 French | 94% (301/320) |
30-
| 🇭🇺 Hungarian | 20% (65/320) |
31-
| 🇮🇩 Indonesian | 23% (75/320) |
32-
| 🇮🇹 Italian | 90% (291/320) |
33-
| 🇯🇵 Japanese | 22% (73/320) |
34-
| 🇲🇲 Burmese | 28% (90/320) |
35-
| 🇳🇴 Norwegian Bokmål | 33% (106/320) |
36-
| 🇳🇱 Dutch | 66% (212/320) |
37-
| 🇳🇴 Norwegian Nynorsk | 33% (106/320) |
38-
| 🇵🇱 Polish | 93% (300/320) |
39-
| 🇧🇷 Portuguese (Brazil) | 97% (312/320) |
40-
| 🇵🇹 Portuguese (Portugal) | 22% (71/320) |
41-
| 🇷🇴 Romanian | 94% (301/320) |
42-
| 🇷🇺 Russian | 95% (305/320) |
43-
| 🇸🇰 Slovak | 20% (65/320) |
44-
| 🇸🇮 Slovenian | 34% (109/320) |
45-
| 🇸🇪 Swedish | 19% (63/320) |
46-
| 🇵🇭 Tagalog | 20% (65/320) |
47-
| 🇹🇷 Turkish | 22% (73/320) |
48-
| 🇺🇦 Ukrainian | 98% (314/320) |
49-
| 🇻🇳 Vietnamese | 33% (107/320) |
50-
| 🇨🇳 Chinese (Simplified) | 97% (312/320) |
51-
| 🇹🇼 Chinese (Traditional) | 91% (294/320) |
22+
| 🇺🇸 English | 100% (326/326) |
23+
| 🇪🇸 Catalan | 19% (65/326) |
24+
| 🇨🇿 Czech | 96% (313/326) |
25+
| 🇩🇰 Danish | 21% (69/326) |
26+
| 🇩🇪 German | 96% (313/326) |
27+
| 🇬🇷 Greek | 22% (72/326) |
28+
| 🇪🇸 Spanish | 96% (313/326) |
29+
| 🇫🇷 French | 92% (301/326) |
30+
| 🇭🇺 Hungarian | 19% (65/326) |
31+
| 🇮🇩 Indonesian | 23% (75/326) |
32+
| 🇮🇹 Italian | 89% (291/326) |
33+
| 🇯🇵 Japanese | 22% (73/326) |
34+
| 🇲🇲 Burmese | 27% (90/326) |
35+
| 🇳🇴 Norwegian Bokmål | 32% (106/326) |
36+
| 🇳🇱 Dutch | 65% (212/326) |
37+
| 🇳🇴 Norwegian Nynorsk | 32% (106/326) |
38+
| 🇵🇱 Polish | 92% (300/326) |
39+
| 🇧🇷 Portuguese (Brazil) | 95% (312/326) |
40+
| 🇵🇹 Portuguese (Portugal) | 21% (71/326) |
41+
| 🇷🇴 Romanian | 92% (301/326) |
42+
| 🇷🇺 Russian | 93% (305/326) |
43+
| 🇸🇰 Slovak | 19% (65/326) |
44+
| 🇸🇮 Slovenian | 33% (109/326) |
45+
| 🇸🇪 Swedish | 19% (63/326) |
46+
| 🇵🇭 Tagalog | 19% (65/326) |
47+
| 🇹🇷 Turkish | 22% (73/326) |
48+
| 🇺🇦 Ukrainian | 96% (314/326) |
49+
| 🇻🇳 Vietnamese | 32% (107/326) |
50+
| 🇨🇳 Chinese (Simplified) | 95% (312/326) |
51+
| 🇹🇼 Chinese (Traditional) | 90% (294/326) |
5252
<!-- translations:end -->

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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.philkes.notallyx.data.dao
22

3+
import android.content.ContextWrapper
34
import androidx.lifecycle.LiveData
45
import androidx.lifecycle.map
56
import androidx.room.Dao
@@ -9,6 +10,7 @@ import androidx.room.Query
910
import androidx.room.RawQuery
1011
import androidx.room.Update
1112
import androidx.sqlite.db.SupportSQLiteQuery
13+
import com.philkes.notallyx.R
1214
import com.philkes.notallyx.data.model.Audio
1315
import com.philkes.notallyx.data.model.BaseNote
1416
import com.philkes.notallyx.data.model.FileAttachment
@@ -17,6 +19,10 @@ import com.philkes.notallyx.data.model.LabelsInBaseNote
1719
import com.philkes.notallyx.data.model.ListItem
1820
import com.philkes.notallyx.data.model.Reminder
1921
import com.philkes.notallyx.data.model.Type
22+
import com.philkes.notallyx.presentation.showToast
23+
import com.philkes.notallyx.utils.charLimit
24+
import com.philkes.notallyx.utils.log
25+
import kotlin.text.compareTo
2026

2127
data class NoteIdReminder(val id: Long, val reminders: List<Reminder>)
2228

@@ -27,13 +33,66 @@ data class NoteReminder(
2733
val reminders: List<Reminder>,
2834
)
2935

36+
/** Maximum allowed size of a note body in MB (~340,000 characters) */
37+
const val MAX_BODY_SIZE_MB = 1.5
38+
3039
@Dao
3140
interface BaseNoteDao {
3241

3342
@RawQuery fun query(query: SupportSQLiteQuery): Int
3443

3544
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(baseNote: BaseNote): Long
3645

46+
private fun BaseNote.truncated(): Pair<Boolean, BaseNote> {
47+
return if (body.length > MAX_BODY_CHAR_LENGTH) {
48+
return Pair(true, copy(body = body.take(MAX_BODY_CHAR_LENGTH)))
49+
} else Pair(false, this)
50+
}
51+
52+
suspend fun insertSafe(context: ContextWrapper, baseNote: BaseNote): Long {
53+
val (truncated, note) = baseNote.truncated()
54+
if (truncated) {
55+
context.log(
56+
TAG,
57+
"Note (id: ${note.id}, title: '${note.title}') is too big to insert. Truncating to ${note.body.length} characters (was: ${baseNote.body.length})",
58+
)
59+
context.showToast(
60+
context.getString(
61+
R.string.note_too_big_truncating,
62+
note.body.length,
63+
baseNote.body.length,
64+
)
65+
)
66+
}
67+
return insert(note)
68+
}
69+
70+
suspend fun insertSafe(context: ContextWrapper, baseNotes: List<BaseNote>): List<Long> {
71+
val truncatedNotes = mutableListOf<BaseNote>()
72+
var truncatedCharacterSize = 0
73+
val notes =
74+
baseNotes.map { baseNote ->
75+
val (truncated, note) = baseNote.truncated()
76+
if (truncated) {
77+
truncatedCharacterSize += baseNote.body.length
78+
truncatedNotes.add(note)
79+
}
80+
note
81+
}
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,
91+
)
92+
)
93+
return insert(notes)
94+
}
95+
3796
@Insert suspend fun insert(baseNotes: List<BaseNote>): List<Long>
3897

3998
@Update(entity = BaseNote::class) suspend fun update(labelsInBaseNotes: List<LabelsInBaseNote>)
@@ -135,6 +194,10 @@ interface BaseNoteDao {
135194
spans: List<com.philkes.notallyx.data.model.SpanRepresentation>,
136195
)
137196

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+
138201
/**
139202
* Both id and position can be invalid.
140203
*
@@ -248,4 +311,10 @@ interface BaseNoteDao {
248311
}
249312
return false
250313
}
314+
315+
companion object {
316+
private const val TAG = "BaseNoteDao"
317+
318+
val MAX_BODY_CHAR_LENGTH = MAX_BODY_SIZE_MB.charLimit()
319+
}
251320
}

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

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package com.philkes.notallyx.data.dao
22

3+
import android.content.ContextWrapper
34
import androidx.room.Dao
45
import androidx.room.Transaction
56
import com.philkes.notallyx.data.NotallyDatabase
7+
import com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
68
import com.philkes.notallyx.data.model.BaseNote
79
import com.philkes.notallyx.data.model.Label
810
import com.philkes.notallyx.data.model.LabelsInBaseNote
11+
import com.philkes.notallyx.data.model.SpanRepresentation
12+
import com.philkes.notallyx.data.model.Type
913
import com.philkes.notallyx.data.model.createNoteUrl
1014
import com.philkes.notallyx.data.model.getNoteIdFromUrl
1115
import com.philkes.notallyx.data.model.getNoteTypeFromUrl
1216
import com.philkes.notallyx.data.model.isNoteUrl
17+
import com.philkes.notallyx.utils.NoteSplitUtils
1318

1419
@Dao
1520
abstract class CommonDao(private val database: NotallyDatabase) {
@@ -40,8 +45,20 @@ abstract class CommonDao(private val database: NotallyDatabase) {
4045
}
4146

4247
@Transaction
43-
open suspend fun importBackup(baseNotes: List<BaseNote>, labels: List<Label>) {
44-
database.getBaseNoteDao().insert(baseNotes)
48+
open suspend fun importBackup(
49+
context: ContextWrapper,
50+
baseNotes: List<BaseNote>,
51+
labels: List<Label>,
52+
) {
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+
}
4562
database.getLabelDao().insert(labels)
4663
}
4764

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

68-
// Remap note links in spans where necessary
6983
for (i in baseNotes.indices) {
70-
val note = baseNotes[i]
71-
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) {
7299
var changed = false
73-
val updatedSpans =
74-
note.spans.map { span ->
100+
val updated =
101+
spans.map { span ->
75102
if (span.link && span.linkData?.isNoteUrl() == true) {
76103
val url = span.linkData!!
77104
val oldTargetId = url.getNoteIdFromUrl()
@@ -80,13 +107,11 @@ abstract class CommonDao(private val database: NotallyDatabase) {
80107
if (newTargetId != null) {
81108
changed = true
82109
span.copy(linkData = newTargetId.createNoteUrl(type))
83-
} else {
84-
span
85-
}
110+
} else span
86111
} else span
87112
}
88113
if (changed) {
89-
baseNoteDao.updateSpans(newId, updatedSpans)
114+
baseNoteDao.updateSpans(noteId, updated)
90115
}
91116
}
92117

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().insert(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 {

0 commit comments

Comments
 (0)