diff --git a/app/build.gradle b/app/build.gradle index aac92854..f5ed10c3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,10 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' -apply plugin: 'kotlin-kapt' +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-android-extensions' + id 'kotlin-kapt' + id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" +} def keystorePropertiesFile = rootProject.file("keystore.properties") def keystoreProperties = new Properties() @@ -10,7 +13,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdk 33 defaultConfig { applicationId "com.simplemobiletools.notes.pro" @@ -70,4 +73,6 @@ dependencies { kapt 'androidx.room:room-compiler:2.5.1' implementation 'androidx.room:room-runtime:2.5.1' annotationProcessor 'androidx.room:room-compiler:2.5.1' + + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f27914fe..eb348cee 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,3 +1,27 @@ -keep class com.simplemobiletools.notes.pro.models.* { ; } + +# Keep `Companion` object fields of serializable classes. +# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} + +# Keep `serializer()` on companion objects (both default and named) of serializable classes. +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} + +# Keep `INSTANCE.serializer()` of serializable objects. +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/MainActivity.kt index ad84f6c6..fd60a072 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/MainActivity.kt @@ -43,13 +43,11 @@ import com.simplemobiletools.notes.pro.dialogs.* import com.simplemobiletools.notes.pro.extensions.* import com.simplemobiletools.notes.pro.fragments.TextFragment import com.simplemobiletools.notes.pro.helpers.* -import com.simplemobiletools.notes.pro.helpers.NotesImporter.ImportResult import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.item_checklist.* import java.io.File -import java.io.FileOutputStream -import java.io.OutputStream import java.nio.charset.Charset import java.util.* @@ -63,11 +61,8 @@ class MainActivity : SimpleActivity() { private val PICK_OPEN_FILE_INTENT = 1 private val PICK_EXPORT_FILE_INTENT = 2 - private val PICK_IMPORT_NOTES_INTENT = 3 - private val PICK_EXPORT_NOTES_INTENT = 4 - private lateinit var mCurrentNote: Note - private var mNotes = ArrayList() + private var mNotes = listOf() private var mAdapter: NotesPagerAdapter? = null private var noteViewWithTextSelected: MyEditText? = null private var saveNoteButton: MenuItem? = null @@ -132,6 +127,12 @@ class MainActivity : SimpleActivity() { initViewPager() } + NotesHelper(this).getNotes { lastestNotes -> + if (mNotes.size != lastestNotes.size) { + initViewPager() + } + } + refreshMenuItems() pager_tab_strip.apply { setTextSize(TypedValue.COMPLEX_UNIT_PX, getPercentageFontSize()) @@ -171,31 +172,29 @@ class MainActivity : SimpleActivity() { main_toolbar.menu.apply { findItem(R.id.undo).apply { - isVisible = showUndoButton && mCurrentNote.type == NoteType.TYPE_TEXT.value + isVisible = showUndoButton && mCurrentNote.type == NoteType.TYPE_TEXT icon?.alpha = if (isEnabled) 255 else 127 } findItem(R.id.redo).apply { - isVisible = showRedoButton && mCurrentNote.type == NoteType.TYPE_TEXT.value + isVisible = showRedoButton && mCurrentNote.type == NoteType.TYPE_TEXT icon?.alpha = if (isEnabled) 255 else 127 } findItem(R.id.rename_note).isVisible = multipleNotesExist findItem(R.id.open_note).isVisible = multipleNotesExist findItem(R.id.delete_note).isVisible = multipleNotesExist - findItem(R.id.export_all_notes).isVisible = multipleNotesExist findItem(R.id.open_search).isVisible = !isCurrentItemChecklist findItem(R.id.remove_done_items).isVisible = isCurrentItemChecklist findItem(R.id.sort_checklist).isVisible = isCurrentItemChecklist findItem(R.id.import_folder).isVisible = !isQPlus() - findItem(R.id.import_notes).isVisible = isQPlus() findItem(R.id.lock_note).isVisible = mNotes.isNotEmpty() && (::mCurrentNote.isInitialized && !mCurrentNote.isLocked()) findItem(R.id.unlock_note).isVisible = mNotes.isNotEmpty() && (::mCurrentNote.isInitialized && mCurrentNote.isLocked()) findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(R.bool.hide_google_relations) saveNoteButton = findItem(R.id.save_note) saveNoteButton!!.isVisible = - !config.autosaveNotes && showSaveButton && (::mCurrentNote.isInitialized && mCurrentNote.type == NoteType.TYPE_TEXT.value) + !config.autosaveNotes && showSaveButton && (::mCurrentNote.isInitialized && mCurrentNote.type == NoteType.TYPE_TEXT) } pager_tab_strip.beVisibleIf(multipleNotesExist) @@ -223,8 +222,6 @@ class MainActivity : SimpleActivity() { R.id.open_file -> tryOpenFile() R.id.import_folder -> openFolder() R.id.export_as_file -> fragment?.handleUnlocking { tryExportAsFile() } - R.id.export_all_notes -> tryExportNotes() - R.id.import_notes -> tryImportNotes() R.id.print -> fragment?.handleUnlocking { printText() } R.id.delete_note -> fragment?.handleUnlocking { displayDeleteNotePrompt() } R.id.more_apps_from_us -> launchMoreAppsFromUsIntent() @@ -288,15 +285,10 @@ class MainActivity : SimpleActivity() { val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION applicationContext.contentResolver.takePersistableUriPermission(resultData.data!!, takeFlags) showExportFilePickUpdateDialog(resultData.dataString!!, getCurrentNoteValue()) - } else if (requestCode == PICK_EXPORT_NOTES_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) { - val outputStream = contentResolver.openOutputStream(resultData.data!!) - exportNotesTo(outputStream) - } else if (requestCode == PICK_IMPORT_NOTES_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) { - tryImportingAsJson(resultData.data!!) } } - private fun isCurrentItemChecklist() = if (::mCurrentNote.isInitialized) mCurrentNote.type == NoteType.TYPE_CHECKLIST.value else false + private fun isCurrentItemChecklist() = if (::mCurrentNote.isInitialized) mCurrentNote.type == NoteType.TYPE_CHECKLIST else false @SuppressLint("NewApi") private fun checkShortcuts() { @@ -368,10 +360,10 @@ class MainActivity : SimpleActivity() { val file = File(realPath) handleUri(Uri.fromFile(file)) } else if (intent.getBooleanExtra(NEW_TEXT_NOTE, false)) { - val newTextNote = Note(null, getCurrentFormattedDateTime(), "", NoteType.TYPE_TEXT.value, "", PROTECTION_NONE, "") + val newTextNote = Note(null, getCurrentFormattedDateTime(), "", NoteType.TYPE_TEXT, "", PROTECTION_NONE, "") addNewNote(newTextNote) } else if (intent.getBooleanExtra(NEW_CHECKLIST, false)) { - val newChecklist = Note(null, getCurrentFormattedDateTime(), "", NoteType.TYPE_CHECKLIST.value, "", PROTECTION_NONE, "") + val newChecklist = Note(null, getCurrentFormattedDateTime(), "", NoteType.TYPE_CHECKLIST, "", PROTECTION_NONE, "") addNewNote(newChecklist) } else { handleUri(data!!) @@ -446,7 +438,7 @@ class MainActivity : SimpleActivity() { } } - if (!config.showKeyboard || mCurrentNote.type == NoteType.TYPE_CHECKLIST.value) { + if (!config.showKeyboard || mCurrentNote.type == NoteType.TYPE_CHECKLIST) { hideKeyboard() } refreshMenuItems() @@ -678,7 +670,7 @@ class MainActivity : SimpleActivity() { val checklistItems = fileText.parseChecklistItems() if (checklistItems != null) { val title = it.absolutePath.getFilenameFromPath().substringBeforeLast('.') - val note = Note(null, title, fileText, NoteType.TYPE_CHECKLIST.value, "", PROTECTION_NONE, "") + val note = Note(null, title, fileText, NoteType.TYPE_CHECKLIST, "", PROTECTION_NONE, "") runOnUiThread { OpenFileDialog(this, it.path) { displayNewNoteDialog(note.value, title = it.title, it.path, setChecklistAsDefault = true) @@ -732,29 +724,23 @@ class MainActivity : SimpleActivity() { } private fun importUri(uri: Uri) { - tryImportingAsJson(uri, force = true, showToasts = false) { success -> - if (success) { - return@tryImportingAsJson - } - - when (uri.scheme) { - "file" -> openPath(uri.path!!) - "content" -> { - val realPath = getRealPathFromURI(uri) - if (hasPermission(PERMISSION_READ_STORAGE)) { - if (realPath != null) { - openPath(realPath) - } else { - R.string.unknown_error_occurred - } - } else if (realPath != null && realPath != "") { - checkFile(realPath, false) { - addNoteFromUri(uri, realPath.getFilenameFromPath()) - } + when (uri.scheme) { + "file" -> openPath(uri.path!!) + "content" -> { + val realPath = getRealPathFromURI(uri) + if (hasPermission(PERMISSION_READ_STORAGE)) { + if (realPath != null) { + openPath(realPath) } else { - checkUri(uri) { - addNoteFromUri(uri) - } + R.string.unknown_error_occurred + } + } else if (realPath != null && realPath != "") { + checkFile(realPath, false) { + addNoteFromUri(uri, realPath.getFilenameFromPath()) + } + } else { + checkUri(uri) { + addNoteFromUri(uri) } } } @@ -786,7 +772,7 @@ class MainActivity : SimpleActivity() { } } - val noteType = if (checklistItems != null) NoteType.TYPE_CHECKLIST.value else NoteType.TYPE_TEXT.value + val noteType = if (checklistItems != null) NoteType.TYPE_CHECKLIST else NoteType.TYPE_TEXT if (!canSyncNoteWithFile) { val note = Note(null, noteTitle, content, noteType, "", PROTECTION_NONE, "") displayNewNoteDialog(note.value, title = noteTitle, "") @@ -812,9 +798,9 @@ class MainActivity : SimpleActivity() { val fileText = it.readText().trim() val checklistItems = fileText.parseChecklistItems() val note = if (checklistItems != null) { - Note(null, title.substringBeforeLast('.'), fileText, NoteType.TYPE_CHECKLIST.value, "", PROTECTION_NONE, "") + Note(null, title.substringBeforeLast('.'), fileText, NoteType.TYPE_CHECKLIST, "", PROTECTION_NONE, "") } else { - Note(null, title, "", NoteType.TYPE_TEXT.value, path, PROTECTION_NONE, "") + Note(null, title, "", NoteType.TYPE_TEXT, path, PROTECTION_NONE, "") } if (mNotes.any { it.title.equals(note.title, true) }) { @@ -885,10 +871,10 @@ class MainActivity : SimpleActivity() { private fun exportAsFile() { ExportFileDialog(this, mCurrentNote) { - val textToExport = if (mCurrentNote.type == NoteType.TYPE_TEXT.value) getCurrentNoteText() else mCurrentNote.value + val textToExport = if (mCurrentNote.type == NoteType.TYPE_TEXT) getCurrentNoteText() else mCurrentNote.value if (textToExport == null || textToExport.isEmpty()) { toast(R.string.unknown_error_occurred) - } else if (mCurrentNote.type == NoteType.TYPE_TEXT.value) { + } else if (mCurrentNote.type == NoteType.TYPE_TEXT) { showExportFilePickUpdateDialog(it, textToExport) } else { tryExportNoteValueToFile(it, mCurrentNote.title, textToExport, true) @@ -896,138 +882,6 @@ class MainActivity : SimpleActivity() { } } - private fun tryExportNotes() { - if (isQPlus()) { - hideKeyboard() - val fileName = "${getString(R.string.notes)}_${getCurrentFormattedDateTime()}" - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - type = EXPORT_MIME_TYPE - putExtra(Intent.EXTRA_TITLE, fileName) - addCategory(Intent.CATEGORY_OPENABLE) - - try { - startActivityForResult(this, PICK_EXPORT_NOTES_INTENT) - } catch (e: ActivityNotFoundException) { - toast(R.string.system_service_disabled, Toast.LENGTH_LONG) - } catch (e: Exception) { - showErrorToast(e) - } - } - } else { - tryExportAllNotesBelowQ() - } - } - - private fun requestUnlockNotes(callback: (unlockedNoteIds: List) -> Unit) { - val lockedNotes = mNotes.filter { it.isLocked() } - if (lockedNotes.isNotEmpty()) { - runOnUiThread { - UnlockNotesDialog(this, lockedNotes, callback) - } - } else { - callback(emptyList()) - } - } - - private fun exportNotesTo(outputStream: OutputStream?) { - ensureBackgroundThread { - NotesHelper(this).getNotes { - mNotes = it - requestUnlockNotes { unlockedNoteIds -> - toast(R.string.exporting) - val notesExporter = NotesExporter(this) - notesExporter.exportNotes(mNotes, unlockedNoteIds, outputStream) { result -> - val toastId = when (result) { - NotesExporter.ExportResult.EXPORT_OK -> R.string.exporting_successful - else -> R.string.exporting_failed - } - - toast(toastId) - } - } - } - } - } - - private fun tryImportNotes() { - hideKeyboard() - Intent(Intent.ACTION_GET_CONTENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = EXPORT_MIME_TYPE - - try { - startActivityForResult(this, PICK_IMPORT_NOTES_INTENT) - } catch (e: ActivityNotFoundException) { - toast(R.string.system_service_disabled, Toast.LENGTH_LONG) - } catch (e: Exception) { - showErrorToast(e) - } - } - } - - private fun tryImportingAsJson(uri: Uri, force: Boolean = false, showToasts: Boolean = true, callback: ((success: Boolean) -> Unit)? = null) { - val path: String - val filename: String - when (uri.scheme) { - "file" -> { - path = uri.path!! - filename = path.getFilenameFromPath() - } - "content" -> { - val tempFile = getTempFile("messages", "backup.txt") - if (tempFile == null) { - maybeToast(R.string.unknown_error_occurred, showToasts) - callback?.invoke(false) - return - } - - try { - filename = getFilenameFromUri(uri) - val inputStream = contentResolver.openInputStream(uri) - val out = FileOutputStream(tempFile) - inputStream!!.copyTo(out) - path = tempFile.absolutePath - } catch (e: Exception) { - showErrorToast(e) - callback?.invoke(false) - return - } - } - else -> { - maybeToast(R.string.invalid_file_format, showToasts) - callback?.invoke(false) - return - } - } - - maybeToast(R.string.importing, showToasts) - ensureBackgroundThread { - NotesImporter(this).importNotes(path, filename, force) { importResult -> - if (importResult == ImportResult.IMPORT_FAIL) { - maybeToast(R.string.no_new_items, showToasts) - runOnUiThread { callback?.invoke(false) } - return@importNotes - } - - toast( - when (importResult) { - ImportResult.IMPORT_OK -> R.string.importing_successful - ImportResult.IMPORT_PARTIAL -> R.string.importing_some_entries_failed - else -> R.string.no_new_items - } - ) - initViewPager() - runOnUiThread { callback?.invoke(true) } - } - } - } - - private fun maybeToast(id: Int, show: Boolean) { - if (show) { - toast(id) - } - } - private fun showExportFilePickUpdateDialog(exportPath: String, textToExport: String) { val items = arrayListOf( RadioItem(EXPORT_FILE_SYNC, getString(R.string.update_file_at_note)), @@ -1053,73 +907,6 @@ class MainActivity : SimpleActivity() { } } - private fun tryExportAllNotesBelowQ() { - handlePermission(PERMISSION_WRITE_STORAGE) { - if (it) { - exportAllNotesBelowQ() - } else { - toast(R.string.no_storage_permissions) - } - } - } - - private fun exportAllNotesBelowQ() { - ensureBackgroundThread { - NotesHelper(this).getNotes { notes -> - mNotes = notes - requestUnlockNotes { unlockedNoteIds -> - ExportFilesDialog(this, mNotes) { parent, extension -> - val items = arrayListOf( - RadioItem(EXPORT_FILE_SYNC, getString(R.string.update_file_at_note)), - RadioItem(EXPORT_FILE_NO_SYNC, getString(R.string.only_export_file_content)) - ) - - RadioGroupDialog(this, items) { any -> - val syncFile = any as Int == EXPORT_FILE_SYNC - var failCount = 0 - mNotes.filter { !it.isLocked() || it.id in unlockedNoteIds }.forEachIndexed { index, note -> - val filename = if (extension.isEmpty()) note.title else "${note.title}.$extension" - val file = File(parent, filename) - if (!filename.isAValidFilename()) { - toast(String.format(getString(R.string.filename_invalid_characters_placeholder, filename))) - } else { - val noteStoredValue = note.getNoteStoredValue(this) ?: "" - tryExportNoteValueToFile(file.absolutePath, mCurrentNote.title, note.value, false) { exportedSuccessfully -> - if (exportedSuccessfully) { - if (syncFile) { - note.path = file.absolutePath - note.value = "" - } else { - note.path = "" - note.value = noteStoredValue - } - - NotesHelper(this).insertOrUpdateNote(note) - } - - if (mCurrentNote.id == note.id) { - mCurrentNote.value = note.value - mCurrentNote.path = note.path - getPagerAdapter().updateCurrentNoteData(view_pager.currentItem, mCurrentNote.path, mCurrentNote.value) - } - - if (!exportedSuccessfully) { - failCount++ - } - - if (index == mNotes.size - 1) { - toast(if (failCount == 0) R.string.exporting_successful else R.string.exporting_some_entries_failed) - } - } - } - } - } - } - } - } - } - } - fun tryExportNoteValueToFile(path: String, title: String, content: String, showSuccessToasts: Boolean, callback: ((success: Boolean) -> Unit)? = null) { if (path.startsWith("content://")) { exportNoteValueToUri(Uri.parse(path), title, content, showSuccessToasts, callback) @@ -1238,7 +1025,7 @@ class MainActivity : SimpleActivity() { private fun getCurrentNoteText() = getPagerAdapter().getCurrentNoteViewText(view_pager.currentItem) private fun getCurrentNoteValue(): String { - return if (mCurrentNote.type == NoteType.TYPE_TEXT.value) { + return if (mCurrentNote.type == NoteType.TYPE_TEXT) { getCurrentNoteText() ?: "" } else { getPagerAdapter().getNoteChecklistItems(view_pager.currentItem) ?: "" @@ -1246,7 +1033,7 @@ class MainActivity : SimpleActivity() { } private fun getPrintableText(): String { - return if (mCurrentNote.type == NoteType.TYPE_TEXT.value) { + return if (mCurrentNote.type == NoteType.TYPE_TEXT) { getCurrentNoteText() ?: "" } else { var printableText = "" @@ -1261,7 +1048,7 @@ class MainActivity : SimpleActivity() { private fun saveCurrentNote(force: Boolean) { getPagerAdapter().saveCurrentNote(view_pager.currentItem, force) - if (mCurrentNote.type == NoteType.TYPE_CHECKLIST.value) { + if (mCurrentNote.type == NoteType.TYPE_CHECKLIST) { mCurrentNote.value = getPagerAdapter().getNoteChecklistItems(view_pager.currentItem) ?: "" } } @@ -1359,8 +1146,8 @@ class MainActivity : SimpleActivity() { } private fun shareText() { - val text = if (mCurrentNote.type == NoteType.TYPE_TEXT.value) getCurrentNoteText() else mCurrentNote.value - if (text == null || text.isEmpty()) { + val text = if (mCurrentNote.type == NoteType.TYPE_TEXT) getCurrentNoteText() else mCurrentNote.value + if (text.isNullOrEmpty()) { toast(R.string.cannot_share_empty_text) return } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/SettingsActivity.kt index 75079d69..12542efb 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/SettingsActivity.kt @@ -1,25 +1,33 @@ package com.simplemobiletools.notes.pro.activities import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.Menu +import androidx.activity.result.contract.ActivityResultContracts import com.simplemobiletools.commons.dialogs.RadioGroupDialog -import com.simplemobiletools.commons.extensions.beVisibleIf -import com.simplemobiletools.commons.extensions.getProperPrimaryColor -import com.simplemobiletools.commons.extensions.updateTextColors +import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.notes.pro.R +import com.simplemobiletools.notes.pro.dialogs.ExportNotesDialog import com.simplemobiletools.notes.pro.extensions.config +import com.simplemobiletools.notes.pro.extensions.requestUnlockNotes import com.simplemobiletools.notes.pro.extensions.updateWidgets import com.simplemobiletools.notes.pro.extensions.widgetsDB import com.simplemobiletools.notes.pro.helpers.* +import com.simplemobiletools.notes.pro.models.Note import com.simplemobiletools.notes.pro.models.Widget import kotlinx.android.synthetic.main.activity_settings.* -import java.util.* +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.util.Locale import kotlin.system.exitProcess class SettingsActivity : SimpleActivity() { + private val notesFileType = "application/json" override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true @@ -50,6 +58,8 @@ class SettingsActivity : SimpleActivity() { setupCursorPlacement() setupIncognitoMode() setupCustomizeWidgetColors() + setupNotesExport() + setupNotesImport() updateTextColors(settings_nested_scrollview) arrayOf( @@ -57,7 +67,8 @@ class SettingsActivity : SimpleActivity() { settings_general_settings_label, settings_text_label, settings_startup_label, - settings_saving_label + settings_saving_label, + settings_migrating_label, ).forEach { it.setTextColor(getProperPrimaryColor()) } @@ -68,6 +79,26 @@ class SettingsActivity : SimpleActivity() { return super.onCreateOptionsMenu(menu) } + private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + if (uri != null) { + toast(R.string.importing) + importNotes(uri) + } + } + + private val saveDocument = registerForActivityResult(ActivityResultContracts.CreateDocument(notesFileType)) { uri -> + if (uri != null) { + toast(R.string.exporting) + NotesHelper(this).getNotes { notes -> + requestUnlockNotes(notes) { unlockedNotes -> + val notLockedNotes = notes.filterNot { it.isLocked() } + val notesToExport = unlockedNotes + notLockedNotes + exportNotes(notesToExport, uri) + } + } + } + } + private fun setupCustomizeColors() { settings_color_customization_holder.setOnClickListener { startCustomizationActivity() @@ -257,4 +288,63 @@ class SettingsActivity : SimpleActivity() { config.useIncognitoMode = settings_use_incognito_mode.isChecked } } + + private fun setupNotesExport() { + settings_export_notes_holder.setOnClickListener { + ExportNotesDialog(this) { filename -> + saveDocument.launch(filename) + } + } + } + + private fun setupNotesImport() { + settings_import_notes_holder.setOnClickListener { + getContent.launch(notesFileType) + } + } + + private fun exportNotes(notes: List, uri: Uri) { + if (notes.isEmpty()) { + toast(R.string.no_entries_for_exporting) + } else { + try { + val outputStream = contentResolver.openOutputStream(uri)!! + + val jsonString = Json.encodeToString(notes) + outputStream.use { + it.write(jsonString.toByteArray()) + } + toast(R.string.exporting_successful) + } catch (e: Exception) { + showErrorToast(e) + } + } + } + + private fun importNotes(uri: Uri) { + try { + val jsonString = contentResolver.openInputStream(uri)!!.use { inputStream -> + inputStream.bufferedReader().readText() + } + val objects = Json.decodeFromString>(jsonString) + if (objects.isEmpty()) { + toast(R.string.no_entries_for_importing) + return + } + NotesHelper(this).importNotes(this, objects) { importResult -> + when (importResult) { + NotesHelper.ImportResult.IMPORT_OK -> toast(R.string.importing_successful) + NotesHelper.ImportResult.IMPORT_PARTIAL -> toast(R.string.importing_some_entries_failed) + NotesHelper.ImportResult.IMPORT_NOTHING_NEW -> toast(R.string.no_new_items) + else -> toast(R.string.importing_failed) + } + } + } catch (_: SerializationException) { + toast(R.string.invalid_file_format) + } catch (_: IllegalArgumentException) { + toast(R.string.invalid_file_format) + } catch (e: Exception) { + showErrorToast(e) + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/WidgetConfigureActivity.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/WidgetConfigureActivity.kt index 70c9e3d9..4adbc2df 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/WidgetConfigureActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/WidgetConfigureActivity.kt @@ -27,8 +27,11 @@ import com.simplemobiletools.notes.pro.extensions.widgetsDB import com.simplemobiletools.notes.pro.helpers.* import com.simplemobiletools.notes.pro.models.ChecklistItem import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType import com.simplemobiletools.notes.pro.models.Widget import kotlinx.android.synthetic.main.widget_config.* +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json class WidgetConfigureActivity : SimpleActivity() { private var mBgAlpha = 0f @@ -39,7 +42,7 @@ class WidgetConfigureActivity : SimpleActivity() { private var mCurrentNoteId = 0L private var mIsCustomizingColors = false private var mShowTitle = false - private var mNotes = ArrayList() + private var mNotes = listOf() public override fun onCreate(savedInstanceState: Bundle?) { useDynamicTheme = false @@ -156,7 +159,7 @@ class WidgetConfigureActivity : SimpleActivity() { mCurrentNoteId = note.id!! notes_picker_value.text = note.title text_note_view_title.text = note.title - if (note.type == NoteType.TYPE_CHECKLIST.value) { + if (note.type == NoteType.TYPE_CHECKLIST) { val checklistItemType = object : TypeToken>() {}.type val items = Gson().fromJson>(note.value, checklistItemType) ?: ArrayList(1) items.apply { diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/ChecklistAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/ChecklistAdapter.kt index 8eb5f708..9a05e03a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/ChecklistAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/ChecklistAdapter.kt @@ -31,7 +31,7 @@ import kotlinx.android.synthetic.main.item_checklist.view.* import java.util.* class ChecklistAdapter( - activity: BaseSimpleActivity, var items: ArrayList, val listener: ChecklistItemsListener?, + activity: BaseSimpleActivity, var items: MutableList, val listener: ChecklistItemsListener?, recyclerView: MyRecyclerView, val showIcons: Boolean, itemClick: (Any) -> Unit ) : MyRecyclerViewAdapter(activity, recyclerView, itemClick), ItemTouchHelperContract { diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/NotesPagerAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/NotesPagerAdapter.kt index bdf1faaf..909659f1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/NotesPagerAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/NotesPagerAdapter.kt @@ -10,8 +10,8 @@ import com.simplemobiletools.notes.pro.fragments.ChecklistFragment import com.simplemobiletools.notes.pro.fragments.NoteFragment import com.simplemobiletools.notes.pro.fragments.TextFragment import com.simplemobiletools.notes.pro.helpers.NOTE_ID -import com.simplemobiletools.notes.pro.helpers.NoteType import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType class NotesPagerAdapter(fm: FragmentManager, val notes: List, val activity: Activity) : FragmentStatePagerAdapter(fm) { private var fragments: HashMap = LinkedHashMap() @@ -30,7 +30,7 @@ class NotesPagerAdapter(fm: FragmentManager, val notes: List, val activity return fragments[position]!! } - val fragment = if (note.type == NoteType.TYPE_TEXT.value) TextFragment() else ChecklistFragment() + val fragment = if (note.type == NoteType.TYPE_TEXT) TextFragment() else ChecklistFragment() fragment.arguments = bundle fragments[position] = fragment return fragment diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/WidgetAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/WidgetAdapter.kt index b270d307..3f71fd48 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/WidgetAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/WidgetAdapter.kt @@ -20,6 +20,9 @@ import com.simplemobiletools.notes.pro.extensions.notesDB import com.simplemobiletools.notes.pro.helpers.* import com.simplemobiletools.notes.pro.models.ChecklistItem import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json class WidgetAdapter(val context: Context, val intent: Intent) : RemoteViewsService.RemoteViewsFactory { private val textIds = arrayOf( @@ -32,7 +35,7 @@ class WidgetAdapter(val context: Context, val intent: Intent) : RemoteViewsServi ) private var widgetTextColor = DEFAULT_WIDGET_TEXT_COLOR private var note: Note? = null - private var checklistItems = ArrayList() + private var checklistItems = mutableListOf() override fun getViewAt(position: Int): RemoteViews { val noteId = intent.getLongExtra(NOTE_ID, 0L) @@ -43,7 +46,7 @@ class WidgetAdapter(val context: Context, val intent: Intent) : RemoteViewsServi } val textSize = context.getPercentageFontSize() / context.resources.displayMetrics.density - if (note!!.type == NoteType.TYPE_CHECKLIST.value) { + if (note!!.type == NoteType.TYPE_CHECKLIST) { remoteView = RemoteViews(context.packageName, R.layout.item_checklist_widget).apply { val checklistItem = checklistItems.getOrNull(position) ?: return@apply val widgetNewTextColor = if (checklistItem.isDone) widgetTextColor.adjustAlpha(DONE_CHECKLIST_ITEM_ALPHA) else widgetTextColor @@ -123,9 +126,8 @@ class WidgetAdapter(val context: Context, val intent: Intent) : RemoteViewsServi widgetTextColor = intent.getIntExtra(WIDGET_TEXT_COLOR, DEFAULT_WIDGET_TEXT_COLOR) val noteId = intent.getLongExtra(NOTE_ID, 0L) note = context.notesDB.getNoteWithId(noteId) - if (note?.type == NoteType.TYPE_CHECKLIST.value) { - val checklistItemType = object : TypeToken>() {}.type - checklistItems = Gson().fromJson>(note!!.getNoteStoredValue(context), checklistItemType) ?: ArrayList(1) + if (note?.type == NoteType.TYPE_CHECKLIST) { + checklistItems = note!!.getNoteStoredValue(context)?.let { Json.decodeFromString(it) } ?: mutableListOf() // checklist title can be null only because of the glitch in upgrade to 6.6.0, remove this check in the future checklistItems = checklistItems.filter { it.title != null }.toMutableList() as ArrayList @@ -135,7 +137,7 @@ class WidgetAdapter(val context: Context, val intent: Intent) : RemoteViewsServi override fun hasStableIds() = true override fun getCount(): Int { - return if (note?.type == NoteType.TYPE_CHECKLIST.value) { + return if (note?.type == NoteType.TYPE_CHECKLIST) { checklistItems.size } else { 1 diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/databases/NotesDatabase.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/databases/NotesDatabase.kt index e54611a8..15cab424 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/databases/NotesDatabase.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/databases/NotesDatabase.kt @@ -9,10 +9,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase import com.simplemobiletools.commons.helpers.PROTECTION_NONE import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.helpers.DEFAULT_WIDGET_TEXT_COLOR -import com.simplemobiletools.notes.pro.helpers.NoteType import com.simplemobiletools.notes.pro.interfaces.NotesDao import com.simplemobiletools.notes.pro.interfaces.WidgetsDao import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType import com.simplemobiletools.notes.pro.models.Widget import java.util.concurrent.Executors @@ -57,7 +57,7 @@ abstract class NotesDatabase : RoomDatabase() { private fun insertFirstNote(context: Context) { Executors.newSingleThreadScheduledExecutor().execute { val generalNote = context.resources.getString(R.string.general_note) - val note = Note(null, generalNote, "", NoteType.TYPE_TEXT.value, "", PROTECTION_NONE, "") + val note = Note(null, generalNote, "", NoteType.TYPE_TEXT, "", PROTECTION_NONE, "") db!!.NotesDao().insertOrUpdate(note) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportNotesDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportNotesDialog.kt new file mode 100644 index 00000000..e5218c0b --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportNotesDialog.kt @@ -0,0 +1,42 @@ +package com.simplemobiletools.notes.pro.dialogs + +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.notes.pro.R +import com.simplemobiletools.notes.pro.activities.SimpleActivity +import kotlinx.android.synthetic.main.dialog_export_notes.view.export_notes_filename + +class ExportNotesDialog(val activity: SimpleActivity, callback: (filename: String) -> Unit) { + + init { + val view = (activity.layoutInflater.inflate(R.layout.dialog_export_notes, null) as ViewGroup).apply { + export_notes_filename.setText( + buildString { + append(context.getString(R.string.notes)) + append("_") + append(context.getCurrentFormattedDateTime()) + } + ) + } + + activity.getAlertDialogBuilder().setPositiveButton(R.string.ok, null).setNegativeButton(R.string.cancel, null).apply { + activity.setupDialogStuff(view, this, R.string.export_notes) { alertDialog -> + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + + val filename = view.export_notes_filename.value + when { + filename.isEmpty() -> activity.toast(R.string.empty_name) + filename.isAValidFilename() -> { + callback(filename) + alertDialog.dismiss() + } + + else -> activity.toast(R.string.invalid_name) + } + } + } + } + } +} + diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ImportFolderDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ImportFolderDialog.kt index 5438e7f4..a7988e03 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ImportFolderDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ImportFolderDialog.kt @@ -9,10 +9,11 @@ import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.SimpleActivity import com.simplemobiletools.notes.pro.extensions.notesDB import com.simplemobiletools.notes.pro.extensions.parseChecklistItems -import com.simplemobiletools.notes.pro.helpers.NoteType import com.simplemobiletools.notes.pro.helpers.NotesHelper import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.dialog_import_folder.view.* +import com.simplemobiletools.notes.pro.models.NoteType +import kotlinx.android.synthetic.main.dialog_import_folder.view.open_file_filename +import kotlinx.android.synthetic.main.dialog_import_folder.view.open_file_type import java.io.File class ImportFolderDialog(val activity: SimpleActivity, val path: String, val callback: () -> Unit) : AlertDialog.Builder(activity) { @@ -50,21 +51,21 @@ class ImportFolderDialog(val activity: SimpleActivity, val path: String, val cal activity.notesDB.getNoteIdWithTitle(filename) != null -> false else -> true } - }.forEach { + }?.forEach { val storePath = if (updateFilesOnEdit) it.absolutePath else "" val title = it.absolutePath.getFilenameFromPath() val value = if (updateFilesOnEdit) "" else it.readText() val fileText = it.readText().trim() val checklistItems = fileText.parseChecklistItems() if (checklistItems != null) { - saveNote(title.substringBeforeLast('.'), fileText, NoteType.TYPE_CHECKLIST.value, "") + saveNote(title.substringBeforeLast('.'), fileText, NoteType.TYPE_CHECKLIST, "") } else { if (updateFilesOnEdit) { activity.handleSAFDialog(path) { - saveNote(title, value, NoteType.TYPE_TEXT.value, storePath) + saveNote(title, value, NoteType.TYPE_TEXT, storePath) } } else { - saveNote(title, value, NoteType.TYPE_TEXT.value, storePath) + saveNote(title, value, NoteType.TYPE_TEXT, storePath) } } } @@ -75,7 +76,7 @@ class ImportFolderDialog(val activity: SimpleActivity, val path: String, val cal } } - private fun saveNote(title: String, value: String, type: Int, path: String) { + private fun saveNote(title: String, value: String, type: NoteType, path: String) { val note = Note(null, title, value, type, path, PROTECTION_NONE, "") NotesHelper(activity).insertOrUpdateNote(note) } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewNoteDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewNoteDialog.kt index 9ef06225..b18720a3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewNoteDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewNoteDialog.kt @@ -8,8 +8,8 @@ import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.extensions.config import com.simplemobiletools.notes.pro.extensions.notesDB -import com.simplemobiletools.notes.pro.helpers.NoteType import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType import kotlinx.android.synthetic.main.dialog_new_note.view.* class NewNoteDialog(val activity: Activity, title: String? = null, val setChecklistAsDefault: Boolean, callback: (note: Note) -> Unit) { @@ -40,12 +40,12 @@ class NewNoteDialog(val activity: Activity, title: String? = null, val setCheckl activity.notesDB.getNoteIdWithTitle(newTitle) != null -> activity.toast(R.string.title_taken) else -> { val type = if (view.new_note_type.checkedRadioButtonId == view.type_checklist.id) { - NoteType.TYPE_CHECKLIST.value + NoteType.TYPE_CHECKLIST } else { - NoteType.TYPE_TEXT.value + NoteType.TYPE_TEXT } - activity.config.lastCreatedNoteType = type + activity.config.lastCreatedNoteType = type.value val newNote = Note(null, newTitle, "", type, "", PROTECTION_NONE, "") callback(newNote) alertDialog.dismiss() diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenFileDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenFileDialog.kt index eeb5ddb8..630eafaa 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenFileDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenFileDialog.kt @@ -9,8 +9,8 @@ import com.simplemobiletools.commons.extensions.setupDialogStuff import com.simplemobiletools.commons.helpers.PROTECTION_NONE import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.SimpleActivity -import com.simplemobiletools.notes.pro.helpers.NoteType import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType import kotlinx.android.synthetic.main.dialog_open_file.view.* import java.io.File @@ -47,7 +47,7 @@ class OpenFileDialog(val activity: SimpleActivity, val path: String, val callbac private fun saveNote(storeContent: String, storePath: String) { val filename = path.getFilenameFromPath() - val note = Note(null, filename, storeContent, NoteType.TYPE_TEXT.value, storePath, PROTECTION_NONE, "") + val note = Note(null, filename, storeContent, NoteType.TYPE_TEXT, storePath, PROTECTION_NONE, "") callback(note) dialog?.dismiss() } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenNoteDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenNoteDialog.kt index 325d1f62..b465a41c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenNoteDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenNoteDialog.kt @@ -31,7 +31,7 @@ class OpenNoteDialog(val activity: Activity, val callback: (checkedId: Long, new } } - private fun initDialog(notes: ArrayList, view: View) { + private fun initDialog(notes: List, view: View) { val textColor = activity.getProperTextColor() notes.forEach { activity.layoutInflater.inflate(R.layout.open_note_item, null).apply { diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/UnlockNotesDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/UnlockNotesDialog.kt index b9b53d9b..2c8b362d 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/UnlockNotesDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/UnlockNotesDialog.kt @@ -10,7 +10,7 @@ import com.simplemobiletools.notes.pro.models.Note import kotlinx.android.synthetic.main.dialog_unlock_notes.view.* import kotlinx.android.synthetic.main.item_locked_note.view.* -class UnlockNotesDialog(val activity: BaseSimpleActivity, val notes: List, callback: (unlockedNoteIds: List) -> Unit) { +class UnlockNotesDialog(val activity: BaseSimpleActivity, val notes: List, callback: (unlockedNotes: List) -> Unit) { private var dialog: AlertDialog? = null private val view = activity.layoutInflater.inflate(R.layout.dialog_unlock_notes, null) as ViewGroup private val redColor = activity.getColor(R.color.md_red) @@ -29,7 +29,7 @@ class UnlockNotesDialog(val activity: BaseSimpleActivity, val notes: List, activity.setupDialogStuff(view, this, R.string.unlock_notes, cancelOnTouchOutside = false) { alertDialog -> dialog = alertDialog alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { - callback(unlockedNoteIds) + callback(unlockedNoteIds.mapNotNull { id -> notes.firstOrNull { it.id == id } }) alertDialog.dismiss() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Context.kt index 1d8cd8da..554728ef 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Context.kt @@ -4,12 +4,15 @@ import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context import android.content.Intent +import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.databases.NotesDatabase +import com.simplemobiletools.notes.pro.dialogs.UnlockNotesDialog import com.simplemobiletools.notes.pro.helpers.Config import com.simplemobiletools.notes.pro.helpers.MyWidgetProvider import com.simplemobiletools.notes.pro.interfaces.NotesDao import com.simplemobiletools.notes.pro.interfaces.WidgetsDao +import com.simplemobiletools.notes.pro.models.Note val Context.config: Config get() = Config.newInstance(applicationContext) @@ -29,3 +32,14 @@ fun Context.updateWidgets() { } fun Context.getPercentageFontSize() = resources.getDimension(R.dimen.middle_text_size) * (config.fontSizePercentage / 100f) + +fun BaseSimpleActivity.requestUnlockNotes(notes: List, callback: (unlockedNotes: List) -> Unit) { + val lockedNotes = notes.filter { it.isLocked() } + if (lockedNotes.isNotEmpty()) { + runOnUiThread { + UnlockNotesDialog(this, lockedNotes, callback) + } + } else { + callback(emptyList()) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/ChecklistFragment.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/ChecklistFragment.kt index ffd1c840..0f44895a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/ChecklistFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/ChecklistFragment.kt @@ -29,7 +29,7 @@ class ChecklistFragment : NoteFragment(), ChecklistItemsListener { lateinit var view: ViewGroup - var items = ArrayList() + var items = mutableListOf() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { view = inflater.inflate(R.layout.fragment_checklist, container, false) as ViewGroup diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Config.kt index 9f3da2ad..6741c520 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Config.kt @@ -5,6 +5,7 @@ import android.content.Context import android.os.Environment import android.view.Gravity import com.simplemobiletools.commons.helpers.BaseConfig +import com.simplemobiletools.notes.pro.models.NoteType class Config(context: Context) : BaseConfig(context) { companion object { diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Constants.kt index bcfc706f..415e7f5a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Constants.kt @@ -44,9 +44,6 @@ const val GRAVITY_LEFT = 0 const val GRAVITY_CENTER = 1 const val GRAVITY_RIGHT = 2 -// note types -enum class NoteType(val value: Int) { TYPE_TEXT(0), TYPE_CHECKLIST(1) } - // mime types const val MIME_TEXT_PLAIN = "text/plain" diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesExporter.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesExporter.kt deleted file mode 100644 index bc9ac4bf..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesExporter.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.simplemobiletools.notes.pro.helpers - -import android.content.Context -import com.google.gson.Gson -import com.google.gson.stream.JsonWriter -import com.simplemobiletools.commons.helpers.PROTECTION_NONE -import com.simplemobiletools.commons.helpers.ensureBackgroundThread -import com.simplemobiletools.notes.pro.models.Note -import java.io.OutputStream - -class NotesExporter(private val context: Context) { - enum class ExportResult { - EXPORT_FAIL, EXPORT_OK - } - - private val gson = Gson() - - fun exportNotes(notes: List, unlockedNoteIds: List, outputStream: OutputStream?, callback: (result: ExportResult) -> Unit) { - ensureBackgroundThread { - if (outputStream == null) { - callback.invoke(ExportResult.EXPORT_FAIL) - return@ensureBackgroundThread - } - val writer = JsonWriter(outputStream.bufferedWriter()) - writer.use { - try { - var written = 0 - writer.beginArray() - for (note in notes) { - if (!note.isLocked() || note.id in unlockedNoteIds) { - val noteToSave = getNoteToExport(note) - writer.jsonValue(gson.toJson(noteToSave)) - written++ - } - } - writer.endArray() - callback.invoke(ExportResult.EXPORT_OK) - } catch (e: Exception) { - callback.invoke(ExportResult.EXPORT_FAIL) - } - } - } - } - - private fun getNoteToExport(note: Note): Note { - return Note(null, note.title, note.getNoteStoredValue(context) ?: "", note.type, "", PROTECTION_NONE, "") - } -} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesHelper.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesHelper.kt index a757eb89..bf25adb5 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesHelper.kt @@ -1,19 +1,20 @@ package com.simplemobiletools.notes.pro.helpers import android.content.Context -import android.net.Uri import android.os.Handler import android.os.Looper +import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.helpers.PROTECTION_NONE import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.extensions.config import com.simplemobiletools.notes.pro.extensions.notesDB import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType import java.io.File class NotesHelper(val context: Context) { - fun getNotes(callback: (notes: ArrayList) -> Unit) { + fun getNotes(callback: (notes: List) -> Unit) { ensureBackgroundThread { // make sure the initial note has enough time to be precreated if (context.config.appRunCount <= 1) { @@ -21,8 +22,8 @@ class NotesHelper(val context: Context) { Thread.sleep(200) } - val notes = context.notesDB.getNotes() as ArrayList - val notesToDelete = ArrayList(notes.size) + val notes = context.notesDB.getNotes().toMutableList() + val notesToDelete = mutableListOf() notes.forEach { if (it.path.isNotEmpty()) { if (!it.path.startsWith("content://") && !File(it.path).exists()) { @@ -36,7 +37,7 @@ class NotesHelper(val context: Context) { if (notes.isEmpty()) { val generalNote = context.resources.getString(R.string.general_note) - val note = Note(null, generalNote, "", NoteType.TYPE_TEXT.value, "", PROTECTION_NONE, "") + val note = Note(null, generalNote, "", NoteType.TYPE_TEXT, "", PROTECTION_NONE, "") context.notesDB.insertOrUpdate(note) notes.add(note) } @@ -73,4 +74,57 @@ class NotesHelper(val context: Context) { } } } + + fun insertOrUpdateNotes(notes: List, callback: ((newNoteIds: List) -> Unit)? = null) { + ensureBackgroundThread { + val noteIds = context.notesDB.insertOrUpdate(notes) + Handler(Looper.getMainLooper()).post { + callback?.invoke(noteIds) + } + } + } + + fun importNotes(activity: BaseSimpleActivity, notes: List, callback: (ImportResult) -> Unit) { + ensureBackgroundThread { + val currentNotes = activity.notesDB.getNotes() + if (currentNotes.isEmpty()) { + insertOrUpdateNotes(notes) { savedNotes -> + + val newCurrentNotes = activity.notesDB.getNotes() + + val result = when { + currentNotes.size == newCurrentNotes.size -> ImportResult.IMPORT_NOTHING_NEW + notes.size == savedNotes.size -> ImportResult.IMPORT_OK + savedNotes.isEmpty() -> ImportResult.IMPORT_FAIL + else -> ImportResult.IMPORT_PARTIAL + } + callback(result) + } + } else { + var imported = 0 + var skipped = 0 + + notes.forEach { note -> + val exists = context.notesDB.getNoteIdWithTitle(note.title) != null + if (!exists) { + context.notesDB.insertOrUpdate(note) + imported++ + } else { + skipped++ + } + } + + val result = when { + skipped == notes.size || imported == 0 -> ImportResult.IMPORT_NOTHING_NEW + imported == notes.size -> ImportResult.IMPORT_OK + else -> ImportResult.IMPORT_PARTIAL + } + callback(result) + } + } + } + + enum class ImportResult { + IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL, IMPORT_NOTHING_NEW + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesImporter.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesImporter.kt deleted file mode 100644 index 8ec4fca0..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesImporter.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.simplemobiletools.notes.pro.helpers - -import android.content.Context -import com.google.gson.Gson -import com.google.gson.JsonSyntaxException -import com.google.gson.reflect.TypeToken -import com.simplemobiletools.commons.extensions.showErrorToast -import com.simplemobiletools.commons.helpers.PROTECTION_NONE -import com.simplemobiletools.commons.helpers.ensureBackgroundThread -import com.simplemobiletools.notes.pro.extensions.notesDB -import com.simplemobiletools.notes.pro.models.Note -import java.io.File - -class NotesImporter(private val context: Context) { - enum class ImportResult { - IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL, IMPORT_NOTHING_NEW - } - - private val gson = Gson() - private var notesImported = 0 - private var notesFailed = 0 - private var notesSkipped = 0 - - fun importNotes(path: String, filename: String, force: Boolean = false, callback: (result: ImportResult) -> Unit) { - ensureBackgroundThread { - try { - val inputStream = if (path.contains("/")) { - File(path).inputStream() - } else { - context.assets.open(path) - } - - inputStream.bufferedReader().use { reader -> - val json = reader.readText() - val type = object : TypeToken>() {}.type - val notes = gson.fromJson>(json, type) - val totalNotes = notes?.size ?: 0 - if (totalNotes <= 0) { - callback.invoke(ImportResult.IMPORT_FAIL) - return@ensureBackgroundThread - } - - for (note in notes) { - val exists = context.notesDB.getNoteIdWithTitle(note.title) != null - if (!exists) { - context.notesDB.insertOrUpdate(note) - notesImported++ - } else { - notesSkipped++ - } - } - } - } catch (e: JsonSyntaxException) { - if (force) { - callback(ImportResult.IMPORT_FAIL) - return@ensureBackgroundThread - } - - // Import notes expects a json with note name, content etc, but lets be more flexible and accept the basic files with note content only too - val inputStream = if (path.contains("/")) { - File(path).inputStream() - } else { - context.assets.open(path) - } - - inputStream.bufferedReader().use { reader -> - val text = reader.readText() - val note = Note(null, filename, text, NoteType.TYPE_TEXT.value, "", PROTECTION_NONE, "") - var i = 1 - if (context.notesDB.getNoteIdWithTitle(note.title) != null) { - while (true) { - val tryTitle = "$filename ($i)" - if (context.notesDB.getNoteIdWithTitle(tryTitle) == null) { - break - } - i++ - } - - note.title = "$filename ($i)" - } - - context.notesDB.insertOrUpdate(note) - notesImported++ - } - - } catch (e: Exception) { - context.showErrorToast(e) - notesFailed++ - } - - callback.invoke( - when { - notesSkipped > 0 && notesImported == 0 -> ImportResult.IMPORT_NOTHING_NEW - notesImported == 0 -> ImportResult.IMPORT_FAIL - notesFailed > 0 -> ImportResult.IMPORT_PARTIAL - else -> ImportResult.IMPORT_OK - } - ) - } - } -} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/NotesDao.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/NotesDao.kt index 1fb7b141..987fba5f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/NotesDao.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/NotesDao.kt @@ -23,6 +23,9 @@ interface NotesDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertOrUpdate(note: Note): Long + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrUpdate(notes: List): List + @Delete fun deleteNote(note: Note) } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/ChecklistItem.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/ChecklistItem.kt index 79d79be8..5f9bc70f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/ChecklistItem.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/ChecklistItem.kt @@ -3,8 +3,16 @@ package com.simplemobiletools.notes.pro.models import com.simplemobiletools.commons.helpers.AlphanumericComparator import com.simplemobiletools.commons.helpers.SORT_BY_TITLE import com.simplemobiletools.commons.helpers.SORT_DESCENDING +import kotlinx.serialization.Serializable + +@Serializable +data class ChecklistItem( + val id: Int, + val dateCreated: Long = 0L, + var title: String, + var isDone: Boolean +) : Comparable { -data class ChecklistItem(val id: Int, val dateCreated: Long = 0L, var title: String, var isDone: Boolean) : Comparable { companion object { var sorting = 0 } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Note.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Note.kt index 7a5170b7..0b010063 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Note.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Note.kt @@ -2,21 +2,27 @@ package com.simplemobiletools.notes.pro.models import android.content.Context import android.net.Uri -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.Index -import androidx.room.PrimaryKey +import androidx.room.* import com.simplemobiletools.commons.extensions.isBiometricIdAvailable import com.simplemobiletools.commons.helpers.PROTECTION_FINGERPRINT import com.simplemobiletools.commons.helpers.PROTECTION_NONE +import kotlinx.serialization.Serializable import java.io.File +/** + * Represents a note. + * + * @property value The content of the note. Could be plain text or [ChecklistItem] + * @property type The type of the note. Should be one of the [NoteType] enum entries. + */ +@Serializable @Entity(tableName = "notes", indices = [(Index(value = ["id"], unique = true))]) +@TypeConverters(NoteTypeConverter::class) data class Note( @PrimaryKey(autoGenerate = true) var id: Long?, @ColumnInfo(name = "title") var title: String, @ColumnInfo(name = "value") var value: String, - @ColumnInfo(name = "type") var type: Int, + @ColumnInfo(name = "type") var type: NoteType, @ColumnInfo(name = "path") var path: String, @ColumnInfo(name = "protection_type") var protectionType: Int, @ColumnInfo(name = "protection_hash") var protectionHash: String diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteType.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteType.kt new file mode 100644 index 00000000..d81d693a --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteType.kt @@ -0,0 +1,15 @@ +package com.simplemobiletools.notes.pro.models + +import kotlinx.serialization.Serializable + +@Serializable +enum class NoteType(val value: Int) { + TYPE_TEXT(0), + TYPE_CHECKLIST(1); + + companion object { + fun fromValue(value: Int): NoteType { + return values().find { it.value == value } ?: TYPE_TEXT + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteTypeConverter.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteTypeConverter.kt new file mode 100644 index 00000000..a2e5b2bb --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteTypeConverter.kt @@ -0,0 +1,15 @@ +package com.simplemobiletools.notes.pro.models + +import androidx.room.TypeConverter + +class NoteTypeConverter { + @TypeConverter + fun fromNoteType(noteType: NoteType): Int { + return noteType.value + } + + @TypeConverter + fun toNoteType(value: Int): NoteType { + return NoteType.fromValue(value) + } +} diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 37c58718..93fc0d18 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -346,6 +346,46 @@ android:text="@string/display_success_message" /> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_export_notes.xml b/app/src/main/res/layout/dialog_export_notes.xml new file mode 100644 index 00000000..7e354d38 --- /dev/null +++ b/app/src/main/res/layout/dialog_export_notes.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu.xml b/app/src/main/res/menu/menu.xml index 50516e8d..51034a2e 100644 --- a/app/src/main/res/menu/menu.xml +++ b/app/src/main/res/menu/menu.xml @@ -70,18 +70,10 @@ android:id="@+id/import_folder" android:title="@string/import_folder" app:showAsAction="never" /> - -