From e38e14bdd4631618ce35745119ca582f4582b0ae Mon Sep 17 00:00:00 2001 From: PhilKes Date: Thu, 31 Oct 2024 13:17:04 +0100 Subject: [PATCH] Refactor Preferences from objects to Enums --- app/build.gradle | 1 + .../philkes/notallyx/NotallyXApplication.kt | 23 +- .../java/com/philkes/notallyx/Preferences.kt | 228 ------------- .../philkes/notallyx/data/NotallyDatabase.kt | 38 ++- .../notallyx/presentation/UiExtensions.kt | 28 +- .../activity/ConfigureWidgetActivity.kt | 4 +- .../presentation/activity/LockedActivity.kt | 14 +- .../activity/main/fragment/NotallyFragment.kt | 16 +- .../main/fragment/SettingsFragment.kt | 273 ++++++++------- .../activity/note/EditActivity.kt | 17 +- .../activity/note/EditListActivity.kt | 8 +- .../activity/note/PickNoteActivity.kt | 16 +- .../presentation/view/main/BaseNoteAdapter.kt | 24 +- .../presentation/view/main/BaseNoteVH.kt | 21 +- .../main/sorting/BaseNoteCreationDateSort.kt | 2 +- .../main/sorting/BaseNoteModifiedDateSort.kt | 2 +- .../view/main/sorting/BaseNoteSort.kt | 2 +- .../view/main/sorting/BaseNoteTitleSort.kt | 2 +- .../presentation/view/misc/ListInfo.kt | 197 ----------- .../{BetterLiveData.kt => NotNullLiveData.kt} | 2 +- .../presentation/view/misc/SeekbarInfo.kt | 69 ---- .../presentation/view/misc/TextInfo.kt | 29 -- .../view/note/listitem/ListItemAdapter.kt | 7 +- .../view/note/listitem/ListItemVH.kt | 12 +- .../view/note/listitem/ListManager.kt | 8 +- .../presentation/viewmodel/BaseNoteModel.kt | 25 +- .../presentation/viewmodel/NotallyModel.kt | 12 +- .../preference/NotallyXPreferences.kt | 157 +++++++++ .../viewmodel/preference/NotesSorting.kt | 68 ++++ .../viewmodel/preference/Preference.kt | 322 ++++++++++++++++++ .../presentation/widget/WidgetFactory.kt | 18 +- .../presentation/widget/WidgetProvider.kt | 8 +- .../com/philkes/notallyx/utils/ActionMode.kt | 6 +- .../com/philkes/notallyx/utils/Operations.kt | 6 +- .../utils/audio/AudioRecordService.kt | 4 +- .../notallyx/utils/backup/AutoBackupWorker.kt | 10 +- .../philkes/notallyx/utils/backup/Export.kt | 18 +- .../listmanager/ListManagerAddDeleteTest.kt | 26 +- .../listmanager/ListManagerCheckedTest.kt | 30 +- .../listmanager/ListManagerIsChildTest.kt | 8 +- .../listmanager/ListManagerMoveTest.kt | 48 +-- .../listmanager/ListManagerTestBase.kt | 19 +- .../ListManagerWithChangeHistoryTest.kt | 24 +- 43 files changed, 925 insertions(+), 927 deletions(-) delete mode 100644 app/src/main/java/com/philkes/notallyx/Preferences.kt delete mode 100644 app/src/main/java/com/philkes/notallyx/presentation/view/misc/ListInfo.kt rename app/src/main/java/com/philkes/notallyx/presentation/view/misc/{BetterLiveData.kt => NotNullLiveData.kt} (76%) delete mode 100644 app/src/main/java/com/philkes/notallyx/presentation/view/misc/SeekbarInfo.kt delete mode 100644 app/src/main/java/com/philkes/notallyx/presentation/view/misc/TextInfo.kt create mode 100644 app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotallyXPreferences.kt create mode 100644 app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotesSorting.kt create mode 100644 app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/Preference.kt diff --git a/app/build.gradle b/app/build.gradle index 88986b23..dec5d476 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,6 +105,7 @@ dependencies { implementation 'net.lingala.zip4j:zip4j:2.11.5' implementation "androidx.work:work-runtime:2.9.1" + implementation "androidx.preference:preference-ktx:1.2.1" //noinspection GradleDependency implementation "androidx.navigation:navigation-ui-ktx:$navVersion" diff --git a/app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt b/app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt index b8493c2b..78ce23cf 100644 --- a/app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt +++ b/app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt @@ -5,32 +5,33 @@ import android.content.Intent import android.content.IntentFilter import androidx.appcompat.app.AppCompatDelegate import androidx.lifecycle.Observer -import com.philkes.notallyx.presentation.view.misc.BetterLiveData -import com.philkes.notallyx.presentation.view.misc.BiometricLock.enabled -import com.philkes.notallyx.presentation.view.misc.Theme +import com.philkes.notallyx.presentation.view.misc.NotNullLiveData +import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences +import com.philkes.notallyx.presentation.viewmodel.preference.Theme import com.philkes.notallyx.presentation.widget.WidgetProvider import com.philkes.notallyx.utils.backup.Export.scheduleAutoBackup import com.philkes.notallyx.utils.security.UnlockReceiver class NotallyXApplication : Application() { - private lateinit var biometricLockObserver: Observer - private lateinit var preferences: Preferences + private lateinit var biometricLockObserver: Observer + private lateinit var preferences: NotallyXPreferences private var unlockReceiver: UnlockReceiver? = null - val locked = BetterLiveData(true) + val locked = NotNullLiveData(true) override fun onCreate() { super.onCreate() - preferences = Preferences.getInstance(this) + preferences = NotallyXPreferences.getInstance(this) preferences.theme.observeForever { theme -> when (theme) { - Theme.dark -> + Theme.DARK -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - Theme.light -> + Theme.LIGHT -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - Theme.followSystem -> + Theme.FOLLOW_SYSTEM -> AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM ) @@ -41,7 +42,7 @@ class NotallyXApplication : Application() { val filter = IntentFilter().apply { addAction(Intent.ACTION_SCREEN_OFF) } biometricLockObserver = Observer { - if (it == enabled) { + if (it == BiometricLock.ENABLED) { unlockReceiver = UnlockReceiver(this) registerReceiver(unlockReceiver, filter) } else if (unlockReceiver != null) { diff --git a/app/src/main/java/com/philkes/notallyx/Preferences.kt b/app/src/main/java/com/philkes/notallyx/Preferences.kt deleted file mode 100644 index 9de3760f..00000000 --- a/app/src/main/java/com/philkes/notallyx/Preferences.kt +++ /dev/null @@ -1,228 +0,0 @@ -package com.philkes.notallyx - -import android.app.Application -import android.os.Build -import android.preference.PreferenceManager -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import com.philkes.notallyx.data.model.Type -import com.philkes.notallyx.data.model.toPreservedByteArray -import com.philkes.notallyx.data.model.toPreservedString -import com.philkes.notallyx.presentation.view.misc.AutoBackup -import com.philkes.notallyx.presentation.view.misc.AutoBackupMax -import com.philkes.notallyx.presentation.view.misc.AutoBackupPeriodDays -import com.philkes.notallyx.presentation.view.misc.BackupPassword -import com.philkes.notallyx.presentation.view.misc.BetterLiveData -import com.philkes.notallyx.presentation.view.misc.BiometricLock -import com.philkes.notallyx.presentation.view.misc.DateFormat -import com.philkes.notallyx.presentation.view.misc.ListInfo -import com.philkes.notallyx.presentation.view.misc.ListItemSorting -import com.philkes.notallyx.presentation.view.misc.MaxItems -import com.philkes.notallyx.presentation.view.misc.MaxLines -import com.philkes.notallyx.presentation.view.misc.MaxTitle -import com.philkes.notallyx.presentation.view.misc.NotesSorting -import com.philkes.notallyx.presentation.view.misc.SeekbarInfo -import com.philkes.notallyx.presentation.view.misc.SortDirection -import com.philkes.notallyx.presentation.view.misc.TextInfo -import com.philkes.notallyx.presentation.view.misc.TextSize -import com.philkes.notallyx.presentation.view.misc.Theme -import com.philkes.notallyx.presentation.view.misc.View -import java.security.SecureRandom -import javax.crypto.Cipher - -private const val DATABASE_ENCRYPTION_KEY = "database_encryption_key" - -private const val ENCRYPTION_IV = "encryption_iv" - -/** - * Custom implementation of androidx.preference library Way faster, simpler and smaller, logic of - * storing preferences has been decoupled from their UI. It is backed by SharedPreferences but it - * should be trivial to shift to another source if needed. - */ -class Preferences private constructor(app: Application) { - - private val preferences = PreferenceManager.getDefaultSharedPreferences(app) - private val editor = preferences.edit() - - private val encryptedPreferences by lazy { - EncryptedSharedPreferences.create( - app, - "secret_shared_prefs", - MasterKey.Builder(app).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, - ) - } - - // Main thread (unfortunately) - val view = BetterLiveData(getListPref(View)) - val theme = BetterLiveData(getListPref(Theme)) - val dateFormat = BetterLiveData(getListPref(DateFormat)) - - val notesSorting = BetterLiveData(getNotesSorting(NotesSorting)) - - val textSize = BetterLiveData(getListPref(TextSize)) - val listItemSorting = BetterLiveData(getListPref(ListItemSorting)) - var maxItems = getSeekbarPref(MaxItems) - var maxLines = getSeekbarPref(MaxLines) - var maxTitle = getSeekbarPref(MaxTitle) - - val autoBackupPath = BetterLiveData(getTextPref(AutoBackup)) - var autoBackupPeriodDays = BetterLiveData(getSeekbarPref(AutoBackupPeriodDays)) - var autoBackupMax = getSeekbarPref(AutoBackupMax) - val backupPassword by lazy { BetterLiveData(getEncryptedTextPref(BackupPassword)) } - - val biometricLock = BetterLiveData(getListPref(BiometricLock)) - var iv: ByteArray? - get() = preferences.getString(ENCRYPTION_IV, null)?.toPreservedByteArray - set(value) { - editor.putString(ENCRYPTION_IV, value?.toPreservedString) - editor.commit() - } - - fun getDatabasePassphrase(): ByteArray { - val string = preferences.getString(DATABASE_ENCRYPTION_KEY, "")!! - return string.toPreservedByteArray - } - - fun generatePassphrase(cipher: Cipher): ByteArray { - val random = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - SecureRandom.getInstanceStrong() - } else { - SecureRandom() - } - val result = ByteArray(64) - - random.nextBytes(result) - - // filter out zero byte values, as SQLCipher does not like them - while (result.contains(0)) { - random.nextBytes(result) - } - - val encryptedPassphrase = cipher.doFinal(result) - editor.putString(DATABASE_ENCRYPTION_KEY, encryptedPassphrase.toPreservedString) - editor.commit() - return result - } - - private fun getListPref(info: ListInfo) = - requireNotNull(preferences.getString(info.key, info.defaultValue)) - - private fun getNotesSorting(info: NotesSorting): Pair { - val sortBy = requireNotNull(preferences.getString(info.key, info.defaultValue)) - val sortDirection = - requireNotNull(preferences.getString(info.directionKey, info.defaultValueDirection)) - return Pair(sortBy, SortDirection.valueOf(sortDirection)) - } - - private fun getTextPref(info: TextInfo) = - requireNotNull(preferences.getString(info.key, info.defaultValue)) - - private fun getEncryptedTextPref(info: TextInfo) = - requireNotNull(encryptedPreferences!!.getString(info.key, info.defaultValue)) - - private fun getSeekbarPref(info: SeekbarInfo) = - requireNotNull(preferences.getInt(info.key, info.defaultValue)) - - fun getWidgetData(id: Int) = preferences.getLong("widget:$id", 0) - - fun getWidgetNoteType(id: Int) = - preferences.getString("widgetNoteType:$id", null)?.let { Type.valueOf(it) } - - fun deleteWidget(id: Int) { - editor.remove("widget:$id") - editor.remove("widgetNoteType:$id") - editor.commit() - } - - fun updateWidget(id: Int, noteId: Long, noteType: Type) { - editor.putLong("widget:$id", noteId) - editor.putString("widgetNoteType:$id", noteType.name) - editor.commit() - } - - fun getUpdatableWidgets(noteIds: LongArray? = null): List> { - val updatableWidgets = ArrayList>() - val pairs = preferences.all - pairs.keys.forEach { key -> - val token = "widget:" - if (key.startsWith(token)) { - val end = key.substringAfter(token) - val id = end.toIntOrNull() - if (id != null) { - val value = pairs[key] as? Long - if (value != null) { - if (noteIds == null || noteIds.contains(value)) { - updatableWidgets.add(Pair(id, value)) - } - } - } - } - } - return updatableWidgets - } - - fun savePreference(info: SeekbarInfo, value: Int) { - editor.putInt(info.key, value) - editor.commit() - when (info) { - MaxItems -> maxItems = getSeekbarPref(MaxItems) - MaxLines -> maxLines = getSeekbarPref(MaxLines) - MaxTitle -> maxTitle = getSeekbarPref(MaxTitle) - AutoBackupMax -> autoBackupMax = getSeekbarPref(AutoBackupMax) - AutoBackupPeriodDays -> - autoBackupPeriodDays.postValue(getSeekbarPref(AutoBackupPeriodDays)) - } - } - - fun savePreference(info: NotesSorting, sortBy: String, sortDirection: SortDirection) { - editor.putString(info.key, sortBy) - editor.putString(info.directionKey, sortDirection.name) - editor.commit() - notesSorting.postValue(getNotesSorting(info)) - } - - fun savePreference(info: ListInfo, value: String) { - editor.putString(info.key, value) - editor.commit() - when (info) { - View -> view.postValue(getListPref(info)) - Theme -> theme.postValue(getListPref(info)) - DateFormat -> dateFormat.postValue(getListPref(info)) - TextSize -> textSize.postValue(getListPref(info)) - ListItemSorting -> listItemSorting.postValue(getListPref(info)) - BiometricLock -> biometricLock.postValue(getListPref(info)) - else -> return - } - } - - fun savePreference(info: TextInfo, value: String) { - val editor = if (info is BackupPassword) encryptedPreferences!!.edit() else this.editor - editor.putString(info.key, value) - editor.commit() - when (info) { - AutoBackup -> autoBackupPath.postValue(getTextPref(info)) - BackupPassword -> backupPassword.postValue(getEncryptedTextPref(info)) - } - } - - fun showDateCreated(): Boolean { - return dateFormat.value != DateFormat.none - } - - companion object { - - @Volatile private var instance: Preferences? = null - - fun getInstance(app: Application): Preferences { - return instance - ?: synchronized(this) { - val instance = Preferences(app) - Companion.instance = instance - return instance - } - } - } -} diff --git a/app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt b/app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt index 048c10a4..569bc5e6 100644 --- a/app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt +++ b/app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt @@ -10,16 +10,16 @@ import androidx.room.TypeConverters import androidx.room.migration.Migration import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteDatabase -import com.philkes.notallyx.Preferences import com.philkes.notallyx.data.dao.BaseNoteDao import com.philkes.notallyx.data.dao.CommonDao import com.philkes.notallyx.data.dao.LabelDao import com.philkes.notallyx.data.model.BaseNote import com.philkes.notallyx.data.model.Converters import com.philkes.notallyx.data.model.Label -import com.philkes.notallyx.presentation.observeForeverSkipFirst -import com.philkes.notallyx.presentation.view.misc.BetterLiveData -import com.philkes.notallyx.presentation.view.misc.BiometricLock.enabled +import com.philkes.notallyx.presentation.view.misc.NotNullLiveData +import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences +import com.philkes.notallyx.presentation.viewmodel.preference.observeForeverSkipFirst import com.philkes.notallyx.utils.security.getInitializedCipherForDecryption import net.sqlcipher.database.SupportFactory @@ -37,20 +37,20 @@ abstract class NotallyDatabase : RoomDatabase() { getBaseNoteDao().query(SimpleSQLiteQuery("pragma wal_checkpoint(FULL)")) } - private var observer: Observer? = null + private var biometricLockObserver: Observer? = null companion object { const val DatabaseName = "NotallyDatabase" - @Volatile private var instance: BetterLiveData? = null + @Volatile private var instance: NotNullLiveData? = null - fun getDatabase(app: Application): BetterLiveData { + fun getDatabase(app: Application): NotNullLiveData { return instance ?: synchronized(this) { - val preferences = Preferences.getInstance(app) + val preferences = NotallyXPreferences.getInstance(app) this.instance = - BetterLiveData( + NotNullLiveData( createInstance(app, preferences, preferences.biometricLock.value) ) return this.instance!! @@ -59,31 +59,33 @@ abstract class NotallyDatabase : RoomDatabase() { private fun createInstance( app: Application, - preferences: Preferences, - biometrickLock: String, + preferences: NotallyXPreferences, + biometrickLock: BiometricLock, ): NotallyDatabase { val instanceBuilder = Room.databaseBuilder(app, NotallyDatabase::class.java, DatabaseName) .addMigrations(Migration2, Migration3, Migration4, Migration5, Migration6) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (biometrickLock == enabled) { - val initializationVector = preferences.iv!! + if (biometrickLock == BiometricLock.ENABLED) { + val initializationVector = preferences.iv.value!! val cipher = getInitializedCipherForDecryption(iv = initializationVector) - val encryptedPassphrase = preferences.getDatabasePassphrase() + val encryptedPassphrase = preferences.databaseEncryptionKey.value val passphrase = cipher.doFinal(encryptedPassphrase) val factory = SupportFactory(passphrase) instanceBuilder.openHelperFactory(factory) } val instance = instanceBuilder.build() - instance.observer = Observer { newBiometrickLock -> - NotallyDatabase.instance?.value?.observer?.let { + instance.biometricLockObserver = Observer { newBiometrickLock -> + NotallyDatabase.instance?.value?.biometricLockObserver?.let { preferences.biometricLock.removeObserver(it) } val newInstance = createInstance(app, preferences, newBiometrickLock) NotallyDatabase.instance?.postValue(newInstance) - preferences.biometricLock.observeForeverSkipFirst(newInstance.observer!!) + preferences.biometricLock.observeForeverSkipFirst( + newInstance.biometricLockObserver!! + ) } - preferences.biometricLock.observeForeverSkipFirst(instance.observer!!) + preferences.biometricLock.observeForeverSkipFirst(instance.biometricLockObserver!!) return instance } return instanceBuilder.build() diff --git a/app/src/main/java/com/philkes/notallyx/presentation/UiExtensions.kt b/app/src/main/java/com/philkes/notallyx/presentation/UiExtensions.kt index 7ede122d..f0afc69c 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/UiExtensions.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/UiExtensions.kt @@ -46,9 +46,7 @@ import androidx.appcompat.app.AppCompatActivity.INPUT_METHOD_SERVICE import androidx.appcompat.app.AppCompatActivity.KEYGUARD_SERVICE import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.philkes.notallyx.R import com.philkes.notallyx.data.imports.ImportProgress @@ -57,10 +55,10 @@ import com.philkes.notallyx.data.model.Folder import com.philkes.notallyx.data.model.SpanRepresentation import com.philkes.notallyx.data.model.getUrl import com.philkes.notallyx.databinding.DialogProgressBinding -import com.philkes.notallyx.presentation.view.misc.DateFormat import com.philkes.notallyx.presentation.view.misc.EditTextWithHistory import com.philkes.notallyx.presentation.view.misc.Progress import com.philkes.notallyx.presentation.view.note.listitem.ListManager +import com.philkes.notallyx.presentation.viewmodel.preference.DateFormat import com.philkes.notallyx.utils.changehistory.ChangeHistory import com.philkes.notallyx.utils.changehistory.EditTextWithHistoryChange import java.io.File @@ -198,10 +196,10 @@ fun Menu.add( fun TextView.displayFormattedTimestamp( timestamp: Long?, - dateFormat: String, + dateFormat: DateFormat, prefixResId: Int? = null, ) { - if (dateFormat != DateFormat.none && timestamp != null) { + if (dateFormat != DateFormat.NONE && timestamp != null) { visibility = View.VISIBLE text = "${prefixResId?.let { getString(it) } ?: ""} ${formatTimestamp(timestamp, dateFormat)}" @@ -324,22 +322,6 @@ fun Context.canAuthenticateWithBiometrics(): Int { return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE } -fun LiveData.observeForeverSkipFirst(observer: Observer) { - var isFirstEvent = true - this.observeForever { value -> - if (isFirstEvent) { - isFirstEvent = false - } else { - observer.onChanged(value) - } - } -} - -fun String.fileNameWithoutExtension(): String { - return this.substringAfterLast("/") // Remove the path - .substringBeforeLast(".") // Remove the extension -} - fun Context.getFileName(uri: Uri): String? = when (uri.scheme) { ContentResolver.SCHEME_CONTENT -> getContentFileName(uri) @@ -458,10 +440,10 @@ fun Activity.checkNotificationPermission( } else onSuccess() } -private fun formatTimestamp(timestamp: Long, dateFormat: String): String { +private fun formatTimestamp(timestamp: Long, dateFormat: DateFormat): String { val date = Date(timestamp) return when (dateFormat) { - DateFormat.relative -> PrettyTime().format(date) + DateFormat.RELATIVE -> PrettyTime().format(date) else -> java.text.DateFormat.getDateInstance(java.text.DateFormat.FULL).format(date) } } diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/ConfigureWidgetActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/ConfigureWidgetActivity.kt index 073f5d6e..98da2812 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/ConfigureWidgetActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/ConfigureWidgetActivity.kt @@ -3,9 +3,9 @@ package com.philkes.notallyx.presentation.activity import android.appwidget.AppWidgetManager import android.content.Intent import android.os.Bundle -import com.philkes.notallyx.Preferences import com.philkes.notallyx.data.model.BaseNote import com.philkes.notallyx.presentation.activity.note.PickNoteActivity +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import com.philkes.notallyx.presentation.widget.WidgetProvider class ConfigureWidgetActivity : PickNoteActivity() { @@ -27,7 +27,7 @@ class ConfigureWidgetActivity : PickNoteActivity() { override fun onClick(position: Int) { if (position != -1) { - val preferences = Preferences.getInstance(application) + val preferences = NotallyXPreferences.getInstance(application) val baseNote = adapter.getItem(position) as BaseNote preferences.updateWidget(id, baseNote.id, baseNote.type) diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt index b5d66a50..dfe9494d 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt @@ -12,9 +12,9 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.viewbinding.ViewBinding import com.philkes.notallyx.NotallyXApplication -import com.philkes.notallyx.Preferences import com.philkes.notallyx.R -import com.philkes.notallyx.presentation.view.misc.BiometricLock.enabled +import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt abstract class LockedActivity : AppCompatActivity() { @@ -24,12 +24,12 @@ abstract class LockedActivity : AppCompatActivity() { ActivityResultLauncher protected lateinit var binding: T - protected lateinit var preferences: Preferences + protected lateinit var preferences: NotallyXPreferences override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) notallyXApplication = (application as NotallyXApplication) - preferences = Preferences.getInstance(application) + preferences = NotallyXPreferences.getInstance(application) biometricAuthenticationActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> @@ -42,7 +42,7 @@ abstract class LockedActivity : AppCompatActivity() { } override fun onResume() { - if (preferences.biometricLock.value == enabled) { + if (preferences.biometricLock.value == BiometricLock.ENABLED) { if (hasToAuthenticateWithBiometric()) { hide() showLockScreen() @@ -55,7 +55,7 @@ abstract class LockedActivity : AppCompatActivity() { override fun onPause() { super.onPause() - if (preferences.biometricLock.value == enabled) { + if (preferences.biometricLock.value == BiometricLock.ENABLED) { hide() } } @@ -63,7 +63,7 @@ abstract class LockedActivity : AppCompatActivity() { open fun showLockScreen() { showBiometricOrPinPrompt( true, - preferences.iv!!, + preferences.iv.value!!, biometricAuthenticationActivityResultLauncher, R.string.unlock, onSuccess = { unlock() }, diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/NotallyFragment.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/NotallyFragment.kt index a4df0d1f..f5aab24d 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/NotallyFragment.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/NotallyFragment.kt @@ -31,9 +31,9 @@ import com.philkes.notallyx.presentation.getQuantityString import com.philkes.notallyx.presentation.movedToResId import com.philkes.notallyx.presentation.view.Constants import com.philkes.notallyx.presentation.view.main.BaseNoteAdapter -import com.philkes.notallyx.presentation.view.misc.View as ViewPref import com.philkes.notallyx.presentation.view.note.listitem.ListItemListener import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel +import com.philkes.notallyx.presentation.viewmodel.preference.NotesView abstract class NotallyFragment : Fragment(), ListItemListener { @@ -139,11 +139,11 @@ abstract class NotallyFragment : Fragment(), ListItemListener { BaseNoteAdapter( model.actionMode.selectedIds, dateFormat.value, - notesSorting.value.first, + notesSorting.value.sortedBy, textSize.value, - maxItems, - maxLines, - maxTitle, + maxItems.value, + maxLines.value, + maxTitle.value, model.imageRoot, this@NotallyFragment, ) @@ -170,8 +170,8 @@ abstract class NotallyFragment : Fragment(), ListItemListener { binding?.ImageView?.isVisible = list.isEmpty() } - model.preferences.notesSorting.observe(viewLifecycleOwner) { (sortBy, sortDirection) -> - notesAdapter?.setSorting(sortBy, sortDirection) + model.preferences.notesSorting.observe(viewLifecycleOwner) { notesSort -> + notesAdapter?.setSorting(notesSort) } model.actionMode.closeListener.observe(viewLifecycleOwner) { event -> @@ -187,7 +187,7 @@ abstract class NotallyFragment : Fragment(), ListItemListener { private fun setupRecyclerView() { binding?.RecyclerView?.layoutManager = - if (model.preferences.view.value == ViewPref.grid) { + if (model.preferences.notesView.value == NotesView.GRID) { StaggeredGridLayoutManager(2, RecyclerView.VERTICAL) } else LinearLayoutManager(requireContext()) } diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SettingsFragment.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SettingsFragment.kt index a2f86b74..6b36d8dd 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SettingsFragment.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SettingsFragment.kt @@ -35,27 +35,20 @@ import com.philkes.notallyx.presentation.canAuthenticateWithBiometrics import com.philkes.notallyx.presentation.checkedTag import com.philkes.notallyx.presentation.setupImportProgressDialog import com.philkes.notallyx.presentation.setupProgressDialog -import com.philkes.notallyx.presentation.view.misc.AutoBackup -import com.philkes.notallyx.presentation.view.misc.AutoBackupMax -import com.philkes.notallyx.presentation.view.misc.AutoBackupPeriodDays -import com.philkes.notallyx.presentation.view.misc.BackupPassword -import com.philkes.notallyx.presentation.view.misc.BackupPassword.emptyPassword -import com.philkes.notallyx.presentation.view.misc.BiometricLock -import com.philkes.notallyx.presentation.view.misc.BiometricLock.disabled -import com.philkes.notallyx.presentation.view.misc.BiometricLock.enabled -import com.philkes.notallyx.presentation.view.misc.DateFormat -import com.philkes.notallyx.presentation.view.misc.ListInfo -import com.philkes.notallyx.presentation.view.misc.MaxItems -import com.philkes.notallyx.presentation.view.misc.MaxLines -import com.philkes.notallyx.presentation.view.misc.MaxTitle import com.philkes.notallyx.presentation.view.misc.MenuDialog -import com.philkes.notallyx.presentation.view.misc.NotesSorting -import com.philkes.notallyx.presentation.view.misc.SeekbarInfo -import com.philkes.notallyx.presentation.view.misc.SortDirection -import com.philkes.notallyx.presentation.view.misc.TextSize import com.philkes.notallyx.presentation.view.misc.TextWithIconAdapter -import com.philkes.notallyx.presentation.view.misc.Theme import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel +import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock +import com.philkes.notallyx.presentation.viewmodel.preference.Constants.BACKUP_PATH_EMPTY +import com.philkes.notallyx.presentation.viewmodel.preference.Constants.PASSWORD_EMPTY +import com.philkes.notallyx.presentation.viewmodel.preference.EnumPreference +import com.philkes.notallyx.presentation.viewmodel.preference.IntPreference +import com.philkes.notallyx.presentation.viewmodel.preference.NotesSort +import com.philkes.notallyx.presentation.viewmodel.preference.NotesSortBy +import com.philkes.notallyx.presentation.viewmodel.preference.NotesSortPreference +import com.philkes.notallyx.presentation.viewmodel.preference.SortDirection +import com.philkes.notallyx.presentation.viewmodel.preference.StringPreference +import com.philkes.notallyx.presentation.viewmodel.preference.TextProvider import com.philkes.notallyx.utils.Operations import com.philkes.notallyx.utils.backup.Export.scheduleAutoBackup import com.philkes.notallyx.utils.security.decryptDatabase @@ -76,22 +69,20 @@ class SettingsFragment : Fragment() { private fun setupBinding(binding: FragmentSettingsBinding) { model.preferences.apply { - view.observe(viewLifecycleOwner) { value -> - binding.View.setup(com.philkes.notallyx.presentation.view.misc.View, value) - } + notesView.observe(viewLifecycleOwner) { value -> binding.View.setup(notesView, value) } - theme.observe(viewLifecycleOwner) { value -> binding.Theme.setup(Theme, value) } + theme.observe(viewLifecycleOwner) { value -> binding.Theme.setup(theme, value) } dateFormat.observe(viewLifecycleOwner) { value -> - binding.DateFormat.setup(DateFormat, value) + binding.DateFormat.setup(dateFormat, value) } textSize.observe(viewLifecycleOwner) { value -> - binding.TextSize.setup(TextSize, value) + binding.TextSize.setup(textSize, value) } - notesSorting.observe(viewLifecycleOwner) { (sortBy, sortDirection) -> - binding.NotesSortOrder.setup(NotesSorting, sortBy, sortDirection) + notesSorting.observe(viewLifecycleOwner) { notesSort -> + binding.NotesSortOrder.setup(notesSorting, notesSort) } // TODO: Hide for now until checked auto-sort is working reliably @@ -99,29 +90,29 @@ class SettingsFragment : Fragment() { // binding.CheckedListItemSorting.setup(ListItemSorting, value) // } - binding.MaxItems.setup(MaxItems, maxItems) + binding.MaxItems.setup(maxItems) - binding.MaxLines.setup(MaxLines, maxLines) + binding.MaxLines.setup(maxLines) - binding.MaxTitle.setup(MaxTitle, maxTitle) + binding.MaxTitle.setup(maxTitle) - binding.AutoBackupMax.setup(AutoBackupMax, autoBackupMax) + binding.AutoBackupMax.setup(autoBackupMax) autoBackupPath.observe(viewLifecycleOwner) { value -> - binding.AutoBackup.setup(AutoBackup, value) + binding.AutoBackup.setupAutoBackup(autoBackupPath, value) } autoBackupPeriodDays.observe(viewLifecycleOwner) { value -> - binding.AutoBackupPeriodDays.setup(AutoBackupPeriodDays, value) + binding.AutoBackupPeriodDays.setup(autoBackupPeriodDays, value) scheduleAutoBackup(value.toLong(), requireContext()) } backupPassword.observe(viewLifecycleOwner) { value -> - binding.BackupPassword.setup(BackupPassword, value) + binding.BackupPassword.setupBackupPassword(backupPassword, value) } biometricLock.observe(viewLifecycleOwner) { value -> - binding.BiometricLock.setup(BiometricLock, value) + binding.BiometricLock.setup(biometricLock, value) } } @@ -209,7 +200,7 @@ class SettingsFragment : Fragment() { val layout = TextInputDialogBinding.inflate(layoutInflater, null, false) val password = model.preferences.backupPassword.value layout.InputText.apply { - if (password != emptyPassword) { + if (password != PASSWORD_EMPTY) { setText(password) } transformationMethod = PasswordTransformationMethod.getInstance() @@ -384,24 +375,23 @@ class SettingsFragment : Fragment() { .show() } - private fun PreferenceBinding.setup(info: ListInfo, value: String) { - Title.setText(info.title) - - val entries = info.getEntries(requireContext()) - val entryValues = info.getEntryValues() - - val checked = entryValues.indexOf(value) - val displayValue = entries[checked] - - Value.text = displayValue - + private inline fun PreferenceBinding.setup( + enumPreference: EnumPreference, + value: T, + ) where T : Enum, T : TextProvider { + Title.setText(enumPreference.titleResId!!) + val context = requireContext() + Value.text = value.getText(context) + val enumEntries = T::class.java.enumConstants!!.toList() + val entries = enumEntries.map { it.getText(requireContext()) }.toTypedArray() + val checked = enumEntries.indexOfFirst { it == value } root.setOnClickListener { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(info.title) + MaterialAlertDialogBuilder(context) + .setTitle(enumPreference.titleResId) .setSingleChoiceItems(entries, checked) { dialog, which -> dialog.cancel() - val newValue = entryValues[which] - model.savePreference(info, newValue) + val newValue = enumEntries[which] + model.savePreference(enumPreference, newValue) } .setNegativeButton(R.string.cancel, null) .show() @@ -409,35 +399,73 @@ class SettingsFragment : Fragment() { } private fun PreferenceBinding.setup( - info: NotesSorting, - sortBy: String, - sortDirection: SortDirection, + preference: EnumPreference, + value: BiometricLock, ) { - Title.setText(info.title) + Title.setText(preference.titleResId!!) - val entries = info.getEntries(requireContext()) - val entryValues = info.getEntryValues() + val context = requireContext() + Value.text = value.getText(context) + val enumEntries = BiometricLock.entries + val entries = enumEntries.map { context.getString(it.textResId) }.toTypedArray() + val checked = enumEntries.indexOfFirst { it == value } - val checked = entryValues.indexOf(sortBy) - val displayValue = entries[checked] + root.setOnClickListener { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(preference.titleResId) + .setSingleChoiceItems(entries, checked) { dialog, which -> + dialog.cancel() + val newValue = enumEntries[which] + if (newValue == BiometricLock.ENABLED) { + when (requireContext().canAuthenticateWithBiometrics()) { + BiometricManager.BIOMETRIC_SUCCESS -> showEnableBiometricLock() + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> + showNoBiometricsSupportToast() - Value.text = "$displayValue (${requireContext().getString(sortDirection.textResId)})" + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> + showBiometricsNotSetupDialog() + } + } else { + when (requireContext().canAuthenticateWithBiometrics()) { + BiometricManager.BIOMETRIC_SUCCESS -> showDisableBiometricLock() + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { + showNoBiometricsSupportToast() + model.savePreference( + model.preferences.biometricLock, + BiometricLock.DISABLED, + ) + } + + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + showBiometricsNotSetupDialog() + model.savePreference( + model.preferences.biometricLock, + BiometricLock.DISABLED, + ) + } + } + } + } + .setNegativeButton(R.string.cancel, null) + .show() + } + } + + private fun PreferenceBinding.setup(preference: NotesSortPreference, value: NotesSort) { + Title.setText(preference.titleResId!!) + + Value.text = value.getText(requireContext()) root.setOnClickListener { val layout = NotesSortDialogBinding.inflate(layoutInflater, null, false) - entries.zip(entryValues).forEachIndexed { idx, (choiceText, sortByValue) -> + NotesSortBy.entries.forEachIndexed { idx, notesSortBy -> ChoiceItemBinding.inflate(layoutInflater).root.apply { id = idx - text = choiceText - tag = sortByValue + text = requireContext().getString(notesSortBy.textResId) + tag = notesSortBy layout.NotesSortByRadioGroup.addView(this) - setCompoundDrawablesRelativeWithIntrinsicBounds( - NotesSorting.getSortIconResId(sortByValue), - 0, - 0, - 0, - ) - if (sortByValue == sortBy) { + setCompoundDrawablesRelativeWithIntrinsicBounds(notesSortBy.iconResId, 0, 0, 0) + if (notesSortBy == value.sortedBy) { layout.NotesSortByRadioGroup.check(this.id) } } @@ -450,37 +478,43 @@ class SettingsFragment : Fragment() { tag = sortDir setCompoundDrawablesRelativeWithIntrinsicBounds(sortDir.iconResId, 0, 0, 0) layout.NotesSortDirectionRadioGroup.addView(this) - if (sortDir == sortDirection) { + if (sortDir == value.sortDirection) { layout.NotesSortDirectionRadioGroup.check(this.id) } } } MaterialAlertDialogBuilder(requireContext()) - .setTitle(info.title) + .setTitle(preference.titleResId) .setView(layout.root) .setPositiveButton(R.string.save) { dialog, _ -> dialog.cancel() - val newSortBy = layout.NotesSortByRadioGroup.checkedTag() as String + val newSortBy = layout.NotesSortByRadioGroup.checkedTag() as NotesSortBy val newSortDirection = layout.NotesSortDirectionRadioGroup.checkedTag() as SortDirection - model.preferences.savePreference(info, newSortBy, newSortDirection) + model.savePreference( + model.preferences.notesSorting, + NotesSort(newSortBy, newSortDirection), + ) } .setNegativeButton(R.string.cancel, null) .show() } } - private fun PreferenceBinding.setup(info: BackupPassword, password: String) { - Title.setText(info.title) + private fun PreferenceBinding.setupBackupPassword( + preference: StringPreference, + password: String, + ) { + Title.setText(preference.titleResId!!) Value.transformationMethod = - if (password != emptyPassword) PasswordTransformationMethod.getInstance() else null - Value.text = if (password != emptyPassword) password else getText(R.string.tap_to_set_up) + if (password != PASSWORD_EMPTY) PasswordTransformationMethod.getInstance() else null + Value.text = if (password != PASSWORD_EMPTY) password else getText(R.string.tap_to_set_up) root.setOnClickListener { val layout = TextInputDialogBinding.inflate(layoutInflater, null, false) layout.InputText.apply { - if (password != emptyPassword) { + if (password != PASSWORD_EMPTY) { setText(password) } transformationMethod = PasswordTransformationMethod.getInstance() @@ -491,66 +525,22 @@ class SettingsFragment : Fragment() { visibility = View.VISIBLE } MaterialAlertDialogBuilder(requireContext()) - .setTitle(info.title) + .setTitle(preference.titleResId) .setView(layout.root) .setPositiveButton(R.string.save) { dialog, _ -> dialog.cancel() val updatedPassword = layout.InputText.text.toString() - model.preferences.savePreference(info, updatedPassword) + model.savePreference(preference, updatedPassword) } .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.clear) { dialog, _ -> dialog.cancel() - model.preferences.savePreference(info, emptyPassword) + model.savePreference(preference, PASSWORD_EMPTY) } .show() } } - private fun PreferenceBinding.setup(info: BiometricLock, value: String) { - Title.setText(info.title) - - val entries = info.getEntries(requireContext()) - val entryValues = info.getEntryValues() - - val checked = entryValues.indexOf(value) - val displayValue = entries[checked] - - Value.text = displayValue - - root.setOnClickListener { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(info.title) - .setSingleChoiceItems(entries, checked) { dialog, which -> - dialog.cancel() - val newValue = entryValues[which] - if (newValue == enabled) { - when (requireContext().canAuthenticateWithBiometrics()) { - BiometricManager.BIOMETRIC_SUCCESS -> showEnableBiometricLock() - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> - showNoBiometricsSupportToast() - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> - showBiometricsNotSetupDialog() - } - } else { - when (requireContext().canAuthenticateWithBiometrics()) { - BiometricManager.BIOMETRIC_SUCCESS -> showDisableBiometricLock() - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { - showNoBiometricsSupportToast() - model.preferences.biometricLock.value = disabled - } - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { - showBiometricsNotSetupDialog() - model.preferences.biometricLock.value = disabled - } - } - } - } - .setNegativeButton(R.string.cancel, null) - .show() - } - } - private fun showEnableBiometricLock() { showBiometricOrPinPrompt( false, @@ -559,10 +549,10 @@ class SettingsFragment : Fragment() { R.string.enable_lock_description, onSuccess = { cipher -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - model.preferences.iv = cipher.iv - val passphrase = model.preferences.generatePassphrase(cipher) + model.savePreference(model.preferences.iv, cipher.iv) + val passphrase = model.preferences.databaseEncryptionKey.init(cipher) encryptDatabase(requireContext(), passphrase) - model.savePreference(BiometricLock, enabled) + model.savePreference(model.preferences.biometricLock, BiometricLock.ENABLED) } val app = (activity?.application as NotallyXApplication) app.locked.value = false @@ -579,14 +569,14 @@ class SettingsFragment : Fragment() { disableLockActivityResultLauncher, R.string.disable_lock_title, R.string.disable_lock_description, - model.preferences.iv!!, + model.preferences.iv.value!!, onSuccess = { cipher -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val encryptedPassphrase = model.preferences.getDatabasePassphrase() + val encryptedPassphrase = model.preferences.databaseEncryptionKey.value val passphrase = cipher.doFinal(encryptedPassphrase) model.closeDatabase() decryptDatabase(requireContext(), passphrase) - model.savePreference(BiometricLock, disabled) + model.savePreference(model.preferences.biometricLock, BiometricLock.DISABLED) } showBiometricsDisabledToast() }, @@ -632,10 +622,10 @@ class SettingsFragment : Fragment() { .show() } - private fun PreferenceBinding.setup(info: AutoBackup, value: String) { - Title.setText(info.title) + private fun PreferenceBinding.setupAutoBackup(preference: StringPreference, value: String) { + Title.setText(preference.titleResId!!) - if (value == info.emptyPath) { + if (value == BACKUP_PATH_EMPTY) { Value.setText(R.string.tap_to_set_up) root.setOnClickListener { displayChooseFolderDialog() } @@ -655,14 +645,17 @@ class SettingsFragment : Fragment() { } } - private fun PreferenceSeekbarBinding.setup(info: SeekbarInfo, initialValue: Int) { - Title.setText(info.title) + private fun PreferenceSeekbarBinding.setup( + preference: IntPreference, + value: Int = preference.value, + ) { + Title.setText(preference.titleResId!!) Slider.apply { - valueTo = info.max.toFloat() - valueFrom = info.min.toFloat() - value = initialValue.toFloat() - addOnChangeListener { _, value, _ -> model.savePreference(info, value.toInt()) } + valueTo = preference.max.toFloat() + valueFrom = preference.min.toFloat() + this@apply.value = value.toFloat() + addOnChangeListener { _, value, _ -> model.savePreference(preference, value.toInt()) } } } diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt index d272e592..456da3d0 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt @@ -40,14 +40,12 @@ import com.philkes.notallyx.presentation.getParcelableExtraCompat import com.philkes.notallyx.presentation.getQuantityString import com.philkes.notallyx.presentation.setupProgressDialog import com.philkes.notallyx.presentation.view.Constants -import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByCreationDate -import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByModifiedDate -import com.philkes.notallyx.presentation.view.misc.TextSize import com.philkes.notallyx.presentation.view.note.ErrorAdapter import com.philkes.notallyx.presentation.view.note.audio.AudioAdapter import com.philkes.notallyx.presentation.view.note.preview.PreviewFileAdapter import com.philkes.notallyx.presentation.view.note.preview.PreviewImageAdapter import com.philkes.notallyx.presentation.viewmodel.NotallyModel +import com.philkes.notallyx.presentation.viewmodel.preference.NotesSortBy import com.philkes.notallyx.presentation.widget.WidgetProvider import com.philkes.notallyx.utils.FileError import com.philkes.notallyx.utils.Operations @@ -282,13 +280,12 @@ abstract class EditActivity(private val type: Type) : LockedActivity Pair(model.timestamp, R.string.creation_date) - autoSortByModifiedDate -> Pair(model.modifiedTimestamp, R.string.modified_date) + when (preferences.notesSorting.value.sortedBy) { + NotesSortBy.CREATION_DATE -> Pair(model.timestamp, R.string.creation_date) + NotesSortBy.MODIFIED_DATE -> Pair(model.modifiedTimestamp, R.string.modified_date) else -> Pair(null, null) } binding.Date.displayFormattedTimestamp(date, preferences.dateFormat.value, datePrefixResId) - binding.EnterTitle.setText(model.title) Operations.bindLabels(binding.LabelGroup, model.labels, model.textSize) @@ -602,9 +599,9 @@ abstract class EditActivity(private val type: Type) : LockedActivity ListItemSortedByCheckedCallback(adapter) + ListItemSort.AUTO_SORT_BY_CHECKED -> ListItemSortedByCheckedCallback(adapter) else -> ListItemNoSortCallback(adapter) } items = ListItemSortedList(sortCallback) diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/PickNoteActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/PickNoteActivity.kt index 50ad29de..74d4d254 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/PickNoteActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/PickNoteActivity.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager -import com.philkes.notallyx.Preferences import com.philkes.notallyx.R import com.philkes.notallyx.data.NotallyDatabase import com.philkes.notallyx.data.model.BaseNote @@ -14,9 +13,10 @@ import com.philkes.notallyx.data.model.Header import com.philkes.notallyx.databinding.ActivityPickNoteBinding import com.philkes.notallyx.presentation.activity.LockedActivity import com.philkes.notallyx.presentation.view.main.BaseNoteAdapter -import com.philkes.notallyx.presentation.view.misc.View import com.philkes.notallyx.presentation.view.note.listitem.ListItemListener import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences +import com.philkes.notallyx.presentation.viewmodel.preference.NotesView import com.philkes.notallyx.utils.IO.getExternalImagesDirectory import java.util.Collections import kotlinx.coroutines.Dispatchers @@ -37,18 +37,18 @@ open class PickNoteActivity : LockedActivity(), ListIte val result = Intent() setResult(RESULT_CANCELED, result) - val preferences = Preferences.getInstance(application) + val preferences = NotallyXPreferences.getInstance(application) adapter = with(preferences) { BaseNoteAdapter( Collections.emptySet(), dateFormat.value, - notesSorting.value.first, + notesSorting.value.sortedBy, textSize.value, - maxItems, - maxLines, - maxTitle, + maxItems.value, + maxLines.value, + maxTitle.value, application.getExternalImagesDirectory(), this@PickNoteActivity, ) @@ -58,7 +58,7 @@ open class PickNoteActivity : LockedActivity(), ListIte adapter = this@PickNoteActivity.adapter setHasFixedSize(true) layoutManager = - if (preferences.view.value == View.grid) { + if (preferences.notesView.value == NotesView.GRID) { StaggeredGridLayoutManager(2, RecyclerView.VERTICAL) } else LinearLayoutManager(this@PickNoteActivity) } diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt index 9fa0c763..817e8203 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteAdapter.kt @@ -13,17 +13,19 @@ import com.philkes.notallyx.databinding.RecyclerHeaderBinding import com.philkes.notallyx.presentation.view.main.sorting.BaseNoteCreationDateSort import com.philkes.notallyx.presentation.view.main.sorting.BaseNoteModifiedDateSort import com.philkes.notallyx.presentation.view.main.sorting.BaseNoteTitleSort -import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByModifiedDate -import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByTitle -import com.philkes.notallyx.presentation.view.misc.SortDirection import com.philkes.notallyx.presentation.view.note.listitem.ListItemListener +import com.philkes.notallyx.presentation.viewmodel.preference.DateFormat +import com.philkes.notallyx.presentation.viewmodel.preference.NotesSort +import com.philkes.notallyx.presentation.viewmodel.preference.NotesSortBy +import com.philkes.notallyx.presentation.viewmodel.preference.SortDirection +import com.philkes.notallyx.presentation.viewmodel.preference.TextSize import java.io.File class BaseNoteAdapter( private val selectedIds: Set, - private val dateFormat: String, - private val sortedBy: String, - private val textSize: String, + private val dateFormat: DateFormat, + private val sortedBy: NotesSortBy, + private val textSize: TextSize, private val maxItems: Int, private val maxLines: Int, private val maxTitle: Int, @@ -82,12 +84,12 @@ class BaseNoteAdapter( } } - fun setSorting(sortBy: String, sortDirection: SortDirection) { + fun setSorting(notesSort: NotesSort) { val sortCallback = - when (sortBy) { - autoSortByTitle -> BaseNoteTitleSort(this, sortDirection) - autoSortByModifiedDate -> BaseNoteModifiedDateSort(this, sortDirection) - else -> BaseNoteCreationDateSort(this, sortDirection) + when (notesSort.sortedBy) { + NotesSortBy.TITLE -> BaseNoteTitleSort(this, notesSort.sortDirection) + NotesSortBy.MODIFIED_DATE -> BaseNoteModifiedDateSort(this, notesSort.sortDirection) + else -> BaseNoteCreationDateSort(this, notesSort.sortDirection) } replaceSorting(sortCallback) } diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteVH.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteVH.kt index 1a8bb298..8aed1839 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteVH.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/view/main/BaseNoteVH.kt @@ -28,17 +28,17 @@ import com.philkes.notallyx.presentation.applySpans import com.philkes.notallyx.presentation.displayFormattedTimestamp import com.philkes.notallyx.presentation.dp import com.philkes.notallyx.presentation.getQuantityString -import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByCreationDate -import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByModifiedDate -import com.philkes.notallyx.presentation.view.misc.TextSize import com.philkes.notallyx.presentation.view.note.listitem.ListItemListener +import com.philkes.notallyx.presentation.viewmodel.preference.DateFormat +import com.philkes.notallyx.presentation.viewmodel.preference.NotesSortBy +import com.philkes.notallyx.presentation.viewmodel.preference.TextSize import com.philkes.notallyx.utils.Operations import java.io.File class BaseNoteVH( private val binding: RecyclerBaseNoteBinding, - private val dateFormat: String, - private val textSize: String, + private val dateFormat: DateFormat, + private val textSize: TextSize, private val maxItems: Int, maxLines: Int, maxTitle: Int, @@ -46,8 +46,8 @@ class BaseNoteVH( ) : RecyclerView.ViewHolder(binding.root) { init { - val title = TextSize.getDisplayTitleSize(textSize) - val body = TextSize.getDisplayBodySize(textSize) + val title = textSize.displayTitleSize + val body = textSize.displayBodySize binding.apply { Title.setTextSize(TypedValue.COMPLEX_UNIT_SP, title) @@ -75,7 +75,7 @@ class BaseNoteVH( binding.root.isChecked = checked } - fun bind(baseNote: BaseNote, imageRoot: File?, checked: Boolean, sortBy: String) { + fun bind(baseNote: BaseNote, imageRoot: File?, checked: Boolean, sortBy: NotesSortBy) { updateCheck(checked) when (baseNote.type) { @@ -84,8 +84,9 @@ class BaseNoteVH( } val (date, datePrefixResId) = when (sortBy) { - autoSortByCreationDate -> Pair(baseNote.timestamp, R.string.creation_date) - autoSortByModifiedDate -> Pair(baseNote.modifiedTimestamp, R.string.modified_date) + NotesSortBy.CREATION_DATE -> Pair(baseNote.timestamp, R.string.creation_date) + NotesSortBy.MODIFIED_DATE -> + Pair(baseNote.modifiedTimestamp, R.string.modified_date) else -> Pair(null, null) } binding.Date.displayFormattedTimestamp(date, dateFormat, datePrefixResId) diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteCreationDateSort.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteCreationDateSort.kt index c51764d1..b2daa3c3 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteCreationDateSort.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteCreationDateSort.kt @@ -2,7 +2,7 @@ package com.philkes.notallyx.presentation.view.main.sorting import androidx.recyclerview.widget.RecyclerView import com.philkes.notallyx.data.model.BaseNote -import com.philkes.notallyx.presentation.view.misc.SortDirection +import com.philkes.notallyx.presentation.viewmodel.preference.SortDirection class BaseNoteCreationDateSort(adapter: RecyclerView.Adapter<*>?, sortDirection: SortDirection) : BaseNoteSort(adapter, sortDirection) { diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteModifiedDateSort.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteModifiedDateSort.kt index 53d7ad25..741d92d1 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteModifiedDateSort.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteModifiedDateSort.kt @@ -2,7 +2,7 @@ package com.philkes.notallyx.presentation.view.main.sorting import androidx.recyclerview.widget.RecyclerView import com.philkes.notallyx.data.model.BaseNote -import com.philkes.notallyx.presentation.view.misc.SortDirection +import com.philkes.notallyx.presentation.viewmodel.preference.SortDirection class BaseNoteModifiedDateSort(adapter: RecyclerView.Adapter<*>?, sortDirection: SortDirection) : BaseNoteSort(adapter, sortDirection) { diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteSort.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteSort.kt index 0714adf5..f819d4a3 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteSort.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteSort.kt @@ -5,7 +5,7 @@ import androidx.recyclerview.widget.SortedListAdapterCallback import com.philkes.notallyx.data.model.BaseNote import com.philkes.notallyx.data.model.Header import com.philkes.notallyx.data.model.Item -import com.philkes.notallyx.presentation.view.misc.SortDirection +import com.philkes.notallyx.presentation.viewmodel.preference.SortDirection abstract class BaseNoteSort( adapter: RecyclerView.Adapter<*>?, diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteTitleSort.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteTitleSort.kt index 8dbfbe88..90a0296f 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteTitleSort.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteTitleSort.kt @@ -2,7 +2,7 @@ package com.philkes.notallyx.presentation.view.main.sorting import androidx.recyclerview.widget.RecyclerView import com.philkes.notallyx.data.model.BaseNote -import com.philkes.notallyx.presentation.view.misc.SortDirection +import com.philkes.notallyx.presentation.viewmodel.preference.SortDirection class BaseNoteTitleSort(adapter: RecyclerView.Adapter<*>?, sortDirection: SortDirection) : BaseNoteSort(adapter, sortDirection) { diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/misc/ListInfo.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/misc/ListInfo.kt deleted file mode 100644 index 927f0295..00000000 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/misc/ListInfo.kt +++ /dev/null @@ -1,197 +0,0 @@ -package com.philkes.notallyx.presentation.view.misc - -import android.content.Context -import com.philkes.notallyx.R -import java.text.DateFormat -import java.util.Date -import org.ocpsoft.prettytime.PrettyTime - -sealed interface ListInfo { - - val title: Int - - val key: String - val defaultValue: String - - fun getEntryValues(): Array - - fun getEntries(context: Context): Array - - fun convertToValues(ids: Array, context: Context): Array { - return Array(ids.size) { index -> - val id = ids[index] - context.getString(id) - } - } -} - -object View : ListInfo { - const val list = "list" - const val grid = "grid" - - override val title = R.string.view - override val key = "view" - override val defaultValue = list - - override fun getEntryValues() = arrayOf(list, grid) - - override fun getEntries(context: Context): Array { - val ids = arrayOf(R.string.list, R.string.grid) - return convertToValues(ids, context) - } -} - -object Theme : ListInfo { - const val dark = "dark" - const val light = "light" - const val followSystem = "followSystem" - - override val title = R.string.theme - override val key = "theme" - override val defaultValue = followSystem - - override fun getEntryValues() = arrayOf(dark, light, followSystem) - - override fun getEntries(context: Context): Array { - val ids = arrayOf(R.string.dark, R.string.light, R.string.follow_system) - return convertToValues(ids, context) - } -} - -object DateFormat : ListInfo { - const val none = "none" - const val relative = "relative" - const val absolute = "absolute" - - override val title = R.string.date_format - override val key = "dateFormat" - override val defaultValue = relative - - override fun getEntryValues() = arrayOf(none, relative, absolute) - - override fun getEntries(context: Context): Array { - val none = context.getString(R.string.none) - val date = Date(System.currentTimeMillis() - 86400000) - val relative = PrettyTime().format(date) - val absolute = DateFormat.getDateInstance(DateFormat.FULL).format(date) - return arrayOf(none, relative, absolute) - } -} - -object TextSize : ListInfo { - const val small = "small" - const val medium = "medium" - const val large = "large" - - override val title = R.string.text_size - override val key = "textSize" - override val defaultValue = medium - - override fun getEntryValues() = arrayOf(small, medium, large) - - override fun getEntries(context: Context): Array { - val ids = arrayOf(R.string.small, R.string.medium, R.string.large) - return convertToValues(ids, context) - } - - fun getEditBodySize(textSize: String): Float { - return when (textSize) { - small -> 14f - medium -> 16f - large -> 18f - else -> throw IllegalArgumentException("Invalid : $textSize") - } - } - - fun getEditTitleSize(textSize: String): Float { - return when (textSize) { - small -> 18f - medium -> 20f - large -> 22f - else -> throw IllegalArgumentException("Invalid : $textSize") - } - } - - fun getDisplayBodySize(textSize: String): Float { - return when (textSize) { - small -> 12f - medium -> 14f - large -> 16f - else -> throw IllegalArgumentException("Invalid : $textSize") - } - } - - fun getDisplayTitleSize(textSize: String): Float { - return when (textSize) { - small -> 14f - medium -> 16f - large -> 18f - else -> throw IllegalArgumentException("Invalid : $textSize") - } - } -} - -object ListItemSorting : ListInfo { - const val noAutoSort = "noAutoSort" - const val autoSortByChecked = "autoSortByChecked" - - override val title = R.string.checked_list_item_sorting - override val key = "checkedListItemSorting" - override val defaultValue = noAutoSort - - override fun getEntryValues() = arrayOf(noAutoSort, autoSortByChecked) - - override fun getEntries(context: Context): Array { - val ids = arrayOf(R.string.no_auto_sort, R.string.auto_sort_by_checked) - return convertToValues(ids, context) - } -} - -object NotesSorting : ListInfo { - const val autoSortByCreationDate = "autoSortByCreationDate" - const val autoSortByModifiedDate = "autoSortByModifiedDate" - const val autoSortByTitle = "autoSortByTitle" - - override val title = R.string.notes_sorted_by - override val key = "notesSorting" - const val directionKey = "notesSortingDirection" - override val defaultValue = autoSortByCreationDate - val defaultValueDirection = SortDirection.DESC.name - - override fun getEntryValues() = - arrayOf(autoSortByCreationDate, autoSortByModifiedDate, autoSortByTitle) - - override fun getEntries(context: Context): Array { - val ids = arrayOf(R.string.creation_date, R.string.modified_date, R.string.title) - return convertToValues(ids, context) - } - - fun getSortIconResId(sortBy: String): Int { - return when (sortBy) { - autoSortByModifiedDate -> R.drawable.edit_calendar - autoSortByTitle -> R.drawable.sort_by_alpha - else -> R.drawable.calendar_add_on - } - } -} - -enum class SortDirection(val textResId: Int, val iconResId: Int) { - ASC(R.string.ascending, R.drawable.arrow_upward), - DESC(R.string.descending, R.drawable.arrow_downward), -} - -object BiometricLock : ListInfo { - const val enabled = "enabled" - const val disabled = "disabled" - - override val title = R.string.biometric_lock - override val key = "biometricLock" - override val defaultValue = disabled - - override fun getEntryValues() = arrayOf(enabled, disabled) - - override fun getEntries(context: Context): Array { - val ids = arrayOf(R.string.enabled, R.string.disabled) - return convertToValues(ids, context) - } -} diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/misc/BetterLiveData.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/misc/NotNullLiveData.kt similarity index 76% rename from app/src/main/java/com/philkes/notallyx/presentation/view/misc/BetterLiveData.kt rename to app/src/main/java/com/philkes/notallyx/presentation/view/misc/NotNullLiveData.kt index e2c51722..23528c91 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/misc/BetterLiveData.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/view/misc/NotNullLiveData.kt @@ -3,7 +3,7 @@ package com.philkes.notallyx.presentation.view.misc import androidx.lifecycle.MutableLiveData // LiveData that doesn't accept null values -class BetterLiveData(value: T) : MutableLiveData(value) { +open class NotNullLiveData(value: T) : MutableLiveData(value) { override fun getValue(): T { return requireNotNull(super.getValue()) diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/misc/SeekbarInfo.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/misc/SeekbarInfo.kt deleted file mode 100644 index ab9b0d85..00000000 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/misc/SeekbarInfo.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.philkes.notallyx.presentation.view.misc - -import com.philkes.notallyx.R - -sealed interface SeekbarInfo { - - val title: Int - - val key: String - val defaultValue: Int - - val min: Int - val max: Int -} - -object MaxItems : SeekbarInfo { - - override val title = R.string.max_items_to_display - - override val key = "maxItemsToDisplayInList.v1" - override val defaultValue = 4 - - override val min = 1 - override val max = 10 -} - -object MaxLines : SeekbarInfo { - - override val title = R.string.max_lines_to_display - - override val key = "maxLinesToDisplayInNote.v1" - override val defaultValue = 8 - - override val min = 1 - override val max = 10 -} - -object MaxTitle : SeekbarInfo { - - override val title = R.string.max_lines_to_display_title - - override val key = "maxLinesToDisplayInTitle" - override val defaultValue = 1 - - override val min = 1 - override val max = 10 -} - -object AutoBackupMax : SeekbarInfo { - - override val title = R.string.max_backups - - override val key = "maxBackups" - override val defaultValue = 3 - - override val min = 1 - override val max = 10 -} - -object AutoBackupPeriodDays : SeekbarInfo { - - override val title = R.string.backup_period_days - - override val key = "autoBackupPeriodDays" - override val defaultValue = 1 - - override val min = 1 - override val max = 31 -} diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/misc/TextInfo.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/misc/TextInfo.kt deleted file mode 100644 index 63beecbc..00000000 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/misc/TextInfo.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.philkes.notallyx.presentation.view.misc - -import com.philkes.notallyx.R - -sealed interface TextInfo { - - val title: Int - - val key: String - val defaultValue: String -} - -object AutoBackup : TextInfo { - const val emptyPath = "emptyPath" - - override val title = R.string.auto_backup - - override val key = "autoBackup" - override val defaultValue = emptyPath -} - -object BackupPassword : TextInfo { - const val emptyPassword = "None" - - override val title = R.string.backup_password - - override val key = "backupPassword" - override val defaultValue = emptyPassword -} diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListItemAdapter.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListItemAdapter.kt index 7757a0fb..7a31a483 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListItemAdapter.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListItemAdapter.kt @@ -4,14 +4,15 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import com.philkes.notallyx.Preferences import com.philkes.notallyx.databinding.RecyclerListItemBinding import com.philkes.notallyx.presentation.view.note.listitem.sorting.ListItemSortedList +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences +import com.philkes.notallyx.presentation.viewmodel.preference.TextSize class ListItemAdapter( - private val textSize: String, + private val textSize: TextSize, elevation: Float, - private val preferences: Preferences, + private val preferences: NotallyXPreferences, private val listManager: ListManager, ) : RecyclerView.Adapter() { diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListItemVH.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListItemVH.kt index ef1847e6..f2fa9e63 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListItemVH.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListItemVH.kt @@ -14,22 +14,22 @@ import com.philkes.notallyx.data.model.ListItem import com.philkes.notallyx.databinding.RecyclerListItemBinding import com.philkes.notallyx.presentation.createListTextWatcherWithHistory import com.philkes.notallyx.presentation.setOnNextAction -import com.philkes.notallyx.presentation.view.misc.ListItemSorting import com.philkes.notallyx.presentation.view.misc.SwipeLayout.SwipeActionsListener -import com.philkes.notallyx.presentation.view.misc.TextSize +import com.philkes.notallyx.presentation.viewmodel.preference.ListItemSort +import com.philkes.notallyx.presentation.viewmodel.preference.TextSize class ListItemVH( val binding: RecyclerListItemBinding, val listManager: ListManager, touchHelper: ItemTouchHelper, - textSize: String, + textSize: TextSize, ) : RecyclerView.ViewHolder(binding.root) { private var editTextWatcher: TextWatcher private var dragHandleInitialY: Float = 0f init { - val body = TextSize.getEditBodySize(textSize) + val body = textSize.editBodySize binding.EditText.apply { setTextSize(TypedValue.COMPLEX_UNIT_SP, body) @@ -62,7 +62,7 @@ class ListItemVH( } } - fun bind(item: ListItem, firstItem: Boolean, autoSort: String) { + fun bind(item: ListItem, firstItem: Boolean, autoSort: ListItemSort) { updateEditText(item) updateCheckBox(item) @@ -70,7 +70,7 @@ class ListItemVH( updateDeleteButton(item) updateSwipe(item.isChild, !firstItem && !item.checked) - if (item.checked && autoSort == ListItemSorting.autoSortByChecked) { + if (item.checked && autoSort == ListItemSort.AUTO_SORT_BY_CHECKED) { binding.DragHandle.visibility = INVISIBLE } else { binding.DragHandle.visibility = VISIBLE diff --git a/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListManager.kt b/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListManager.kt index f66e9f93..8d0b73c3 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListManager.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/ListManager.kt @@ -5,11 +5,9 @@ import android.util.Log import android.view.inputmethod.InputMethodManager import android.widget.EditText import androidx.recyclerview.widget.RecyclerView -import com.philkes.notallyx.Preferences import com.philkes.notallyx.data.model.ListItem import com.philkes.notallyx.data.model.areAllChecked import com.philkes.notallyx.data.model.plus -import com.philkes.notallyx.presentation.view.misc.ListItemSorting import com.philkes.notallyx.presentation.view.note.listitem.sorting.ListItemSortedList import com.philkes.notallyx.presentation.view.note.listitem.sorting.deleteItem import com.philkes.notallyx.presentation.view.note.listitem.sorting.filter @@ -24,6 +22,8 @@ import com.philkes.notallyx.presentation.view.note.listitem.sorting.setCheckedWi import com.philkes.notallyx.presentation.view.note.listitem.sorting.setIsChild import com.philkes.notallyx.presentation.view.note.listitem.sorting.shiftItemOrders import com.philkes.notallyx.presentation.view.note.listitem.sorting.toReadableString +import com.philkes.notallyx.presentation.viewmodel.preference.ListItemSort +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import com.philkes.notallyx.utils.changehistory.ChangeCheckedForAllChange import com.philkes.notallyx.utils.changehistory.ChangeHistory import com.philkes.notallyx.utils.changehistory.DeleteCheckedChange @@ -41,7 +41,7 @@ import com.philkes.notallyx.utils.changehistory.ListMoveChange class ListManager( private val recyclerView: RecyclerView, private val changeHistory: ChangeHistory, - private val preferences: Preferences, + private val preferences: NotallyXPreferences, private val inputMethodManager: InputMethodManager, ) { @@ -396,7 +396,7 @@ class ListManager( } private fun isAutoSortByCheckedEnabled() = - preferences.listItemSorting.value == ListItemSorting.autoSortByChecked + preferences.listItemSorting.value == ListItemSort.AUTO_SORT_BY_CHECKED private val Int.isBeforeChildItemOfOtherParent: Boolean get() { diff --git a/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt index c616e15f..a8347e54 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt @@ -13,7 +13,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.viewModelScope import androidx.room.withTransaction -import com.philkes.notallyx.Preferences import com.philkes.notallyx.R import com.philkes.notallyx.data.NotallyDatabase import com.philkes.notallyx.data.dao.BaseNoteDao @@ -38,10 +37,10 @@ import com.philkes.notallyx.data.model.SearchResult import com.philkes.notallyx.data.model.Type import com.philkes.notallyx.presentation.applySpans import com.philkes.notallyx.presentation.getQuantityString -import com.philkes.notallyx.presentation.view.misc.AutoBackup -import com.philkes.notallyx.presentation.view.misc.ListInfo import com.philkes.notallyx.presentation.view.misc.Progress -import com.philkes.notallyx.presentation.view.misc.SeekbarInfo +import com.philkes.notallyx.presentation.viewmodel.preference.BasePreference +import com.philkes.notallyx.presentation.viewmodel.preference.Constants.BACKUP_PATH_EMPTY +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import com.philkes.notallyx.utils.ActionMode import com.philkes.notallyx.utils.Cache import com.philkes.notallyx.utils.IO.deleteAttachments @@ -105,7 +104,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) { private val pinned = Header(app.getString(R.string.pinned)) private val others = Header(app.getString(R.string.others)) - val preferences = Preferences.getInstance(app) + val preferences = NotallyXPreferences.getInstance(app) val imageRoot = app.getExternalImagesDirectory() val fileRoot = app.getExternalFilesDirectory() @@ -181,24 +180,20 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) { private fun transform(list: List) = transform(list, pinned, others) - fun savePreference(info: SeekbarInfo, value: Int) = executeAsync { - preferences.savePreference(info, value) - } - - fun savePreference(info: ListInfo, value: String) = executeAsync { - preferences.savePreference(info, value) - } - fun disableAutoBackup() { clearPersistedUriPermissions() - executeAsync { preferences.savePreference(AutoBackup, AutoBackup.emptyPath) } + savePreference(preferences.autoBackupPath, BACKUP_PATH_EMPTY) } fun setAutoBackupPath(uri: Uri) { clearPersistedUriPermissions() val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION app.contentResolver.takePersistableUriPermission(uri, flags) - executeAsync { preferences.savePreference(AutoBackup, uri.toString()) } + savePreference(preferences.autoBackupPath, uri.toString()) + } + + fun savePreference(preference: BasePreference, value: T) { + executeAsync { preference.save(value) } } /** diff --git a/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/NotallyModel.kt b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/NotallyModel.kt index ec4f20cc..458ae92a 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/NotallyModel.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/NotallyModel.kt @@ -16,7 +16,6 @@ import androidx.core.text.getSpans import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import com.philkes.notallyx.Preferences import com.philkes.notallyx.R import com.philkes.notallyx.data.DataUtil import com.philkes.notallyx.data.NotallyDatabase @@ -30,8 +29,9 @@ import com.philkes.notallyx.data.model.ListItem import com.philkes.notallyx.data.model.SpanRepresentation import com.philkes.notallyx.data.model.Type import com.philkes.notallyx.presentation.applySpans -import com.philkes.notallyx.presentation.view.misc.BetterLiveData +import com.philkes.notallyx.presentation.view.misc.NotNullLiveData import com.philkes.notallyx.presentation.view.misc.Progress +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import com.philkes.notallyx.presentation.widget.WidgetProvider import com.philkes.notallyx.utils.Cache import com.philkes.notallyx.utils.Event @@ -51,7 +51,7 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) { private val database = NotallyDatabase.getDatabase(app) private lateinit var baseNoteDao: BaseNoteDao - val textSize = Preferences.getInstance(app).textSize.value + val textSize = NotallyXPreferences.getInstance(app).textSize.value var isNewNote = true @@ -71,9 +71,9 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) { var body: Editable = SpannableStringBuilder() val items = ArrayList() - val images = BetterLiveData>(emptyList()) - val files = BetterLiveData>(emptyList()) - val audios = BetterLiveData>(emptyList()) + val images = NotNullLiveData>(emptyList()) + val files = NotNullLiveData>(emptyList()) + val audios = NotNullLiveData>(emptyList()) val addingFiles = MutableLiveData() val eventBus = MutableLiveData>>() diff --git a/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotallyXPreferences.kt b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotallyXPreferences.kt new file mode 100644 index 00000000..9f2eef49 --- /dev/null +++ b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotallyXPreferences.kt @@ -0,0 +1,157 @@ +package com.philkes.notallyx.presentation.viewmodel.preference + +import android.app.Application +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.philkes.notallyx.R +import com.philkes.notallyx.data.model.Type +import com.philkes.notallyx.presentation.viewmodel.preference.Constants.BACKUP_PATH_EMPTY +import com.philkes.notallyx.presentation.viewmodel.preference.Constants.PASSWORD_EMPTY + +class NotallyXPreferences private constructor(app: Application) { + + private val preferences = PreferenceManager.getDefaultSharedPreferences(app) + + private val encryptedPreferences by lazy { + EncryptedSharedPreferences.create( + app, + "secret_shared_prefs", + MasterKey.Builder(app).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + } + + // Main thread (unfortunately) + val theme = createEnumPreference(preferences, "theme", Theme.FOLLOW_SYSTEM, R.string.theme) + val textSize = + createEnumPreference(preferences, "textSize", TextSize.MEDIUM, R.string.text_size) + val dateFormat = + createEnumPreference(preferences, "dateFormat", DateFormat.RELATIVE, R.string.date_format) + + val notesView = createEnumPreference(preferences, "view", NotesView.LIST, R.string.view) + val notesSorting = NotesSortPreference(preferences) + val listItemSorting = + createEnumPreference( + preferences, + "checkedListItemSorting", + ListItemSort.NO_AUTO_SORT, + R.string.checked_list_item_sorting, + ) + + val maxItems = + IntPreference( + "maxItemsToDisplayInList.v1", + preferences, + 4, + 1, + 10, + R.string.max_items_to_display, + ) + val maxLines = + IntPreference( + "maxLinesToDisplayInNote.v1", + preferences, + 8, + 1, + 10, + R.string.max_lines_to_display, + ) + val maxTitle = + IntPreference( + "maxLinesToDisplayInTitle", + preferences, + 1, + 1, + 10, + R.string.max_lines_to_display_title, + ) + + val autoBackupPath = + StringPreference("autoBackup", preferences, BACKUP_PATH_EMPTY, R.string.auto_backup) + val autoBackupPeriodDays = + IntPreference("autoBackupPeriodDays", preferences, 1, 1, 31, R.string.backup_period_days) + val autoBackupMax = IntPreference("maxBackups", preferences, 3, 1, 10, R.string.max_backups) + + val backupPassword by lazy { + StringPreference( + "backupPassword", + encryptedPreferences, + PASSWORD_EMPTY, + R.string.backup_password, + ) + } + + val biometricLock = + createEnumPreference( + preferences, + "biometricLock", + BiometricLock.DISABLED, + R.string.biometric_lock, + ) + + val iv = ByteArrayPreference("encryption_iv", preferences, null) + val databaseEncryptionKey = + EncryptedPassphrasePreference("database_encryption_key", preferences, ByteArray(0)) + + fun getWidgetData(id: Int) = preferences.getLong("widget:$id", 0) + + fun getWidgetNoteType(id: Int) = + preferences.getString("widgetNoteType:$id", null)?.let { Type.valueOf(it) } + + fun deleteWidget(id: Int) { + preferences.edit(true) { + remove("widget:$id") + remove("widgetNoteType:$id") + } + } + + fun updateWidget(id: Int, noteId: Long, noteType: Type) { + preferences.edit(true) { + putLong("widget:$id", noteId) + putString("widgetNoteType:$id", noteType.name) + commit() + } + } + + fun getUpdatableWidgets(noteIds: LongArray? = null): List> { + val updatableWidgets = ArrayList>() + val pairs = preferences.all + pairs.keys.forEach { key -> + val token = "widget:" + if (key.startsWith(token)) { + val end = key.substringAfter(token) + val id = end.toIntOrNull() + if (id != null) { + val value = pairs[key] as? Long + if (value != null) { + if (noteIds == null || noteIds.contains(value)) { + updatableWidgets.add(Pair(id, value)) + } + } + } + } + } + return updatableWidgets + } + + fun showDateCreated(): Boolean { + return dateFormat.value != DateFormat.NONE + } + + companion object { + + @Volatile private var instance: NotallyXPreferences? = null + + fun getInstance(app: Application): NotallyXPreferences { + return instance + ?: synchronized(this) { + val instance = NotallyXPreferences(app) + Companion.instance = instance + return instance + } + } + } +} diff --git a/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotesSorting.kt b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotesSorting.kt new file mode 100644 index 00000000..798733cd --- /dev/null +++ b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotesSorting.kt @@ -0,0 +1,68 @@ +package com.philkes.notallyx.presentation.viewmodel.preference + +import android.content.Context +import android.content.SharedPreferences +import android.content.SharedPreferences.Editor +import com.philkes.notallyx.R + +class NotesSortPreference(sharedPreferences: SharedPreferences) : + BasePreference(sharedPreferences, NotesSort(), R.string.notes_sorted_by) { + + override fun getValue(sharedPreferences: SharedPreferences): NotesSort { + val sortedByName = sharedPreferences.getString(SORTED_BY_KEY, null) + val sortedBy = + sortedByName?.let { + try { + NotesSortBy.fromValue(sortedByName) + } catch (e: Exception) { + defaultValue.sortedBy + } + } ?: defaultValue.sortedBy + val sortDirectionName = sharedPreferences.getString(SORT_DIRECTION_KEY, null) + val sortDirection = + sortDirectionName?.let { + try { + SortDirection.valueOf(sortDirectionName) + } catch (e: Exception) { + defaultValue.sortDirection + } + } ?: defaultValue.sortDirection + return NotesSort(sortedBy, sortDirection) + } + + override fun Editor.put(value: NotesSort) { + putString(SORTED_BY_KEY, value.sortedBy.value) + putString(SORT_DIRECTION_KEY, value.sortDirection.name) + } + + companion object { + const val SORTED_BY_KEY = "notesSorting" + const val SORT_DIRECTION_KEY = "notesSortingDirection" + } +} + +enum class SortDirection(val textResId: Int, val iconResId: Int) { + ASC(R.string.ascending, R.drawable.arrow_upward), + DESC(R.string.descending, R.drawable.arrow_downward), +} + +enum class NotesSortBy(val textResId: Int, val iconResId: Int, val value: String) { + TITLE(R.string.title, R.drawable.sort_by_alpha, "autoSortByTitle"), + CREATION_DATE(R.string.creation_date, R.drawable.calendar_add_on, "autoSortByCreationDate"), + MODIFIED_DATE(R.string.modified_date, R.drawable.edit_calendar, "autoSortByModifiedDate"); + + companion object { + fun fromValue(value: String): NotesSortBy? { + return entries.find { it.value == value } + } + } +} + +data class NotesSort( + val sortedBy: NotesSortBy = NotesSortBy.CREATION_DATE, + val sortDirection: SortDirection = SortDirection.DESC, +) : TextProvider { + override fun getText(context: Context): String { + return "${context.getString(sortedBy.textResId)} (${context.getString(sortDirection.textResId)})" + } +} diff --git a/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/Preference.kt b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/Preference.kt new file mode 100644 index 00000000..bc2ba9a8 --- /dev/null +++ b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/Preference.kt @@ -0,0 +1,322 @@ +package com.philkes.notallyx.presentation.viewmodel.preference + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import androidx.core.content.edit +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer +import com.philkes.notallyx.R +import com.philkes.notallyx.data.model.toPreservedByteArray +import com.philkes.notallyx.data.model.toPreservedString +import com.philkes.notallyx.presentation.view.misc.NotNullLiveData +import java.security.SecureRandom +import java.util.Date +import java.util.Locale +import javax.crypto.Cipher +import org.ocpsoft.prettytime.PrettyTime + +/** + * Every Preference can be observed like a [NotNullLiveData]. + * + * @param titleResId Optional string resource id, if preference can be set via the UI. + */ +abstract class BasePreference( + private val sharedPreferences: SharedPreferences, + protected val defaultValue: T, + val titleResId: Int? = null, +) { + private var data: NotNullLiveData? = null + private var cachedValue: T? = null + + val value: T + get() { + if (cachedValue == null) { + cachedValue = getValue(sharedPreferences) + } + return cachedValue!! + } + + protected abstract fun getValue(sharedPreferences: SharedPreferences): T + + private fun getData(): NotNullLiveData { + if (data == null) { + data = NotNullLiveData(value) + } + return data as NotNullLiveData + } + + internal fun save(value: T) { + sharedPreferences.edit(true) { put(value) } + cachedValue = value + getData().postValue(value) + } + + protected abstract fun SharedPreferences.Editor.put(value: T) + + fun observe(lifecycleOwner: LifecycleOwner, observer: Observer) { + getData().observe(lifecycleOwner, observer) + } + + fun observeForever(observer: Observer) { + getData().observeForever(observer) + } + + fun removeObserver(observer: Observer) { + getData().removeObserver(observer) + } + + fun removeObservers(lifecycleOwner: LifecycleOwner) { + getData().removeObservers(lifecycleOwner) + } +} + +fun BasePreference.observeForeverSkipFirst(observer: Observer) { + var isFirstEvent = true + this.observeForever { value -> + if (isFirstEvent) { + isFirstEvent = false + } else { + observer.onChanged(value) + } + } +} + +interface TextProvider { + fun getText(context: Context): String +} + +interface StaticTextProvider : TextProvider { + val textResId: Int + + override fun getText(context: Context): String { + return context.getString(textResId) + } +} + +class EnumPreference( + sharedPreferences: SharedPreferences, + private val key: String, + defaultValue: T, + private val enumClass: Class, + titleResId: Int? = null, +) : BasePreference(sharedPreferences, defaultValue, titleResId) where +T : Enum, +T : TextProvider { + + override fun getValue(sharedPreferences: SharedPreferences): T { + val storedValue = sharedPreferences.getString(key, null) + return try { + storedValue?.let { java.lang.Enum.valueOf(enumClass, it.fromCamelCaseToEnumName()) } + ?: defaultValue + } catch (e: IllegalArgumentException) { + defaultValue + } + } + + override fun SharedPreferences.Editor.put(value: T) { + putString(key, value.name.toCamelCase()) + } +} + +inline fun createEnumPreference( + sharedPreferences: SharedPreferences, + key: String, + defaultValue: T, + titleResId: Int? = null, +): EnumPreference where T : Enum, T : TextProvider { + return EnumPreference(sharedPreferences, key, defaultValue, T::class.java, titleResId) +} + +class IntPreference( + private val key: String, + sharedPreferences: SharedPreferences, + defaultValue: Int, + val min: Int, + val max: Int, + titleResId: Int? = null, +) : BasePreference(sharedPreferences, defaultValue, titleResId) { + + override fun getValue(sharedPreferences: SharedPreferences): Int { + return sharedPreferences.getInt(key, defaultValue) + } + + override fun SharedPreferences.Editor.put(value: Int) { + putInt(key, value) + } +} + +class StringPreference( + private val key: String, + sharedPreferences: SharedPreferences, + defaultValue: String, + titleResId: Int? = null, +) : BasePreference(sharedPreferences, defaultValue, titleResId) { + + override fun getValue(sharedPreferences: SharedPreferences): String { + return sharedPreferences.getString(key, defaultValue)!! + } + + override fun SharedPreferences.Editor.put(value: String) { + putString(key, value) + } +} + +class ByteArrayPreference( + private val key: String, + sharedPreferences: SharedPreferences, + defaultValue: ByteArray?, + titleResId: Int? = null, +) : BasePreference(sharedPreferences, defaultValue, titleResId) { + + override fun getValue(sharedPreferences: SharedPreferences): ByteArray? { + return sharedPreferences.getString(key, null)?.toPreservedByteArray ?: defaultValue + } + + override fun SharedPreferences.Editor.put(value: ByteArray?) { + putString(key, value?.toPreservedString) + } +} + +class EncryptedPassphrasePreference( + private val key: String, + sharedPreferences: SharedPreferences, + defaultValue: ByteArray, + titleResId: Int? = null, +) : BasePreference(sharedPreferences, defaultValue, titleResId) { + + override fun getValue(sharedPreferences: SharedPreferences): ByteArray { + return sharedPreferences.getString(key, null)?.toPreservedByteArray ?: defaultValue + } + + override fun SharedPreferences.Editor.put(value: ByteArray) { + putString(key, value.toPreservedString) + } + + fun init(cipher: Cipher): ByteArray { + val random = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + SecureRandom.getInstanceStrong() + } else { + SecureRandom() + } + val result = ByteArray(64) + + random.nextBytes(result) + + // filter out zero byte values, as SQLCipher does not like them + while (result.contains(0)) { + random.nextBytes(result) + } + + val encryptedPassphrase = cipher.doFinal(result) + save(encryptedPassphrase) + return result + } +} + +enum class NotesView(override val textResId: Int) : StaticTextProvider { + LIST(R.string.list), + GRID(R.string.grid), +} + +enum class Theme(override val textResId: Int) : StaticTextProvider { + DARK(R.string.dark), + LIGHT(R.string.light), + FOLLOW_SYSTEM(R.string.follow_system), +} + +enum class DateFormat : TextProvider { + NONE, + RELATIVE, + ABSOLUTE; + + override fun getText(context: Context): String { + val date = Date(System.currentTimeMillis() - 86400000) + return when (this) { + NONE -> context.getString(R.string.none) + RELATIVE -> PrettyTime().format(date) + ABSOLUTE -> java.text.DateFormat.getDateInstance(java.text.DateFormat.FULL).format(date) + } + } +} + +enum class TextSize(override val textResId: Int) : StaticTextProvider { + SMALL(R.string.small), + MEDIUM(R.string.medium), + LARGE(R.string.large); + + val editBodySize: Float + get() { + return when (this) { + SMALL -> 14f + MEDIUM -> 16f + LARGE -> 18f + } + } + + val editTitleSize: Float + get() { + return when (this) { + SMALL -> 18f + MEDIUM -> 20f + LARGE -> 22f + } + } + + val displayBodySize: Float + get() { + return when (this) { + SMALL -> 12f + MEDIUM -> 14f + LARGE -> 16f + } + } + + val displayTitleSize: Float + get() { + return when (this) { + SMALL -> 14f + MEDIUM -> 16f + LARGE -> 18f + } + } +} + +enum class ListItemSort(override val textResId: Int) : StaticTextProvider { + NO_AUTO_SORT(R.string.no_auto_sort), + AUTO_SORT_BY_CHECKED(R.string.auto_sort_by_checked), +} + +enum class BiometricLock(override val textResId: Int) : StaticTextProvider { + ENABLED(R.string.enabled), + DISABLED(R.string.disabled), +} + +object Constants { + const val BACKUP_PATH_EMPTY = "emptyPath" + const val PASSWORD_EMPTY = "None" +} + +fun String.toCamelCase(): String { + return this.lowercase() + .split("_") + .mapIndexed { index, word -> + if (index == 0) word + else + word.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() + } + } + .joinToString("") +} + +fun String.fromCamelCaseToEnumName(): String { + return this.fold(StringBuilder()) { acc, char -> + if (char.isUpperCase() && acc.isNotEmpty()) { + acc.append("_") + } + acc.append(char.uppercase()) + } + .toString() +} diff --git a/app/src/main/java/com/philkes/notallyx/presentation/widget/WidgetFactory.kt b/app/src/main/java/com/philkes/notallyx/presentation/widget/WidgetFactory.kt index d91eacda..58e82d65 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/widget/WidgetFactory.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/widget/WidgetFactory.kt @@ -8,12 +8,11 @@ import android.view.View import android.widget.RemoteViews import android.widget.RemoteViewsService import com.philkes.notallyx.NotallyXApplication -import com.philkes.notallyx.Preferences import com.philkes.notallyx.R import com.philkes.notallyx.data.NotallyDatabase import com.philkes.notallyx.data.model.BaseNote import com.philkes.notallyx.data.model.Type -import com.philkes.notallyx.presentation.view.misc.TextSize +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import com.philkes.notallyx.presentation.widget.WidgetProvider.Companion.getSelectNoteIntent class WidgetFactory( @@ -24,7 +23,7 @@ class WidgetFactory( private var baseNote: BaseNote? = null private lateinit var database: NotallyDatabase - private val preferences = Preferences.getInstance(app) + private val preferences = NotallyXPreferences.getInstance(app) init { NotallyDatabase.getDatabase(app).observeForever { database = it } @@ -64,14 +63,11 @@ class WidgetFactory( private fun getNoteView(note: BaseNote): RemoteViews { return RemoteViews(app.packageName, R.layout.widget_note).apply { - setTextViewTextSize( - R.id.Title, - TypedValue.COMPLEX_UNIT_SP, - TextSize.getDisplayTitleSize(preferences.textSize.value), - ) + val textSize = preferences.textSize.value + setTextViewTextSize(R.id.Title, TypedValue.COMPLEX_UNIT_SP, textSize.displayTitleSize) setTextViewText(R.id.Title, note.title) - val bodyTextSize = TextSize.getDisplayBodySize(preferences.textSize.value) + val bodyTextSize = textSize.displayBodySize setTextViewTextSize(R.id.Note, TypedValue.COMPLEX_UNIT_SP, bodyTextSize) if (note.body.isNotEmpty()) { @@ -91,7 +87,7 @@ class WidgetFactory( setTextViewTextSize( R.id.Title, TypedValue.COMPLEX_UNIT_SP, - TextSize.getDisplayTitleSize(preferences.textSize.value), + preferences.textSize.value.displayTitleSize, ) setTextViewText(R.id.Title, list.title) @@ -116,7 +112,7 @@ class WidgetFactory( setTextViewTextSize( R.id.CheckBox, TypedValue.COMPLEX_UNIT_SP, - TextSize.getDisplayBodySize(preferences.textSize.value), + preferences.textSize.value.displayBodySize, ) setTextViewText(R.id.CheckBox, item.body) setInt( diff --git a/app/src/main/java/com/philkes/notallyx/presentation/widget/WidgetProvider.kt b/app/src/main/java/com/philkes/notallyx/presentation/widget/WidgetProvider.kt index d82ea678..97e6383b 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/widget/WidgetProvider.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/widget/WidgetProvider.kt @@ -9,7 +9,6 @@ import android.content.Intent import android.net.Uri import android.os.Build import android.widget.RemoteViews -import com.philkes.notallyx.Preferences import com.philkes.notallyx.R import com.philkes.notallyx.data.NotallyDatabase import com.philkes.notallyx.data.dao.BaseNoteDao @@ -21,6 +20,7 @@ import com.philkes.notallyx.presentation.activity.ConfigureWidgetActivity import com.philkes.notallyx.presentation.activity.note.EditListActivity import com.philkes.notallyx.presentation.activity.note.EditNoteActivity import com.philkes.notallyx.presentation.view.Constants +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -110,7 +110,7 @@ class WidgetProvider : AppWidgetProvider() { override fun onDeleted(context: Context, appWidgetIds: IntArray) { val app = context.applicationContext as Application - val preferences = Preferences.getInstance(app) + val preferences = NotallyXPreferences.getInstance(app) appWidgetIds.forEach { id -> preferences.deleteWidget(id) } } @@ -121,7 +121,7 @@ class WidgetProvider : AppWidgetProvider() { appWidgetIds: IntArray, ) { val app = context.applicationContext as Application - val preferences = Preferences.getInstance(app) + val preferences = NotallyXPreferences.getInstance(app) appWidgetIds.forEach { id -> val noteId = preferences.getWidgetData(id) @@ -134,7 +134,7 @@ class WidgetProvider : AppWidgetProvider() { fun updateWidgets(context: Context, noteIds: LongArray? = null, locked: Boolean = false) { val app = context.applicationContext as Application - val preferences = Preferences.getInstance(app) + val preferences = NotallyXPreferences.getInstance(app) val manager = AppWidgetManager.getInstance(context) val updatableWidgets = preferences.getUpdatableWidgets(noteIds) diff --git a/app/src/main/java/com/philkes/notallyx/utils/ActionMode.kt b/app/src/main/java/com/philkes/notallyx/utils/ActionMode.kt index f666b2cd..587c31c0 100644 --- a/app/src/main/java/com/philkes/notallyx/utils/ActionMode.kt +++ b/app/src/main/java/com/philkes/notallyx/utils/ActionMode.kt @@ -2,12 +2,12 @@ package com.philkes.notallyx.utils import androidx.lifecycle.MutableLiveData import com.philkes.notallyx.data.model.BaseNote -import com.philkes.notallyx.presentation.view.misc.BetterLiveData +import com.philkes.notallyx.presentation.view.misc.NotNullLiveData class ActionMode { - val enabled = BetterLiveData(false) - val count = BetterLiveData(0) + val enabled = NotNullLiveData(false) + val count = NotNullLiveData(0) val selectedNotes = HashMap() val selectedIds = selectedNotes.keys val closeListener = MutableLiveData>>() diff --git a/app/src/main/java/com/philkes/notallyx/utils/Operations.kt b/app/src/main/java/com/philkes/notallyx/utils/Operations.kt index 26e38967..5f48b3ab 100644 --- a/app/src/main/java/com/philkes/notallyx/utils/Operations.kt +++ b/app/src/main/java/com/philkes/notallyx/utils/Operations.kt @@ -19,7 +19,7 @@ import com.philkes.notallyx.R import com.philkes.notallyx.data.model.Color import com.philkes.notallyx.data.model.ListItem import com.philkes.notallyx.databinding.LabelBinding -import com.philkes.notallyx.presentation.view.misc.TextSize +import com.philkes.notallyx.presentation.viewmodel.preference.TextSize import java.io.File import java.io.FileOutputStream import java.io.OutputStreamWriter @@ -99,7 +99,7 @@ object Operations { } } - fun bindLabels(group: ChipGroup, labels: List, textSize: String) { + fun bindLabels(group: ChipGroup, labels: List, textSize: TextSize) { if (labels.isEmpty()) { group.visibility = View.GONE } else { @@ -107,7 +107,7 @@ object Operations { group.removeAllViews() val inflater = LayoutInflater.from(group.context) - val labelSize = TextSize.getDisplayBodySize(textSize) + val labelSize = textSize.displayBodySize for (label in labels) { val view = LabelBinding.inflate(inflater, group, true).root diff --git a/app/src/main/java/com/philkes/notallyx/utils/audio/AudioRecordService.kt b/app/src/main/java/com/philkes/notallyx/utils/audio/AudioRecordService.kt index 84ed0ce1..67c06aaf 100644 --- a/app/src/main/java/com/philkes/notallyx/utils/audio/AudioRecordService.kt +++ b/app/src/main/java/com/philkes/notallyx/utils/audio/AudioRecordService.kt @@ -6,7 +6,7 @@ import android.media.MediaRecorder import android.os.Build import android.os.SystemClock import androidx.annotation.RequiresApi -import com.philkes.notallyx.presentation.view.misc.BetterLiveData +import com.philkes.notallyx.presentation.view.misc.NotNullLiveData import com.philkes.notallyx.utils.IO.getTempAudioFile import com.philkes.notallyx.utils.audio.Status.PAUSED import com.philkes.notallyx.utils.audio.Status.READY @@ -15,7 +15,7 @@ import com.philkes.notallyx.utils.audio.Status.RECORDING @RequiresApi(24) class AudioRecordService : Service() { - var status = BetterLiveData(READY) + var status = NotNullLiveData(READY) private var lastStart = 0L private var audioDuration = 0L diff --git a/app/src/main/java/com/philkes/notallyx/utils/backup/AutoBackupWorker.kt b/app/src/main/java/com/philkes/notallyx/utils/backup/AutoBackupWorker.kt index 2cc35c7a..ec8e1c70 100644 --- a/app/src/main/java/com/philkes/notallyx/utils/backup/AutoBackupWorker.kt +++ b/app/src/main/java/com/philkes/notallyx/utils/backup/AutoBackupWorker.kt @@ -6,8 +6,8 @@ import android.net.Uri import androidx.documentfile.provider.DocumentFile import androidx.work.Worker import androidx.work.WorkerParameters -import com.philkes.notallyx.Preferences -import com.philkes.notallyx.presentation.view.misc.AutoBackup +import com.philkes.notallyx.presentation.viewmodel.preference.Constants.BACKUP_PATH_EMPTY +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import com.philkes.notallyx.utils.backup.Export.exportAsZip import java.text.SimpleDateFormat import java.util.Locale @@ -17,11 +17,11 @@ class AutoBackupWorker(private val context: Context, params: WorkerParameters) : override fun doWork(): Result { val app = context.applicationContext as Application - val preferences = Preferences.getInstance(app) + val preferences = NotallyXPreferences.getInstance(app) val backupPath = preferences.autoBackupPath.value - val maxBackups = preferences.autoBackupMax + val maxBackups = preferences.autoBackupMax.value - if (backupPath != AutoBackup.emptyPath) { + if (backupPath != BACKUP_PATH_EMPTY) { val uri = Uri.parse(backupPath) val folder = requireNotNull(DocumentFile.fromTreeUri(app, uri)) diff --git a/app/src/main/java/com/philkes/notallyx/utils/backup/Export.kt b/app/src/main/java/com/philkes/notallyx/utils/backup/Export.kt index 2b234d2f..ee4a3b3c 100644 --- a/app/src/main/java/com/philkes/notallyx/utils/backup/Export.kt +++ b/app/src/main/java/com/philkes/notallyx/utils/backup/Export.kt @@ -8,13 +8,13 @@ import androidx.lifecycle.MutableLiveData import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager -import com.philkes.notallyx.Preferences import com.philkes.notallyx.data.NotallyDatabase import com.philkes.notallyx.data.model.Converters import com.philkes.notallyx.data.model.FileAttachment -import com.philkes.notallyx.presentation.view.misc.BackupPassword.emptyPassword -import com.philkes.notallyx.presentation.view.misc.BiometricLock.enabled import com.philkes.notallyx.presentation.view.misc.Progress +import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock +import com.philkes.notallyx.presentation.viewmodel.preference.Constants.PASSWORD_EMPTY +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import com.philkes.notallyx.utils.IO.SUBFOLDER_AUDIOS import com.philkes.notallyx.utils.IO.SUBFOLDER_FILES import com.philkes.notallyx.utils.IO.SUBFOLDER_IMAGES @@ -43,28 +43,28 @@ object Export { backupProgress?.postValue(Progress(indeterminate = true)) val database = NotallyDatabase.getDatabase(app).value database.checkpoint() - val preferences = Preferences.getInstance(app) + val preferences = NotallyXPreferences.getInstance(app) val backupPassword = preferences.backupPassword.value val tempFile = File.createTempFile("export", "tmp", app.cacheDir) val zipFile = ZipFile( tempFile, - if (backupPassword != emptyPassword) backupPassword.toCharArray() else null, + if (backupPassword != PASSWORD_EMPTY) backupPassword.toCharArray() else null, ) val zipParameters = ZipParameters().apply { - isEncryptFiles = backupPassword != emptyPassword + isEncryptFiles = backupPassword != PASSWORD_EMPTY compressionLevel = CompressionLevel.NO_COMPRESSION encryptionMethod = EncryptionMethod.AES } if ( - preferences.biometricLock.value == enabled && + preferences.biometricLock.value == BiometricLock.ENABLED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) { - val cipher = getInitializedCipherForDecryption(iv = preferences.iv!!) - val passphrase = cipher.doFinal(preferences.getDatabasePassphrase()) + val cipher = getInitializedCipherForDecryption(iv = preferences.iv.value!!) + val passphrase = cipher.doFinal(preferences.databaseEncryptionKey.value) val decryptedFile = File.createTempFile("decrypted", "tmp", app.cacheDir) decryptDatabase(app, passphrase, decryptedFile) zipFile.addFile(decryptedFile, zipParameters.copy(NotallyDatabase.DatabaseName)) diff --git a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerAddDeleteTest.kt b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerAddDeleteTest.kt index 721dd891..2c1ffe53 100644 --- a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerAddDeleteTest.kt +++ b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerAddDeleteTest.kt @@ -1,7 +1,7 @@ package com.philkes.notallyx.recyclerview.listmanager import com.philkes.notallyx.data.model.ListItem -import com.philkes.notallyx.presentation.view.misc.ListItemSorting +import com.philkes.notallyx.presentation.viewmodel.preference.ListItemSort import com.philkes.notallyx.test.assert import com.philkes.notallyx.test.assertChildren import com.philkes.notallyx.test.assertOrder @@ -21,7 +21,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { // region add @Test fun `add item at default position`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) val item = createListItem("Test") val newItem = item.clone() as ListItem listManager.add(item = item) @@ -33,7 +33,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `add default item before child item`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true) listManager.add(1) @@ -45,7 +45,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `add default item after child item`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true) listManager.add(2) @@ -57,7 +57,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `add checked item with correct order`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) val itemToAdd = ListItem("Test", true, false, null, mutableListOf()) val newItem = itemToAdd.clone() as ListItem listManager.add(0, item = itemToAdd) @@ -71,7 +71,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `add checked item with correct order when auto-sort enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) val itemToAdd = ListItem("Test", true, false, null, mutableListOf()) val newItem = itemToAdd.clone() as ListItem listManager.add(position = 0, item = itemToAdd) @@ -85,7 +85,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `add item with children`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) val childItem1 = ListItem("Child1", false, true, null, mutableListOf()) val childItem2 = ListItem("Child2", false, true, null, mutableListOf()) val parentItem = @@ -114,7 +114,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `delete first item from list unforced does not delete`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) val deletedItem = listManager.delete(position = 0, force = false, allowFocusChange = false) assertNull(deletedItem) @@ -124,7 +124,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `delete first item from list forced`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) val deletedItem = listManager.delete(position = 0, force = true) assertEquals("A", deletedItem!!.body) @@ -134,7 +134,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `delete item with children from list`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true) val deletedItem = listManager.delete(position = 0, force = true)!! @@ -147,7 +147,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `delete at invalid position`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) val deletedItem = listManager.delete(10) assertNull(deletedItem) @@ -160,7 +160,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `delete checked items`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true) listManager.changeChecked(2, true) listManager.changeChecked(0, true) @@ -174,7 +174,7 @@ class ListManagerAddDeleteTest : ListManagerTestBase() { @Test fun `delete checked items with auto-sort enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(3, true) listManager.changeChecked(2, true) listManager.changeChecked(0, true) diff --git a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerCheckedTest.kt b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerCheckedTest.kt index b7ee501e..ac3b8524 100644 --- a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerCheckedTest.kt +++ b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerCheckedTest.kt @@ -1,6 +1,6 @@ package com.philkes.notallyx.recyclerview.listmanager -import com.philkes.notallyx.presentation.view.misc.ListItemSorting +import com.philkes.notallyx.presentation.viewmodel.preference.ListItemSort import com.philkes.notallyx.test.assert import com.philkes.notallyx.test.assertChecked import com.philkes.notallyx.test.assertOrder @@ -14,7 +14,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeChecked checks item`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeChecked(0, checked = true, pushChange = true) "A".assertIsChecked() @@ -24,7 +24,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeChecked unchecks item and updates order`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeChecked(0, true) listManager.changeChecked(0, checked = false, pushChange = true) @@ -35,7 +35,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeChecked does nothing when state is unchanged`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeChecked(0, true) listManager.changeChecked(0, checked = true, pushChange = true) @@ -46,7 +46,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeChecked on child item`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true) listManager.changeChecked(1, checked = true, pushChange = true) @@ -61,7 +61,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeChecked on parent item checks all children`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true) listManager.changeIsChild(2, true) @@ -79,7 +79,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeChecked on child item and does not move when auto-sort is enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(1, true) listManager.changeChecked(1, checked = true, pushChange = true) @@ -94,7 +94,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeChecked true on parent item checks all children and moves when auto-sort is enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(1, true) listManager.changeIsChild(2, true) @@ -115,7 +115,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeChecked false on parent item unchecks all children and moves when auto-sort is enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(1, true) listManager.changeIsChild(2, true) listManager.changeChecked(0, checked = true, pushChange = true) @@ -137,7 +137,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeChecked false on parent item after add another item when auto-sort is enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(1, true) listManager.changeIsChild(2, true) listManager.changeChecked(0, checked = true, pushChange = true) @@ -155,7 +155,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeChecked false on child item also unchecks parent`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(2, true) listManager.changeIsChild(3, true) listManager.changeChecked(1, checked = true, pushChange = true) @@ -176,7 +176,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeCheckedForAll true checks all unchecked items`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeChecked(1, true) listManager.changeChecked(3, true) @@ -189,7 +189,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeCheckedForAll false unchecks all unchecked items`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(2, true) listManager.changeChecked(1, true) listManager.changeChecked(3, true) @@ -203,7 +203,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeCheckedForAll true correct order with auto-sort enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(3, true) listManager.changeChecked(2, true) listManager.changeChecked(0, true) @@ -217,7 +217,7 @@ class ListManagerCheckedTest : ListManagerTestBase() { @Test fun `changeCheckedForAll false correct order with auto-sort enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(3, true) listManager.changeChecked(2, true) listManager.changeChecked(0, true) diff --git a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerIsChildTest.kt b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerIsChildTest.kt index 5ebee05d..c6a1ad96 100644 --- a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerIsChildTest.kt +++ b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerIsChildTest.kt @@ -1,6 +1,6 @@ package com.philkes.notallyx.recyclerview.listmanager -import com.philkes.notallyx.presentation.view.misc.ListItemSorting +import com.philkes.notallyx.presentation.viewmodel.preference.ListItemSort import com.philkes.notallyx.test.assert import com.philkes.notallyx.test.assertOrder import com.philkes.notallyx.utils.changehistory.ListIsChildChange @@ -12,7 +12,7 @@ class ListManagerIsChildTest : ListManagerTestBase() { @Test fun `changeIsChild changes parent to child and pushes change`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, isChild = true) items.assertOrder("A", "B") @@ -22,7 +22,7 @@ class ListManagerIsChildTest : ListManagerTestBase() { @Test fun `changeIsChild changes child to parent`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, isChild = true) listManager.changeIsChild(1, isChild = false) @@ -34,7 +34,7 @@ class ListManagerIsChildTest : ListManagerTestBase() { @Test fun `changeIsChild adds all child items when item becomes a parent`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true) listManager.changeIsChild(2, true) diff --git a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerMoveTest.kt b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerMoveTest.kt index 3aeb097b..d28d9661 100644 --- a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerMoveTest.kt +++ b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerMoveTest.kt @@ -1,7 +1,7 @@ package com.philkes.notallyx.recyclerview.listmanager -import com.philkes.notallyx.presentation.view.misc.ListItemSorting import com.philkes.notallyx.presentation.view.note.listitem.sorting.lastIndex +import com.philkes.notallyx.presentation.viewmodel.preference.ListItemSort import com.philkes.notallyx.test.assert import com.philkes.notallyx.test.assertOrder import com.philkes.notallyx.test.createListItem @@ -18,7 +18,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `move parent without children`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) val newPosition = listManager.move(3, 1) items.assertOrder("A", "D", "B") @@ -28,7 +28,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `move parent with children into other parent`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true, false) listManager.changeIsChild(2, true, false) listManager.changeIsChild(4, true, false) @@ -46,7 +46,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `move parent with children to bottom`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true, false) listManager.changeIsChild(2, true, false) items.printList("Before") @@ -63,7 +63,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `move parent with children to top`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true, false) listManager.changeIsChild(4, true, false) listManager.add(0, createListItem("G")) @@ -81,7 +81,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `move child to other parent`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true, false) val newPosition = listManager.move(3, 1) @@ -94,7 +94,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `move child above other child`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true, false) listManager.changeIsChild(4, true, false) listManager.changeIsChild(5, true, false) @@ -109,7 +109,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `move child to top`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true, false) val newPosition = listManager.move(3, 0) @@ -122,7 +122,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `dont move parent into own children`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true) listManager.changeIsChild(4, true) @@ -135,7 +135,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `dont move parent under checked item if auto-sort enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeChecked(5, true) val newPosition = listManager.move(2, 5) @@ -150,7 +150,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `undoMove parent without children`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) val newPosition = listManager.move(1, 4)!! val itemBeforeMove = (changeHistory.lookUp() as ListMoveChange).itemBeforeMove @@ -161,7 +161,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `undoMove parent with children into other parent`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(2, true, false) val newPosition = listManager.move(1, 3)!! val itemBeforeMove = (changeHistory.lookUp() as ListMoveChange).itemBeforeMove @@ -175,7 +175,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `undoMove move parent with children to bottom`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true, false) listManager.changeIsChild(2, true, false) val newPosition = listManager.move(0, items.lastIndex)!! @@ -191,7 +191,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `undoMove parent with children to top`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true, false) listManager.changeIsChild(4, true, false) items.printList("Before") @@ -208,7 +208,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `undoMove child to other parent`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true, false) val newPosition = listManager.move(3, 1)!! val itemBeforeMove = (changeHistory.lookUp() as ListMoveChange).itemBeforeMove @@ -222,7 +222,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `undoMove child above other child`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true, false) listManager.changeIsChild(4, true, false) listManager.changeIsChild(5, true, false) @@ -239,7 +239,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `undoMove child to top`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true, false) val newPosition = listManager.move(3, 0)!! val itemBeforeMove = (changeHistory.lookUp() as ListMoveChange).itemBeforeMove @@ -256,7 +256,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `endDrag parent without children`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listItemDragCallback.simulateDrag(3, 1, "D".itemCount) items.assertOrder("A", "D", "B") @@ -265,7 +265,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `endDrag parent with children into other parent`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true, false) listManager.changeIsChild(2, true, false) listManager.changeIsChild(5, true, false) @@ -300,7 +300,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `endDrag parent with children to bottom`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true, false) listManager.changeIsChild(2, true, false) items.printList("Before") @@ -316,7 +316,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `endDrag parent with children to top`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true, false) listManager.changeIsChild(4, true, false) items.printList("Before") @@ -332,7 +332,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `endDrag child to other parent`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true, false) listItemDragCallback.simulateDrag(3, 1, "D".itemCount) @@ -344,7 +344,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `endDrag child above other child`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true, false) listManager.changeIsChild(4, true, false) listManager.changeIsChild(5, true, false) @@ -358,7 +358,7 @@ class ListManagerMoveTest : ListManagerTestBase() { @Test fun `endDrag child to top`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(3, true, false) listItemDragCallback.simulateDrag(3, 0, "D".itemCount) diff --git a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerTestBase.kt b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerTestBase.kt index 755d6358..f9eb1560 100644 --- a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerTestBase.kt +++ b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerTestBase.kt @@ -3,10 +3,7 @@ package com.philkes.notallyx.recyclerview.listmanager import android.view.inputmethod.InputMethodManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SortedList -import com.philkes.notallyx.Preferences import com.philkes.notallyx.data.model.ListItem -import com.philkes.notallyx.presentation.view.misc.BetterLiveData -import com.philkes.notallyx.presentation.view.misc.ListItemSorting import com.philkes.notallyx.presentation.view.note.listitem.ListItemDragCallback import com.philkes.notallyx.presentation.view.note.listitem.ListItemVH import com.philkes.notallyx.presentation.view.note.listitem.ListManager @@ -15,6 +12,9 @@ import com.philkes.notallyx.presentation.view.note.listitem.sorting.ListItemSort import com.philkes.notallyx.presentation.view.note.listitem.sorting.ListItemSortedList import com.philkes.notallyx.presentation.view.note.listitem.sorting.find import com.philkes.notallyx.presentation.view.note.listitem.sorting.indexOfFirst +import com.philkes.notallyx.presentation.viewmodel.preference.EnumPreference +import com.philkes.notallyx.presentation.viewmodel.preference.ListItemSort +import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import com.philkes.notallyx.test.assertChildren import com.philkes.notallyx.test.createListItem import com.philkes.notallyx.test.mockAndroidLog @@ -34,7 +34,7 @@ open class ListManagerTestBase { protected lateinit var inputMethodManager: InputMethodManager protected lateinit var changeHistory: ChangeHistory protected lateinit var listItemVH: ListItemVH - protected lateinit var preferences: Preferences + protected lateinit var preferences: NotallyXPreferences protected lateinit var listItemDragCallback: ListItemDragCallback protected lateinit var items: ListItemSortedList @@ -49,17 +49,17 @@ open class ListManagerTestBase { inputMethodManager = mock(InputMethodManager::class.java) changeHistory = ChangeHistory() {} listItemVH = mock(ListItemVH::class.java) - preferences = mock(Preferences::class.java) + preferences = mock(NotallyXPreferences::class.java) listManager = ListManager(recyclerView, changeHistory, preferences, inputMethodManager) listManager.adapter = adapter as RecyclerView.Adapter // Prepare view holder `when`(recyclerView.findViewHolderForAdapterPosition(anyInt())).thenReturn(listItemVH) } - protected fun setSorting(sorting: String) { + protected fun setSorting(sorting: ListItemSort) { val sortCallback = when (sorting) { - ListItemSorting.autoSortByChecked -> ListItemSortedByCheckedCallback(adapter) + ListItemSort.AUTO_SORT_BY_CHECKED -> ListItemSortedByCheckedCallback(adapter) else -> ListItemNoSortCallback(adapter) } items = ListItemSortedList(sortCallback) @@ -76,7 +76,10 @@ open class ListManagerTestBase { ) listManager.initList(items) listItemDragCallback = ListItemDragCallback(1.0f, listManager) - `when`(preferences.listItemSorting).thenReturn(BetterLiveData(sorting)) + val listItemSortingPreference = mock(EnumPreference::class.java) + `when`(listItemSortingPreference.value).thenReturn(sorting) + `when`(preferences.listItemSorting) + .thenReturn(listItemSortingPreference as EnumPreference) } protected operator fun List.get(body: String): ListItem { diff --git a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerWithChangeHistoryTest.kt b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerWithChangeHistoryTest.kt index 4b126353..19e3b508 100644 --- a/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerWithChangeHistoryTest.kt +++ b/app/src/test/kotlin/com/philkes/notallyx/recyclerview/listmanager/ListManagerWithChangeHistoryTest.kt @@ -1,8 +1,8 @@ package com.philkes.notallyx.recyclerview.listmanager -import com.philkes.notallyx.presentation.view.misc.ListItemSorting import com.philkes.notallyx.presentation.view.note.listitem.sorting.lastIndex import com.philkes.notallyx.presentation.view.note.listitem.sorting.map +import com.philkes.notallyx.presentation.viewmodel.preference.ListItemSort import com.philkes.notallyx.test.assertChecked import com.philkes.notallyx.test.assertIds import com.philkes.notallyx.test.assertOrder @@ -12,7 +12,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo moves`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.move(0, 4) listManager.move(2, 3) listManager.move(4, 1) @@ -34,7 +34,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo changeChecked`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeChecked(0, true) listManager.changeChecked(3, true) listManager.changeChecked(0, false) @@ -55,7 +55,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo changeChecked if auto-sort enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeChecked(0, true) listManager.changeChecked(3, true) listManager.changeChecked(0, false) @@ -79,7 +79,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo changeChecked false on child item`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(2, true) listManager.changeIsChild(3, true) listManager.changeChecked(1, checked = true, pushChange = true) @@ -96,7 +96,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo changeIsChild`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true) listManager.changeIsChild(2, true) listManager.changeIsChild(4, true) @@ -120,7 +120,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo add parents with children`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.addWithChildren(0, "Parent1", "Child1") listManager.addWithChildren(4, "Parent2") listManager.addWithChildren(0, "Parent3") @@ -150,7 +150,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo delete parents with children`() { - setSorting(ListItemSorting.noAutoSort) + setSorting(ListItemSort.NO_AUTO_SORT) listManager.changeIsChild(1, true) listManager.changeIsChild(3, true) listManager.changeIsChild(4, true) @@ -174,7 +174,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo check all with auto-sort enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(3, true) listManager.changeChecked(2, true) listManager.changeChecked(0, true) @@ -191,7 +191,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo uncheck all with auto-sort enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(3, true) listManager.changeChecked(2, true) listManager.changeChecked(0, true) @@ -208,7 +208,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo delete checked with auto-sort enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(3, true) listManager.changeChecked(2, true) listManager.changeChecked(0, true) @@ -225,7 +225,7 @@ class ListManagerWithChangeHistoryTest : ListManagerTestBase() { @Test fun `undo and redo various changes with auto-sort enabled`() { - setSorting(ListItemSorting.autoSortByChecked) + setSorting(ListItemSort.AUTO_SORT_BY_CHECKED) listManager.changeIsChild(1, true) listManager.changeIsChild(3, true) listManager.changeIsChild(4, true)