Skip to content

Commit cc00c45

Browse files
authored
Merge pull request #846 from Crustack/feat/393
Add preference to auto remove deleted notes
2 parents b58dd7d + 4cb6638 commit cc00c45

File tree

13 files changed

+373
-41
lines changed

13 files changed

+373
-41
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% (329/329) |
23-
| 🇪🇸 Catalan | 19% (65/329) |
24-
| 🇨🇿 Czech | 95% (313/329) |
25-
| 🇩🇰 Danish | 20% (69/329) |
26-
| 🇩🇪 German | 99% (327/329) |
27-
| 🇬🇷 Greek | 21% (72/329) |
28-
| 🇪🇸 Spanish | 95% (314/329) |
29-
| 🇫🇷 French | 99% (327/329) |
30-
| 🇭🇺 Hungarian | 19% (65/329) |
31-
| 🇮🇩 Indonesian | 22% (75/329) |
32-
| 🇮🇹 Italian | 88% (291/329) |
33-
| 🇯🇵 Japanese | 22% (73/329) |
34-
| 🇲🇲 Burmese | 27% (90/329) |
35-
| 🇳🇴 Norwegian Bokmål | 32% (106/329) |
36-
| 🇳🇱 Dutch | 64% (212/329) |
37-
| 🇳🇴 Norwegian Nynorsk | 32% (106/329) |
38-
| 🇵🇱 Polish | 91% (300/329) |
39-
| 🇧🇷 Portuguese (Brazil) | 94% (312/329) |
40-
| 🇵🇹 Portuguese (Portugal) | 21% (71/329) |
41-
| 🇷🇴 Romanian | 91% (301/329) |
42-
| 🇷🇺 Russian | 92% (305/329) |
43-
| 🇸🇰 Slovak | 19% (65/329) |
44-
| 🇸🇮 Slovenian | 33% (109/329) |
45-
| 🇸🇪 Swedish | 19% (63/329) |
46-
| 🇵🇭 Tagalog | 19% (65/329) |
47-
| 🇹🇷 Turkish | 22% (73/329) |
48-
| 🇺🇦 Ukrainian | 99% (326/329) |
49-
| 🇻🇳 Vietnamese | 32% (107/329) |
50-
| 🇨🇳 Chinese (Simplified) | 98% (323/329) |
51-
| 🇹🇼 Chinese (Traditional) | 89% (294/329) |
22+
| 🇺🇸 English | 100% (331/331) |
23+
| 🇪🇸 Catalan | 19% (65/331) |
24+
| 🇨🇿 Czech | 94% (313/331) |
25+
| 🇩🇰 Danish | 20% (69/331) |
26+
| 🇩🇪 German | 98% (327/331) |
27+
| 🇬🇷 Greek | 21% (72/331) |
28+
| 🇪🇸 Spanish | 94% (314/331) |
29+
| 🇫🇷 French | 98% (327/331) |
30+
| 🇭🇺 Hungarian | 19% (65/331) |
31+
| 🇮🇩 Indonesian | 22% (75/331) |
32+
| 🇮🇹 Italian | 87% (291/331) |
33+
| 🇯🇵 Japanese | 22% (73/331) |
34+
| 🇲🇲 Burmese | 27% (90/331) |
35+
| 🇳🇴 Norwegian Bokmål | 32% (106/331) |
36+
| 🇳🇱 Dutch | 64% (212/331) |
37+
| 🇳🇴 Norwegian Nynorsk | 32% (106/331) |
38+
| 🇵🇱 Polish | 90% (300/331) |
39+
| 🇧🇷 Portuguese (Brazil) | 94% (312/331) |
40+
| 🇵🇹 Portuguese (Portugal) | 21% (71/331) |
41+
| 🇷🇴 Romanian | 90% (301/331) |
42+
| 🇷🇺 Russian | 92% (305/331) |
43+
| 🇸🇰 Slovak | 19% (65/331) |
44+
| 🇸🇮 Slovenian | 32% (109/331) |
45+
| 🇸🇪 Swedish | 19% (63/331) |
46+
| 🇵🇭 Tagalog | 19% (65/331) |
47+
| 🇹🇷 Turkish | 22% (73/331) |
48+
| 🇺🇦 Ukrainian | 98% (326/331) |
49+
| 🇻🇳 Vietnamese | 32% (107/331) |
50+
| 🇨🇳 Chinese (Simplified) | 97% (323/331) |
51+
| 🇹🇼 Chinese (Traditional) | 88% (294/331) |
5252
<!-- translations:end -->

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,29 @@ package com.philkes.notallyx
33
import android.app.Activity
44
import android.app.Application
55
import android.content.Context
6+
import android.content.ContextWrapper
67
import android.content.Intent
78
import android.content.IntentFilter
89
import android.os.Build
910
import android.os.Bundle
11+
import android.util.Log
1012
import androidx.appcompat.app.AppCompatDelegate
1113
import androidx.lifecycle.Observer
14+
import androidx.work.ExistingPeriodicWorkPolicy
15+
import androidx.work.PeriodicWorkRequest
1216
import androidx.work.WorkInfo
1317
import androidx.work.WorkManager
1418
import com.google.android.material.color.DynamicColors
19+
import com.philkes.notallyx.NotallyXApplication.Companion.AUTO_REMOVE_DELETED_NOTES
20+
import com.philkes.notallyx.NotallyXApplication.Companion.TAG
1521
import com.philkes.notallyx.presentation.setEnabledSecureFlag
1622
import com.philkes.notallyx.presentation.view.misc.NotNullLiveData
1723
import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock
1824
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences
1925
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences.Companion.EMPTY_PATH
2026
import com.philkes.notallyx.presentation.viewmodel.preference.Theme
2127
import com.philkes.notallyx.presentation.widget.WidgetProvider
28+
import com.philkes.notallyx.utils.AutoRemoveDeletedNotesWorker
2229
import com.philkes.notallyx.utils.backup.AUTO_BACKUP_WORK_NAME
2330
import com.philkes.notallyx.utils.backup.autoBackupOnSave
2431
import com.philkes.notallyx.utils.backup.autoBackupOnSaveFileExists
@@ -30,6 +37,7 @@ import com.philkes.notallyx.utils.backup.isEqualTo
3037
import com.philkes.notallyx.utils.backup.modifiedNoteBackupExists
3138
import com.philkes.notallyx.utils.backup.scheduleAutoBackup
3239
import com.philkes.notallyx.utils.backup.updateAutoBackup
40+
import com.philkes.notallyx.utils.log
3341
import com.philkes.notallyx.utils.observeOnce
3442
import com.philkes.notallyx.utils.security.UnlockReceiver
3543
import java.util.concurrent.TimeUnit
@@ -94,6 +102,9 @@ class NotallyXApplication : Application(), Application.ActivityLifecycleCallback
94102
val backupFolder = preferences.backupsFolder.value
95103
checkUpdatePeriodicBackup(backupFolder, backupFolder, value.periodInDays.toLong())
96104
}
105+
preferences.autoRemoveDeletedNotesAfterDays.observeForever { value ->
106+
checkUpdateAutoRemoveOldDeletedNotes(value)
107+
}
97108

98109
val filter = IntentFilter().apply { addAction(Intent.ACTION_SCREEN_OFF) }
99110
biometricLockObserver = Observer { biometricLock ->
@@ -199,6 +210,15 @@ class NotallyXApplication : Application(), Application.ActivityLifecycleCallback
199210
}
200211
}
201212

213+
private fun checkUpdateAutoRemoveOldDeletedNotes(days: Int) {
214+
val workManager = getWorkManagerSafe() ?: return
215+
if (days > 0) {
216+
workManager.scheduleAutoRemoveOldDeletedNotes(this)
217+
} else {
218+
workManager.cancelAutoRemoveOldDeletedNotes()
219+
}
220+
}
221+
202222
private fun getWorkManagerSafe(): WorkManager? {
203223
return try {
204224
WorkManager.getInstance(this)
@@ -229,8 +249,33 @@ class NotallyXApplication : Application(), Application.ActivityLifecycleCallback
229249
override fun onActivityDestroyed(activity: Activity) {}
230250

231251
companion object {
232-
private fun isTestRunner(): Boolean {
252+
const val TAG = "NotallyXApplication"
253+
const val AUTO_REMOVE_DELETED_NOTES = "com.philkes.notallyx.AutoRemoveDeletedNotes"
254+
255+
fun isTestRunner(): Boolean {
233256
return Build.FINGERPRINT.equals("robolectric", ignoreCase = true)
234257
}
235258
}
236259
}
260+
261+
fun WorkManager.scheduleAutoRemoveOldDeletedNotes(context: ContextWrapper) {
262+
Log.d(TAG, "Scheduling auto removal of old deleted notes")
263+
val request =
264+
PeriodicWorkRequest.Builder(AutoRemoveDeletedNotesWorker::class.java, 1, TimeUnit.DAYS)
265+
.build()
266+
try {
267+
enqueueUniquePeriodicWork(
268+
AUTO_REMOVE_DELETED_NOTES,
269+
ExistingPeriodicWorkPolicy.KEEP,
270+
request,
271+
)
272+
} catch (e: IllegalStateException) {
273+
// only happens in Unit-Tests
274+
context.log(TAG, "Scheduling auto removal of old deleted notes failed", throwable = e)
275+
}
276+
}
277+
278+
fun WorkManager.cancelAutoRemoveOldDeletedNotes() {
279+
Log.d(TAG, "Cancelling auto removal of old deleted notes")
280+
cancelUniqueWork(AUTO_REMOVE_DELETED_NOTES)
281+
}

app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.room.TypeConverters
1212
import androidx.room.migration.Migration
1313
import androidx.sqlite.db.SimpleSQLiteQuery
1414
import androidx.sqlite.db.SupportSQLiteDatabase
15+
import com.philkes.notallyx.NotallyXApplication.Companion.isTestRunner
1516
import com.philkes.notallyx.data.dao.BaseNoteDao
1617
import com.philkes.notallyx.data.dao.CommonDao
1718
import com.philkes.notallyx.data.dao.LabelDao
@@ -113,13 +114,30 @@ abstract class NotallyDatabase : RoomDatabase() {
113114
}
114115
}
115116

117+
private var testInstance: NotallyDatabase? = null
118+
119+
private fun getTestDatabase(context: ContextWrapper): NotallyDatabase {
120+
return testInstance
121+
?: synchronized(this) {
122+
testInstance =
123+
Room.inMemoryDatabaseBuilder(context, NotallyDatabase::class.java)
124+
.allowMainThreadQueries()
125+
.build()
126+
return testInstance!!
127+
}
128+
}
129+
116130
fun getFreshDatabase(context: ContextWrapper, dataInPublic: Boolean): NotallyDatabase {
117-
return createInstance(
118-
context,
119-
NotallyXPreferences.getInstance(context),
120-
false,
121-
dataInPublic = dataInPublic,
122-
)
131+
return if (isTestRunner()) {
132+
getTestDatabase(context)
133+
} else {
134+
createInstance(
135+
context,
136+
NotallyXPreferences.getInstance(context),
137+
false,
138+
dataInPublic = dataInPublic,
139+
)
140+
}
123141
}
124142

125143
private fun createInstance(

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ interface BaseNoteDao {
107107

108108
@Query("SELECT COUNT(*) FROM BaseNote") fun count(): Int
109109

110+
@Query("DELETE FROM BaseNote") suspend fun deleteAll()
111+
110112
@Query("DELETE FROM BaseNote WHERE id = :id") suspend fun delete(id: Long)
111113

112114
@Query("DELETE FROM BaseNote WHERE id IN (:ids)") suspend fun delete(ids: LongArray)
@@ -134,6 +136,15 @@ interface BaseNoteDao {
134136

135137
@Query("SELECT images FROM BaseNote WHERE id = :id") fun getImages(id: Long): String
136138

139+
@Query("SELECT images FROM BaseNote WHERE id IN (:ids)")
140+
fun getImages(ids: LongArray): List<String>
141+
142+
@Query("SELECT files FROM BaseNote WHERE id IN (:ids)")
143+
fun getFiles(ids: LongArray): List<String>
144+
145+
@Query("SELECT audios FROM BaseNote WHERE id IN (:ids)")
146+
fun getAudios(ids: LongArray): List<String>
147+
137148
@Query("SELECT images FROM BaseNote") fun getAllImages(): List<String>
138149

139150
@Query("SELECT files FROM BaseNote") fun getAllFiles(): List<String>
@@ -153,6 +164,9 @@ interface BaseNoteDao {
153164
@Query("SELECT id FROM BaseNote WHERE folder = 'DELETED'")
154165
suspend fun getDeletedNoteIds(): LongArray
155166

167+
@Query("SELECT id FROM BaseNote WHERE folder = 'DELETED' AND modifiedTimestamp < :before")
168+
suspend fun getDeletedNoteIdsOlderThan(before: Long): LongArray
169+
156170
@Query("SELECT images FROM BaseNote WHERE folder = 'DELETED'")
157171
suspend fun getDeletedNoteImages(): List<String>
158172

@@ -165,6 +179,11 @@ interface BaseNoteDao {
165179
@Query("UPDATE BaseNote SET folder = :folder WHERE id IN (:ids)")
166180
suspend fun move(ids: LongArray, folder: Folder)
167181

182+
@Query(
183+
"UPDATE BaseNote SET folder = :folder, modifiedTimestamp = :timestamp WHERE id IN (:ids)"
184+
)
185+
suspend fun move(ids: LongArray, folder: Folder, timestamp: Long)
186+
168187
@Query("SELECT DISTINCT color FROM BaseNote") fun getAllColorsAsync(): LiveData<List<String>>
169188

170189
@Query("SELECT DISTINCT color FROM BaseNote") suspend fun getAllColors(): List<String>

app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/PreferenceBindingExtensions.kt

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.philkes.notallyx.presentation.activity.main.fragment.settings
33
import android.content.Context
44
import android.hardware.biometrics.BiometricManager
55
import android.net.Uri
6+
import android.os.Build
67
import android.text.method.PasswordTransformationMethod
78
import android.view.LayoutInflater
89
import android.view.View
@@ -460,9 +461,15 @@ fun PreferenceSeekbarBinding.setup(
460461
max: Int,
461462
context: Context,
462463
enabled: Boolean = true,
464+
tooltipResId: Int? = null,
463465
onChange: (newValue: Int) -> Unit,
464466
) {
465-
Title.setText(titleResId)
467+
Title.apply {
468+
setText(titleResId)
469+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
470+
tooltipResId?.let { tooltipText = context.getString(tooltipResId) }
471+
}
472+
}
466473
val valueInBoundaries = (if (value < min) min else if (value > max) max else value).toFloat()
467474
Slider.apply {
468475
isEnabled = enabled
@@ -487,9 +494,17 @@ fun PreferenceSeekbarBinding.setup(
487494
preference: IntPreference,
488495
context: Context,
489496
value: Int = preference.value,
497+
tooltipResId: Int? = null,
490498
onChange: (newValue: Int) -> Unit,
491499
) {
492-
setup(value, preference.titleResId!!, preference.min, preference.max, context) { newValue ->
500+
setup(
501+
value,
502+
preference.titleResId!!,
503+
preference.min,
504+
preference.max,
505+
context,
506+
tooltipResId = tooltipResId,
507+
) { newValue ->
493508
onChange(newValue)
494509
}
495510
}
@@ -514,7 +529,23 @@ fun PreferenceSeekbarBinding.setupAutoSaveIdleTime(
514529
}
515530
}
516531
}
517-
setup(preference, context, value, onChange)
532+
setup(preference, context, value, onChange = onChange)
533+
}
534+
535+
fun PreferenceSeekbarBinding.setupAutoEmptyBin(
536+
preference: IntPreference,
537+
context: Context,
538+
value: Int = preference.value,
539+
onChange: (newValue: Int) -> Unit,
540+
) {
541+
Slider.apply {
542+
setLabelFormatter { sliderValue ->
543+
if (sliderValue == 0f) {
544+
context.getString(R.string.disabled)
545+
} else "${sliderValue.toInt()} ${context.getString(R.string.days)}"
546+
}
547+
}
548+
setup(preference, context, value, R.string.auto_remove_deleted_notes_hint, onChange)
518549
}
519550

520551
fun PreferenceBinding.setupStartView(

app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/SettingsFragment.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import androidx.core.view.isVisible
2424
import androidx.fragment.app.Fragment
2525
import androidx.fragment.app.activityViewModels
2626
import androidx.lifecycle.lifecycleScope
27+
import androidx.work.WorkManager
2728
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2829
import com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE
2930
import com.philkes.notallyx.NotallyXApplication
3031
import com.philkes.notallyx.R
32+
import com.philkes.notallyx.cancelAutoRemoveOldDeletedNotes
3133
import com.philkes.notallyx.data.imports.FOLDER_OR_FILE_MIMETYPE
3234
import com.philkes.notallyx.data.imports.ImportSource
3335
import com.philkes.notallyx.data.imports.txt.APPLICATION_TEXT_MIME_TYPES
@@ -51,6 +53,7 @@ import com.philkes.notallyx.presentation.viewmodel.preference.PeriodicBackup
5153
import com.philkes.notallyx.presentation.viewmodel.preference.PeriodicBackup.Companion.BACKUP_MAX_MIN
5254
import com.philkes.notallyx.presentation.viewmodel.preference.PeriodicBackup.Companion.BACKUP_PERIOD_DAYS_MIN
5355
import com.philkes.notallyx.presentation.viewmodel.preference.PeriodicBackupsPreference
56+
import com.philkes.notallyx.scheduleAutoRemoveOldDeletedNotes
5457
import com.philkes.notallyx.utils.MIME_TYPE_JSON
5558
import com.philkes.notallyx.utils.MIME_TYPE_ZIP
5659
import com.philkes.notallyx.utils.backup.exportPreferences
@@ -337,6 +340,23 @@ class SettingsFragment : Fragment() {
337340
}
338341
}
339342

343+
autoRemoveDeletedNotesAfterDays.observe(viewLifecycleOwner) { value ->
344+
binding.AutoEmptyBin.setupAutoEmptyBin(
345+
autoRemoveDeletedNotesAfterDays,
346+
requireContext(),
347+
) { newValue ->
348+
model.savePreference(autoRemoveDeletedNotesAfterDays, newValue)
349+
val workManager = WorkManager.getInstance(requireContext())
350+
if (newValue > 0) {
351+
workManager.scheduleAutoRemoveOldDeletedNotes(
352+
requireContext() as ContextWrapper
353+
)
354+
} else {
355+
workManager.cancelAutoRemoveOldDeletedNotes()
356+
}
357+
}
358+
}
359+
340360
binding.MaxLabels.setup(maxLabels, requireContext()) { newValue ->
341361
model.savePreference(maxLabels, newValue)
342362
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,11 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
631631
viewModelScope.launch(
632632
Dispatchers.IO
633633
) { // Only reminders of notes in NOTES folder are active
634-
baseNoteDao.move(ids, folder)
634+
if (folder == Folder.DELETED) {
635+
baseNoteDao.move(ids, folder, System.currentTimeMillis())
636+
} else {
637+
baseNoteDao.move(ids, folder)
638+
}
635639
val notes = baseNoteDao.getByIds(ids).toNoteIdReminders()
636640
// Only reminders of notes in NOTES folder are active
637641
when (folder) {

0 commit comments

Comments
 (0)