Show Import progress as Dialog

This commit is contained in:
PhilKes 2024-10-29 17:40:29 +01:00
parent df8d91e0f6
commit 014b4f5f11
39 changed files with 457 additions and 231 deletions

View file

@ -1,5 +1,4 @@
import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
@ -120,8 +119,11 @@ dependencies {
implementation "com.github.bumptech.glide:glide:4.15.1" implementation "com.github.bumptech.glide:glide:4.15.1"
implementation "com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0" implementation "com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0"
implementation "com.google.code.findbugs:jsr305:3.0.2"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
implementation "org.simpleframework:simple-xml:2.7.1" implementation("org.simpleframework:simple-xml:2.7.1") {
exclude group: 'xpp3', module: 'xpp3'
}
implementation 'org.jsoup:jsoup:1.18.1' implementation 'org.jsoup:jsoup:1.18.1'
testImplementation "junit:junit:4.13.2" testImplementation "junit:junit:4.13.2"

View file

@ -22,5 +22,28 @@
-keep class ** extends androidx.navigation.Navigator -keep class ** extends androidx.navigation.Navigator
-keep class ** implements org.ocpsoft.prettytime.TimeUnit -keep class ** implements org.ocpsoft.prettytime.TimeUnit
# SQLCipher
-keep class net.sqlcipher.** { *; } -keep class net.sqlcipher.** { *; }
-keep class net.sqlcipher.database.** { *; } -keep class net.sqlcipher.database.** { *; }
# SimpleXML
-keepattributes Signature
-keepattributes *Annotation
-keep interface org.simpleframework.xml.core.Label {
public *;
}
-keep class * implements org.simpleframework.xml.core.Label {
public *;
}
-keep interface org.simpleframework.xml.core.Parameter {
public *;
}
-keep class * implements org.simpleframework.xml.core.Parameter {
public *;
}
-keep interface org.simpleframework.xml.core.Extractor {
public *;
}
-keep class * implements org.simpleframework.xml.core.Extractor {
public *;
}

View file

@ -2,10 +2,21 @@ package com.philkes.notallyx.data.imports
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import androidx.lifecycle.MutableLiveData
import com.philkes.notallyx.data.model.BaseNote import com.philkes.notallyx.data.model.BaseNote
import java.io.File import java.io.File
interface ExternalImporter { interface ExternalImporter {
fun importFrom(uri: Uri, app: Application): Pair<List<BaseNote>, File> /**
* Parses [BaseNote]s from [source] and copies attached files/images/audios to [destination]
*
* @return List of [BaseNote]s to import + folder containing attached files
*/
fun import(
app: Application,
source: Uri,
destination: File,
progress: MutableLiveData<ImportProgress>? = null,
): Pair<List<BaseNote>, File>
} }

View file

@ -1,3 +1,3 @@
package com.philkes.notallyx.data.imports package com.philkes.notallyx.data.imports
class ImportException(val textResId: Int, cause: Throwable? = null) : RuntimeException(cause) class ImportException(val textResId: Int, cause: Throwable) : RuntimeException(cause)

View file

@ -0,0 +1,17 @@
package com.philkes.notallyx.data.imports
import com.philkes.notallyx.presentation.view.misc.Progress
open class ImportProgress(
current: Int = 0,
total: Int = 0,
inProgress: Boolean = true,
indeterminate: Boolean = false,
val stage: ImportStage = ImportStage.IMPORT_NOTES,
) : Progress(current, total, inProgress, indeterminate)
enum class ImportStage {
IMPORT_NOTES,
EXTRACT_FILES,
IMPORT_FILES,
}

View file

@ -4,45 +4,83 @@ import android.app.Application
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import com.philkes.notallyx.R import com.philkes.notallyx.R
import com.philkes.notallyx.data.DataUtil import com.philkes.notallyx.data.DataUtil
import com.philkes.notallyx.data.NotallyDatabase import com.philkes.notallyx.data.NotallyDatabase
import com.philkes.notallyx.data.imports.evernote.EvernoteImporter import com.philkes.notallyx.data.imports.evernote.EvernoteImporter
import com.philkes.notallyx.data.imports.google.GoogleKeepImporter import com.philkes.notallyx.data.imports.google.GoogleKeepImporter
import com.philkes.notallyx.data.model.Audio import com.philkes.notallyx.data.model.Audio
import com.philkes.notallyx.data.model.BaseNote
import com.philkes.notallyx.data.model.FileAttachment import com.philkes.notallyx.data.model.FileAttachment
import com.philkes.notallyx.data.model.Label import com.philkes.notallyx.data.model.Label
import com.philkes.notallyx.presentation.viewmodel.NotallyModel import com.philkes.notallyx.presentation.viewmodel.NotallyModel
import java.io.File import java.io.File
import java.util.concurrent.atomic.AtomicInteger
class NotesImporter(private val app: Application, private val database: NotallyDatabase) { class NotesImporter(private val app: Application, private val database: NotallyDatabase) {
suspend fun import(uri: Uri, importSource: ImportSource) { suspend fun import(
val (notes, importDataFolder) = uri: Uri,
when (importSource) { importSource: ImportSource,
ImportSource.GOOGLE_KEEP -> GoogleKeepImporter().importFrom(uri, app) progress: MutableLiveData<ImportProgress>? = null,
ImportSource.EVERNOTE -> EvernoteImporter().importFrom(uri, app) ): Int {
} val tempDir = File(app.cacheDir, IMPORT_CACHE_FOLDER)
database.getLabelDao().insert(notes.flatMap { it.labels }.distinct().map { Label(it) }) if (!tempDir.exists()) {
importFiles( tempDir.mkdirs()
notes.flatMap { it.files }.distinct(), }
importDataFolder, try {
NotallyModel.FileType.ANY, val (notes, importDataFolder) =
) try {
importFiles( when (importSource) {
notes.flatMap { it.images }.distinct(), ImportSource.GOOGLE_KEEP -> GoogleKeepImporter()
importDataFolder, ImportSource.EVERNOTE -> EvernoteImporter()
NotallyModel.FileType.IMAGE, }.import(app, uri, tempDir, progress)
) } catch (e: Exception) {
importAudios(notes.flatMap { it.audios }.distinct(), importDataFolder) Log.e(TAG, "import: failed", e)
database.getBaseNoteDao().insert(notes) progress?.postValue(ImportProgress(inProgress = false))
throw e
}
database.getLabelDao().insert(notes.flatMap { it.labels }.distinct().map { Label(it) })
val files = notes.flatMap { it.files }.distinct()
val images = notes.flatMap { it.images }.distinct()
val audios = notes.flatMap { it.audios }.distinct()
val totalFiles = files.size + images.size + audios.size
val counter = AtomicInteger(1)
progress?.postValue(
ImportProgress(total = totalFiles, stage = ImportStage.IMPORT_FILES)
)
importFiles(
files,
importDataFolder,
NotallyModel.FileType.ANY,
progress,
totalFiles,
counter,
)
importFiles(
images,
importDataFolder,
NotallyModel.FileType.IMAGE,
progress,
totalFiles,
counter,
)
importAudios(audios, importDataFolder, progress, totalFiles, counter)
database.getBaseNoteDao().insert(notes)
progress?.postValue(ImportProgress(inProgress = false))
return notes.size
} finally {
tempDir.deleteRecursively()
}
} }
private suspend fun importFiles( private suspend fun importFiles(
files: List<FileAttachment>, files: List<FileAttachment>,
sourceFolder: File, sourceFolder: File,
fileType: NotallyModel.FileType, fileType: NotallyModel.FileType,
progress: MutableLiveData<ImportProgress>?,
total: Int?,
counter: AtomicInteger?,
) { ) {
files.forEach { file -> files.forEach { file ->
val uri = File(sourceFolder, file.localName).toUri() val uri = File(sourceFolder, file.localName).toUri()
@ -55,27 +93,47 @@ class NotesImporter(private val app: Application, private val database: NotallyD
file.originalName = fileAttachment.originalName file.originalName = fileAttachment.originalName
file.mimeType = fileAttachment.mimeType file.mimeType = fileAttachment.mimeType
} }
error?.let { Log.d(TAG, "Failed to import: $error") } error?.let { Log.e(TAG, "Failed to import: $error") }
progress?.postValue(
ImportProgress(
current = counter!!.getAndIncrement(),
total = total!!,
stage = ImportStage.IMPORT_FILES,
)
)
} }
} }
private suspend fun importAudios(audios: List<Audio>, sourceFolder: File) { private suspend fun importAudios(
audios: List<Audio>,
sourceFolder: File,
progress: MutableLiveData<ImportProgress>?,
totalFiles: Int,
counter: AtomicInteger,
) {
audios.forEach { originalAudio -> audios.forEach { originalAudio ->
val file = File(sourceFolder, originalAudio.name) val file = File(sourceFolder, originalAudio.name)
val audio = DataUtil.addAudio(app, file, false) val audio = DataUtil.addAudio(app, file, false)
originalAudio.name = audio.name originalAudio.name = audio.name
originalAudio.duration = if (audio.duration == 0L) null else audio.duration originalAudio.duration = if (audio.duration == 0L) null else audio.duration
originalAudio.timestamp = audio.timestamp originalAudio.timestamp = audio.timestamp
progress?.postValue(
ImportProgress(
current = counter.getAndIncrement(),
total = totalFiles,
stage = ImportStage.IMPORT_FILES,
)
)
} }
} }
companion object { companion object {
private const val TAG = "NotesImporter" private const val TAG = "NotesImporter"
const val IMPORT_CACHE_FOLDER = "imports"
} }
} }
enum class ImportSource( enum class ImportSource(
val folderName: String,
val displayNameResId: Int, val displayNameResId: Int,
val mimeType: String, val mimeType: String,
val helpTextResId: Int, val helpTextResId: Int,
@ -83,7 +141,6 @@ enum class ImportSource(
val iconResId: Int, val iconResId: Int,
) { ) {
GOOGLE_KEEP( GOOGLE_KEEP(
"googlekeep",
R.string.google_keep, R.string.google_keep,
"application/zip", "application/zip",
R.string.google_keep_help, R.string.google_keep_help,
@ -91,7 +148,6 @@ enum class ImportSource(
R.drawable.icon_google_keep, R.drawable.icon_google_keep,
), ),
EVERNOTE( EVERNOTE(
"evernote",
R.string.evernote, R.string.evernote,
"*/*", // 'application/enex+xml' is not recognized "*/*", // 'application/enex+xml' is not recognized
R.string.evernote_help, R.string.evernote_help,
@ -99,11 +155,3 @@ enum class ImportSource(
R.drawable.icon_evernote, R.drawable.icon_evernote,
), ),
} }
data class NotesImport(
val baseNotes: List<BaseNote>,
val labels: List<Label>,
val files: List<FileAttachment>,
val images: List<FileAttachment>,
val audios: List<Audio>,
)

View file

@ -4,10 +4,12 @@ import android.app.Application
import android.net.Uri import android.net.Uri
import android.util.Base64 import android.util.Base64
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.lifecycle.MutableLiveData
import com.philkes.notallyx.R import com.philkes.notallyx.R
import com.philkes.notallyx.data.imports.ExternalImporter import com.philkes.notallyx.data.imports.ExternalImporter
import com.philkes.notallyx.data.imports.ImportException import com.philkes.notallyx.data.imports.ImportException
import com.philkes.notallyx.data.imports.ImportSource import com.philkes.notallyx.data.imports.ImportProgress
import com.philkes.notallyx.data.imports.ImportStage
import com.philkes.notallyx.data.imports.evernote.EvernoteImporter.Companion.parseTimestamp import com.philkes.notallyx.data.imports.evernote.EvernoteImporter.Companion.parseTimestamp
import com.philkes.notallyx.data.imports.parseBodyAndSpansFromHtml import com.philkes.notallyx.data.imports.parseBodyAndSpansFromHtml
import com.philkes.notallyx.data.model.Audio import com.philkes.notallyx.data.model.Audio
@ -18,6 +20,7 @@ import com.philkes.notallyx.data.model.Folder
import com.philkes.notallyx.data.model.ListItem import com.philkes.notallyx.data.model.ListItem
import com.philkes.notallyx.data.model.Type import com.philkes.notallyx.data.model.Type
import com.philkes.notallyx.utils.IO.write import com.philkes.notallyx.utils.IO.write
import com.philkes.notallyx.utils.Operations
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -31,25 +34,38 @@ import org.simpleframework.xml.core.Persister
class EvernoteImporter : ExternalImporter { class EvernoteImporter : ExternalImporter {
private val serializer: Serializer = Persister(AnnotationStrategy()) private val serializer: Serializer = Persister(AnnotationStrategy())
override fun importFrom(uri: Uri, app: Application): Pair<List<BaseNote>, File> { override fun import(
if (MimeTypeMap.getFileExtensionFromUrl(uri.toString()) != "enex") { app: Application,
throw ImportException(R.string.invalid_evernote) source: Uri,
} destination: File,
val tempDir = File(app.cacheDir, ImportSource.EVERNOTE.folderName) progress: MutableLiveData<ImportProgress>?,
if (!tempDir.exists()) { ): Pair<List<BaseNote>, File> {
tempDir.mkdirs() progress?.postValue(ImportProgress(indeterminate = true))
if (MimeTypeMap.getFileExtensionFromUrl(source.toString()) != "enex") {
throw ImportException(
R.string.invalid_evernote,
IllegalArgumentException("Provided file is not in ENEX format"),
)
} }
val evernoteExport: EvernoteExport = val evernoteExport: EvernoteExport =
parseExport(app.contentResolver.openInputStream(uri)!!)!! parseExport(app.contentResolver.openInputStream(source)!!)!!
saveResourcesToFiles(
evernoteExport.notes.flatMap { it.resources }.distinctBy { it.attributes?.fileName }, val total = evernoteExport.notes.size
tempDir, progress?.postValue(ImportProgress(total = total))
) var counter = 1
try { try {
val notes = evernoteExport.notes.map { it.mapToBaseNote() } val notes =
return Pair(notes, tempDir) evernoteExport.notes.map {
val note = it.mapToBaseNote()
progress?.postValue(ImportProgress(current = counter++, total = total))
note
}
val resources =
evernoteExport.notes.flatMap { it.resources }.distinctBy { it.attributes?.fileName }
saveResourcesToFiles(app, resources, destination, progress)
return Pair(notes, destination)
} catch (e: Exception) { } catch (e: Exception) {
throw ImportException(R.string.invalid_evernote) throw ImportException(R.string.invalid_evernote, e)
} }
} }
@ -60,11 +76,30 @@ class EvernoteImporter : ExternalImporter {
throw ImportException(R.string.invalid_evernote, e) throw ImportException(R.string.invalid_evernote, e)
} }
private fun saveResourcesToFiles(resources: Collection<EvernoteResource>, dir: File) { private fun saveResourcesToFiles(
resources.forEach { app: Application,
resources: Collection<EvernoteResource>,
dir: File,
progress: MutableLiveData<ImportProgress>? = null,
) {
progress?.postValue(
ImportProgress(total = resources.size, stage = ImportStage.EXTRACT_FILES)
)
resources.forEachIndexed { idx, it ->
val file = File(dir, it.attributes!!.fileName) val file = File(dir, it.attributes!!.fileName)
val data = Base64.decode(it.data!!.content.trimStart(), Base64.DEFAULT) try {
file.write(data) val data = Base64.decode(it.data!!.content.trimStart(), Base64.DEFAULT)
file.write(data)
} catch (e: Exception) {
Operations.log(app, e)
}
progress?.postValue(
ImportProgress(
current = idx + 1,
total = resources.size,
stage = ImportStage.EXTRACT_FILES,
)
)
} }
} }

View file

@ -2,10 +2,12 @@ package com.philkes.notallyx.data.imports.google
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import androidx.lifecycle.MutableLiveData
import com.philkes.notallyx.R import com.philkes.notallyx.R
import com.philkes.notallyx.data.imports.ExternalImporter import com.philkes.notallyx.data.imports.ExternalImporter
import com.philkes.notallyx.data.imports.ImportException import com.philkes.notallyx.data.imports.ImportException
import com.philkes.notallyx.data.imports.ImportSource import com.philkes.notallyx.data.imports.ImportProgress
import com.philkes.notallyx.data.imports.ImportStage
import com.philkes.notallyx.data.imports.parseBodyAndSpansFromHtml import com.philkes.notallyx.data.imports.parseBodyAndSpansFromHtml
import com.philkes.notallyx.data.model.Audio import com.philkes.notallyx.data.model.Audio
import com.philkes.notallyx.data.model.BaseNote import com.philkes.notallyx.data.model.BaseNote
@ -32,58 +34,72 @@ class GoogleKeepImporter : ExternalImporter {
allowTrailingComma = true allowTrailingComma = true
} }
override fun importFrom(uri: Uri, app: Application): Pair<List<BaseNote>, File> { override fun import(
val tempDir = File(app.cacheDir, ImportSource.GOOGLE_KEEP.folderName) app: Application,
if (!tempDir.exists()) { source: Uri,
tempDir.mkdirs() destination: File,
} progress: MutableLiveData<ImportProgress>?,
): Pair<List<BaseNote>, File> {
progress?.postValue(ImportProgress(indeterminate = true, stage = ImportStage.EXTRACT_FILES))
val dataFolder = val dataFolder =
try { try {
unzip(tempDir, app.contentResolver.openInputStream(uri)!!) unzip(destination, app.contentResolver.openInputStream(source)!!)
} catch (e: Exception) { } catch (e: Exception) {
throw ImportException(R.string.invalid_google_keep, e) throw ImportException(R.string.invalid_google_keep, e)
} }
if (!dataFolder.exists()) { if (!dataFolder.exists()) {
throw ImportException(R.string.invalid_google_keep) throw ImportException(
R.string.invalid_google_keep,
RuntimeException("Extracting Takeout.zip failed"),
)
} }
val baseNotes = val noteFiles =
dataFolder dataFolder
.walk() .listFiles { file ->
.mapNotNull { importFile -> file.isFile && file.extension.equals("json", ignoreCase = true)
if (importFile.extension == "json") { }
parseToBaseNote(importFile.readText()) ?.toList() ?: emptyList()
} else null val total = noteFiles.size
progress?.postValue(ImportProgress(0, total, stage = ImportStage.IMPORT_NOTES))
var counter = 1
val baseNotes =
noteFiles
.mapNotNull { file ->
val baseNote = file.readText().parseToBaseNote()
progress?.postValue(
ImportProgress(counter++, total, stage = ImportStage.IMPORT_NOTES)
)
baseNote
} }
.toList() .toList()
return Pair(baseNotes, dataFolder) return Pair(baseNotes, dataFolder)
} }
fun parseToBaseNote(jsonString: String): BaseNote { fun String.parseToBaseNote(): BaseNote {
val keepNote = json.decodeFromString<KeepNote>(jsonString) val googleKeepNote = json.decodeFromString<GoogleKeepNote>(this)
val (body, spans) = val (body, spans) =
parseBodyAndSpansFromHtml( parseBodyAndSpansFromHtml(
keepNote.textContentHtml, googleKeepNote.textContentHtml,
paragraphsAsNewLine = true, paragraphsAsNewLine = true,
brTagsAsNewLine = true, brTagsAsNewLine = true,
) )
val images = val images =
keepNote.attachments googleKeepNote.attachments
.filter { it.mimetype.startsWith("image") } .filter { it.mimetype.startsWith("image") }
.map { FileAttachment(it.filePath, it.filePath, it.mimetype) } .map { FileAttachment(it.filePath, it.filePath, it.mimetype) }
val files = val files =
keepNote.attachments googleKeepNote.attachments
.filter { !it.mimetype.startsWith("audio") && !it.mimetype.startsWith("image") } .filter { !it.mimetype.startsWith("audio") && !it.mimetype.startsWith("image") }
.map { FileAttachment(it.filePath, it.filePath, it.mimetype) } .map { FileAttachment(it.filePath, it.filePath, it.mimetype) }
val audios = val audios =
keepNote.attachments googleKeepNote.attachments
.filter { it.mimetype.startsWith("audio") } .filter { it.mimetype.startsWith("audio") }
.map { Audio(it.filePath, 0L, System.currentTimeMillis()) } .map { Audio(it.filePath, 0L, System.currentTimeMillis()) }
val items = val items =
keepNote.listContent.mapIndexed { index, item -> googleKeepNote.listContent.mapIndexed { index, item ->
ListItem( ListItem(
body = item.text, body = item.text,
checked = item.isChecked, checked = item.isChecked,
@ -95,19 +111,19 @@ class GoogleKeepImporter : ExternalImporter {
return BaseNote( return BaseNote(
id = 0L, // Auto-generated id = 0L, // Auto-generated
type = if (keepNote.listContent.isNotEmpty()) Type.LIST else Type.NOTE, type = if (googleKeepNote.listContent.isNotEmpty()) Type.LIST else Type.NOTE,
folder = folder =
when { when {
keepNote.isTrashed -> Folder.DELETED googleKeepNote.isTrashed -> Folder.DELETED
keepNote.isArchived -> Folder.ARCHIVED googleKeepNote.isArchived -> Folder.ARCHIVED
else -> Folder.NOTES else -> Folder.NOTES
}, },
color = Color.DEFAULT, // Ignoring color mapping color = Color.DEFAULT, // Ignoring color mapping
title = keepNote.title, title = googleKeepNote.title,
pinned = keepNote.isPinned, pinned = googleKeepNote.isPinned,
timestamp = keepNote.createdTimestampUsec / 1000, timestamp = googleKeepNote.createdTimestampUsec / 1000,
modifiedTimestamp = keepNote.userEditedTimestampUsec / 1000, modifiedTimestamp = googleKeepNote.userEditedTimestampUsec / 1000,
labels = keepNote.labels.map { it.name }, labels = googleKeepNote.labels.map { it.name },
body = body, body = body,
spans = spans, spans = spans,
items = items, items = items,

View file

@ -4,8 +4,8 @@ import com.philkes.notallyx.data.model.Color
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class KeepNote( data class GoogleKeepNote(
val attachments: List<KeepAttachment> = listOf(), val attachments: List<GoogleKeepAttachment> = listOf(),
val color: String = Color.DEFAULT.name, val color: String = Color.DEFAULT.name,
val isTrashed: Boolean = false, val isTrashed: Boolean = false,
val isArchived: Boolean = false, val isArchived: Boolean = false,
@ -13,14 +13,14 @@ data class KeepNote(
val textContent: String = "", val textContent: String = "",
val textContentHtml: String = "", val textContentHtml: String = "",
val title: String = "", val title: String = "",
val labels: List<KeepLabel> = listOf(), val labels: List<GoogleKeepLabel> = listOf(),
val userEditedTimestampUsec: Long = System.currentTimeMillis(), val userEditedTimestampUsec: Long = System.currentTimeMillis(),
val createdTimestampUsec: Long = System.currentTimeMillis(), val createdTimestampUsec: Long = System.currentTimeMillis(),
val listContent: List<KeepListItem> = listOf(), val listContent: List<GoogleKeepListItem> = listOf(),
) )
@Serializable data class KeepLabel(val name: String) @Serializable data class GoogleKeepLabel(val name: String)
@Serializable data class KeepAttachment(val filePath: String, val mimetype: String) @Serializable data class GoogleKeepAttachment(val filePath: String, val mimetype: String)
@Serializable data class KeepListItem(val text: String, val isChecked: Boolean) @Serializable data class GoogleKeepListItem(val text: String, val isChecked: Boolean)

View file

@ -48,6 +48,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philkes.notallyx.R import com.philkes.notallyx.R
import com.philkes.notallyx.data.imports.ImportProgress
import com.philkes.notallyx.data.imports.ImportStage
import com.philkes.notallyx.data.model.Folder import com.philkes.notallyx.data.model.Folder
import com.philkes.notallyx.data.model.SpanRepresentation import com.philkes.notallyx.data.model.SpanRepresentation
import com.philkes.notallyx.data.model.getUrl import com.philkes.notallyx.data.model.getUrl
@ -279,7 +281,7 @@ fun View.getString(id: Int, vararg formatArgs: String): String {
} }
fun View.getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any): String { fun View.getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any): String {
return context.resources.getQuantityString(id, quantity, *formatArgs) return context.getQuantityString(id, quantity, *formatArgs)
} }
fun Folder.movedToResId(): Int { fun Folder.movedToResId(): Int {
@ -370,11 +372,33 @@ fun MutableLiveData<out Progress>.setupProgressDialog(fragment: Fragment, titleI
) )
} }
private fun MutableLiveData<out Progress>.setupProgressDialog( fun MutableLiveData<ImportProgress>.setupImportProgressDialog(fragment: Fragment, titleId: Int) {
setupProgressDialog(
fragment.requireContext(),
fragment.layoutInflater,
fragment.viewLifecycleOwner,
titleId,
) { context, binding, progress ->
val stageStr =
context.getString(
when (progress.stage) {
ImportStage.IMPORT_NOTES -> R.string.imported_notes
ImportStage.EXTRACT_FILES -> R.string.extracted_files
ImportStage.IMPORT_FILES -> R.string.imported_files
}
)
binding.Count.text =
"${context.getString(R.string.count, progress.current, progress.total)} $stageStr"
}
}
private fun <T : Progress> MutableLiveData<T>.setupProgressDialog(
context: Context, context: Context,
layoutInflater: LayoutInflater, layoutInflater: LayoutInflater,
viewLifecycleOwner: LifecycleOwner, viewLifecycleOwner: LifecycleOwner,
titleId: Int, titleId: Int,
renderProgress: ((context: Context, binding: DialogProgressBinding, progress: T) -> Unit)? =
null,
) { ) {
val dialogBinding = DialogProgressBinding.inflate(layoutInflater) val dialogBinding = DialogProgressBinding.inflate(layoutInflater)
val dialog = val dialog =
@ -398,7 +422,10 @@ private fun MutableLiveData<out Progress>.setupProgressDialog(
max = progress.total max = progress.total
setProgressCompat(progress.current, true) setProgressCompat(progress.current, true)
} }
Count.text = context.getString(R.string.count, progress.current, progress.total) if (renderProgress == null) {
Count.text =
context.getString(R.string.count, progress.current, progress.total)
} else renderProgress.invoke(context, this, progress)
} }
} }
dialog.show() dialog.show()
@ -455,3 +482,7 @@ fun MaterialAlertDialogBuilder.showAndFocus(view: View): AlertDialog {
} }
return dialog return dialog
} }
fun Context.getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any): String {
return resources.getQuantityString(id, quantity, quantity, *formatArgs)
}

View file

@ -41,6 +41,7 @@ import com.philkes.notallyx.presentation.activity.note.EditListActivity
import com.philkes.notallyx.presentation.activity.note.EditNoteActivity import com.philkes.notallyx.presentation.activity.note.EditNoteActivity
import com.philkes.notallyx.presentation.add import com.philkes.notallyx.presentation.add
import com.philkes.notallyx.presentation.applySpans import com.philkes.notallyx.presentation.applySpans
import com.philkes.notallyx.presentation.getQuantityString
import com.philkes.notallyx.presentation.movedToResId import com.philkes.notallyx.presentation.movedToResId
import com.philkes.notallyx.presentation.view.main.ColorAdapter import com.philkes.notallyx.presentation.view.main.ColorAdapter
import com.philkes.notallyx.presentation.view.misc.MenuDialog import com.philkes.notallyx.presentation.view.misc.MenuDialog
@ -217,7 +218,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
val ids = model.moveBaseNotes(folderTo) val ids = model.moveBaseNotes(folderTo)
Snackbar.make( Snackbar.make(
findViewById(R.id.DrawerLayout), findViewById(R.id.DrawerLayout),
resources.getQuantityString(folderTo.movedToResId(), ids.size, ids.size), getQuantityString(folderTo.movedToResId(), ids.size),
Snackbar.LENGTH_SHORT, Snackbar.LENGTH_SHORT,
) )
.apply { setAction(R.string.undo) { model.moveBaseNotes(ids, folderFrom) } } .apply { setAction(R.string.undo) { model.moveBaseNotes(ids, folderFrom) } }

View file

@ -25,6 +25,7 @@ import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.FO
import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.NOTE_ID import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.NOTE_ID
import com.philkes.notallyx.presentation.activity.note.EditListActivity import com.philkes.notallyx.presentation.activity.note.EditListActivity
import com.philkes.notallyx.presentation.activity.note.EditNoteActivity import com.philkes.notallyx.presentation.activity.note.EditNoteActivity
import com.philkes.notallyx.presentation.getQuantityString
import com.philkes.notallyx.presentation.movedToResId import com.philkes.notallyx.presentation.movedToResId
import com.philkes.notallyx.presentation.view.Constants import com.philkes.notallyx.presentation.view.Constants
import com.philkes.notallyx.presentation.view.main.BaseNoteAdapter import com.philkes.notallyx.presentation.view.main.BaseNoteAdapter
@ -103,7 +104,7 @@ abstract class NotallyFragment : Fragment(), ListItemListener {
val folderTo = Folder.valueOf(data.getStringExtra(FOLDER_TO)!!) val folderTo = Folder.valueOf(data.getStringExtra(FOLDER_TO)!!)
Snackbar.make( Snackbar.make(
binding!!.root, binding!!.root,
resources.getQuantityString(folderTo.movedToResId(), 1, 1), requireContext().getQuantityString(folderTo.movedToResId(), 1),
Snackbar.LENGTH_SHORT, Snackbar.LENGTH_SHORT,
) )
.apply { .apply {

View file

@ -31,6 +31,7 @@ import com.philkes.notallyx.databinding.PreferenceSeekbarBinding
import com.philkes.notallyx.databinding.TextInputDialogBinding import com.philkes.notallyx.databinding.TextInputDialogBinding
import com.philkes.notallyx.presentation.canAuthenticateWithBiometrics import com.philkes.notallyx.presentation.canAuthenticateWithBiometrics
import com.philkes.notallyx.presentation.checkedTag import com.philkes.notallyx.presentation.checkedTag
import com.philkes.notallyx.presentation.setupImportProgressDialog
import com.philkes.notallyx.presentation.setupProgressDialog import com.philkes.notallyx.presentation.setupProgressDialog
import com.philkes.notallyx.presentation.view.misc.AutoBackup import com.philkes.notallyx.presentation.view.misc.AutoBackup
import com.philkes.notallyx.presentation.view.misc.AutoBackupMax import com.philkes.notallyx.presentation.view.misc.AutoBackupMax
@ -124,7 +125,7 @@ class SettingsFragment : Fragment() {
binding.ClearData.setOnClickListener { clearData() } binding.ClearData.setOnClickListener { clearData() }
model.exportProgress.setupProgressDialog(this, R.string.exporting_backup) model.exportProgress.setupProgressDialog(this, R.string.exporting_backup)
model.importProgress.setupProgressDialog(this, R.string.importing_backup) model.importProgress.setupImportProgressDialog(this, R.string.importing_backup)
model.deletionProgress.setupProgressDialog(this, R.string.deleting_files) model.deletionProgress.setupProgressDialog(this, R.string.deleting_files)
binding.GitHub.setOnClickListener { openLink("https://github.com/PhilKes/NotallyX") } binding.GitHub.setOnClickListener { openLink("https://github.com/PhilKes/NotallyX") }

View file

@ -34,6 +34,7 @@ import com.philkes.notallyx.databinding.ActivityEditBinding
import com.philkes.notallyx.presentation.activity.LockedActivity import com.philkes.notallyx.presentation.activity.LockedActivity
import com.philkes.notallyx.presentation.add import com.philkes.notallyx.presentation.add
import com.philkes.notallyx.presentation.displayFormattedTimestamp import com.philkes.notallyx.presentation.displayFormattedTimestamp
import com.philkes.notallyx.presentation.getQuantityString
import com.philkes.notallyx.presentation.setupProgressDialog import com.philkes.notallyx.presentation.setupProgressDialog
import com.philkes.notallyx.presentation.view.Constants import com.philkes.notallyx.presentation.view.Constants
import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByCreationDate import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByCreationDate
@ -518,7 +519,7 @@ abstract class EditActivity(private val type: Type) : LockedActivity<ActivityEdi
} else { } else {
R.plurals.cant_add_files R.plurals.cant_add_files
} }
val title = resources.getQuantityString(message, errors.size, errors.size) val title = getQuantityString(message, errors.size)
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(title) .setTitle(title)
.setView(recyclerView) .setView(recyclerView)

View file

@ -15,7 +15,6 @@ import android.view.ActionMode
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.core.util.PatternsCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philkes.notallyx.R import com.philkes.notallyx.R
import com.philkes.notallyx.data.model.Type import com.philkes.notallyx.data.model.Type

View file

@ -20,6 +20,7 @@ import com.philkes.notallyx.data.dao.BaseNoteDao
import com.philkes.notallyx.data.dao.CommonDao import com.philkes.notallyx.data.dao.CommonDao
import com.philkes.notallyx.data.dao.LabelDao import com.philkes.notallyx.data.dao.LabelDao
import com.philkes.notallyx.data.imports.ImportException import com.philkes.notallyx.data.imports.ImportException
import com.philkes.notallyx.data.imports.ImportProgress
import com.philkes.notallyx.data.imports.ImportSource import com.philkes.notallyx.data.imports.ImportSource
import com.philkes.notallyx.data.imports.NotesImporter import com.philkes.notallyx.data.imports.NotesImporter
import com.philkes.notallyx.data.model.Attachment import com.philkes.notallyx.data.model.Attachment
@ -36,6 +37,7 @@ import com.philkes.notallyx.data.model.Label
import com.philkes.notallyx.data.model.SearchResult import com.philkes.notallyx.data.model.SearchResult
import com.philkes.notallyx.data.model.Type import com.philkes.notallyx.data.model.Type
import com.philkes.notallyx.presentation.applySpans 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.AutoBackup
import com.philkes.notallyx.presentation.view.misc.ListInfo import com.philkes.notallyx.presentation.view.misc.ListInfo
import com.philkes.notallyx.presentation.view.misc.Progress import com.philkes.notallyx.presentation.view.misc.Progress
@ -109,7 +111,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
val fileRoot = app.getExternalFilesDirectory() val fileRoot = app.getExternalFilesDirectory()
private val audioRoot = app.getExternalAudioDirectory() private val audioRoot = app.getExternalAudioDirectory()
val importProgress = MutableLiveData<Progress>() val importProgress = MutableLiveData<ImportProgress>()
val exportProgress = MutableLiveData<Progress>() val exportProgress = MutableLiveData<Progress>()
val deletionProgress = MutableLiveData<Progress>() val deletionProgress = MutableLiveData<Progress>()
@ -219,8 +221,15 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
} }
fun importZipBackup(uri: Uri, password: String) { fun importZipBackup(uri: Uri, password: String) {
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Operations.log(app, throwable)
Toast.makeText(app, R.string.invalid_backup, Toast.LENGTH_LONG).show()
}
val backupDir = app.getBackupDir() val backupDir = app.getBackupDir()
viewModelScope.launch { importZip(app, uri, backupDir, password, importProgress) } viewModelScope.launch(exceptionHandler) {
importZip(app, uri, backupDir, password, importProgress)
}
} }
fun importXmlBackup(uri: Uri) { fun importXmlBackup(uri: Uri) {
@ -230,12 +239,15 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
} }
viewModelScope.launch(exceptionHandler) { viewModelScope.launch(exceptionHandler) {
withContext(Dispatchers.IO) { val importedNotes =
val stream = requireNotNull(app.contentResolver.openInputStream(uri)) withContext(Dispatchers.IO) {
val backup = XMLUtils.readBackupFromStream(stream) val stream = requireNotNull(app.contentResolver.openInputStream(uri))
commonDao.importBackup(backup.first, backup.second) val (baseNotes, labels) = XMLUtils.readBackupFromStream(stream)
} commonDao.importBackup(baseNotes, labels)
Toast.makeText(app, R.string.imported_backup, Toast.LENGTH_LONG).show() baseNotes.size
}
val message = app.getQuantityString(R.plurals.imported_notes, importedNotes)
Toast.makeText(app, message, Toast.LENGTH_LONG).show()
} }
} }
@ -254,8 +266,12 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
Operations.log(app, throwable) Operations.log(app, throwable)
} }
viewModelScope.launch(exceptionHandler) { viewModelScope.launch(exceptionHandler) {
withContext(Dispatchers.IO) { NotesImporter(app, database).import(uri, importSource) } val importedNotes =
Toast.makeText(app, R.string.imported_backup, Toast.LENGTH_LONG).show() withContext(Dispatchers.IO) {
NotesImporter(app, database).import(uri, importSource, importProgress)
}
val message = app.getQuantityString(R.plurals.imported_notes, importedNotes)
Toast.makeText(app, message, Toast.LENGTH_LONG).show()
} }
} }

View file

@ -13,7 +13,6 @@ import com.philkes.notallyx.R
import com.philkes.notallyx.data.NotallyDatabase import com.philkes.notallyx.data.NotallyDatabase
import com.philkes.notallyx.data.model.BaseNote import com.philkes.notallyx.data.model.BaseNote
import com.philkes.notallyx.data.model.Type import com.philkes.notallyx.data.model.Type
import com.philkes.notallyx.presentation.displayFormattedTimestamp
import com.philkes.notallyx.presentation.view.misc.TextSize import com.philkes.notallyx.presentation.view.misc.TextSize
import com.philkes.notallyx.presentation.widget.WidgetProvider.Companion.getSelectNoteIntent import com.philkes.notallyx.presentation.widget.WidgetProvider.Companion.getSelectNoteIntent

View file

@ -2,4 +2,8 @@ package com.philkes.notallyx.utils
import com.philkes.notallyx.presentation.viewmodel.NotallyModel import com.philkes.notallyx.presentation.viewmodel.NotallyModel
class FileError(val name: String, val description: String, val fileType: NotallyModel.FileType) data class FileError(
val name: String,
val description: String,
val fileType: NotallyModel.FileType,
)

View file

@ -6,9 +6,9 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.os.Build import android.os.Build
import androidx.lifecycle.MutableLiveData
import android.util.Log import android.util.Log
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import com.philkes.notallyx.data.model.Attachment import com.philkes.notallyx.data.model.Attachment
import com.philkes.notallyx.data.model.Audio import com.philkes.notallyx.data.model.Audio
import com.philkes.notallyx.data.model.FileAttachment import com.philkes.notallyx.data.model.FileAttachment
@ -85,7 +85,7 @@ object IO {
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
val mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE) val mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
mimeType != null && hasAudio != null || mimeType != null && hasAudio != null ||
duration != null // If it has audio metadata, it's a valid audio file duration != null // If it has audio metadata, it's a valid audio file
} catch (e: Exception) { } catch (e: Exception) {
false // An exception means its not a valid audio file false // An exception means its not a valid audio file
} finally { } finally {

View file

@ -9,6 +9,8 @@ import androidx.core.database.getLongOrNull
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.philkes.notallyx.R import com.philkes.notallyx.R
import com.philkes.notallyx.data.NotallyDatabase import com.philkes.notallyx.data.NotallyDatabase
import com.philkes.notallyx.data.imports.ImportProgress
import com.philkes.notallyx.data.imports.ImportStage
import com.philkes.notallyx.data.model.BaseNote import com.philkes.notallyx.data.model.BaseNote
import com.philkes.notallyx.data.model.Color import com.philkes.notallyx.data.model.Color
import com.philkes.notallyx.data.model.Converters import com.philkes.notallyx.data.model.Converters
@ -16,7 +18,7 @@ import com.philkes.notallyx.data.model.FileAttachment
import com.philkes.notallyx.data.model.Folder import com.philkes.notallyx.data.model.Folder
import com.philkes.notallyx.data.model.Label import com.philkes.notallyx.data.model.Label
import com.philkes.notallyx.data.model.Type import com.philkes.notallyx.data.model.Type
import com.philkes.notallyx.presentation.view.misc.Progress import com.philkes.notallyx.presentation.getQuantityString
import com.philkes.notallyx.utils.IO.SUBFOLDER_AUDIOS import com.philkes.notallyx.utils.IO.SUBFOLDER_AUDIOS
import com.philkes.notallyx.utils.IO.SUBFOLDER_FILES import com.philkes.notallyx.utils.IO.SUBFOLDER_FILES
import com.philkes.notallyx.utils.IO.SUBFOLDER_IMAGES import com.philkes.notallyx.utils.IO.SUBFOLDER_IMAGES
@ -46,93 +48,115 @@ object Import {
zipFileUri: Uri, zipFileUri: Uri,
databaseFolder: File, databaseFolder: File,
zipPassword: String, zipPassword: String,
importingBackup: MutableLiveData<Progress>? = null, importingBackup: MutableLiveData<ImportProgress>? = null,
) { ) {
importingBackup?.postValue(Progress(indeterminate = true)) importingBackup?.postValue(ImportProgress(indeterminate = true))
try { try {
withContext(Dispatchers.IO) { val importedNotes =
val stream = requireNotNull(app.contentResolver.openInputStream(zipFileUri)) withContext(Dispatchers.IO) {
val tempZipFile = File(databaseFolder, "TEMP.zip") val stream = requireNotNull(app.contentResolver.openInputStream(zipFileUri))
stream.copyToFile(tempZipFile) val tempZipFile = File(databaseFolder, "TEMP.zip")
val zipFile = ZipFile(tempZipFile) stream.copyToFile(tempZipFile)
if (zipFile.isEncrypted) { val zipFile = ZipFile(tempZipFile)
zipFile.setPassword(zipPassword.toCharArray()) if (zipFile.isEncrypted) {
} zipFile.setPassword(zipPassword.toCharArray())
zipFile.extractFile(
NotallyDatabase.DatabaseName,
databaseFolder.path,
NotallyDatabase.DatabaseName,
)
val database =
SQLiteDatabase.openDatabase(
File(databaseFolder, NotallyDatabase.DatabaseName).path,
null,
SQLiteDatabase.OPEN_READONLY,
)
val labelCursor = database.query("Label", null, null, null, null, null, null)
val baseNoteCursor = database.query("BaseNote", null, null, null, null, null, null)
val labels = labelCursor.toList { cursor -> cursor.toLabel() }
val baseNotes = baseNoteCursor.toList { cursor -> cursor.toBaseNote() }
delay(1000)
val total =
baseNotes.fold(0) { acc, baseNote ->
acc + baseNote.images.size + baseNote.files.size + baseNote.audios.size
} }
importingBackup?.postValue(Progress(0, total)) zipFile.extractFile(
NotallyDatabase.DatabaseName,
databaseFolder.path,
NotallyDatabase.DatabaseName,
)
val current = AtomicInteger(1) val database =
val imageRoot = app.getExternalImagesDirectory() SQLiteDatabase.openDatabase(
val fileRoot = app.getExternalFilesDirectory() File(databaseFolder, NotallyDatabase.DatabaseName).path,
val audioRoot = app.getExternalAudioDirectory() null,
baseNotes.forEach { baseNote -> SQLiteDatabase.OPEN_READONLY,
importFiles( )
app,
baseNote.images, val labelCursor = database.query("Label", null, null, null, null, null, null)
SUBFOLDER_IMAGES, val baseNoteCursor =
imageRoot, database.query("BaseNote", null, null, null, null, null, null)
zipFile,
current, val labels = labelCursor.toList { cursor -> cursor.toLabel() }
total,
var total = baseNoteCursor.count
var counter = 1
importingBackup?.postValue(ImportProgress(0, total))
val baseNotes =
baseNoteCursor.toList { cursor ->
val baseNote = cursor.toBaseNote()
importingBackup?.postValue(ImportProgress(counter++, total))
baseNote
}
delay(1000)
total =
baseNotes.fold(0) { acc, baseNote ->
acc + baseNote.images.size + baseNote.files.size + baseNote.audios.size
}
importingBackup?.postValue(
ImportProgress(0, total, stage = ImportStage.IMPORT_FILES)
) )
importFiles(
app, val current = AtomicInteger(1)
baseNote.files, val imageRoot = app.getExternalImagesDirectory()
SUBFOLDER_FILES, val fileRoot = app.getExternalFilesDirectory()
fileRoot, val audioRoot = app.getExternalAudioDirectory()
zipFile, baseNotes.forEach { baseNote ->
current, importFiles(
total, app,
) baseNote.images,
baseNote.audios.forEach { audio -> SUBFOLDER_IMAGES,
try { imageRoot,
val audioFilePath = "$SUBFOLDER_AUDIOS/${audio.name}" zipFile,
val entry = zipFile.getFileHeader(audioFilePath) current,
if (entry != null) { total,
val name = "${UUID.randomUUID()}.m4a" importingBackup,
zipFile.extractFile(audioFilePath, audioRoot!!.path, name) )
audio.name = name importFiles(
app,
baseNote.files,
SUBFOLDER_FILES,
fileRoot,
zipFile,
current,
total,
importingBackup,
)
baseNote.audios.forEach { audio ->
try {
val audioFilePath = "$SUBFOLDER_AUDIOS/${audio.name}"
val entry = zipFile.getFileHeader(audioFilePath)
if (entry != null) {
val name = "${UUID.randomUUID()}.m4a"
zipFile.extractFile(audioFilePath, audioRoot!!.path, name)
audio.name = name
}
} catch (exception: Exception) {
Operations.log(app, exception)
} finally {
importingBackup?.postValue(
ImportProgress(
current.getAndIncrement(),
total,
stage = ImportStage.IMPORT_FILES,
)
)
} }
} catch (exception: Exception) {
Operations.log(app, exception)
} finally {
importingBackup?.postValue(Progress(current.get(), total))
current.getAndIncrement()
} }
} }
}
NotallyDatabase.getDatabase(app) NotallyDatabase.getDatabase(app)
.value .value
.getCommonDao() .getCommonDao()
.importBackup(baseNotes, labels) .importBackup(baseNotes, labels)
} baseNotes.size
}
databaseFolder.clearDirectory() databaseFolder.clearDirectory()
Toast.makeText(app, R.string.imported_backup, Toast.LENGTH_LONG).show() val message = app.getQuantityString(R.plurals.imported_notes, importedNotes)
Toast.makeText(app, message, Toast.LENGTH_LONG).show()
} catch (e: ZipException) { } catch (e: ZipException) {
if (e.type == ZipException.Type.WRONG_PASSWORD) { if (e.type == ZipException.Type.WRONG_PASSWORD) {
Toast.makeText(app, R.string.wrong_password, Toast.LENGTH_LONG).show() Toast.makeText(app, R.string.wrong_password, Toast.LENGTH_LONG).show()
@ -144,7 +168,7 @@ object Import {
Toast.makeText(app, R.string.invalid_backup, Toast.LENGTH_LONG).show() Toast.makeText(app, R.string.invalid_backup, Toast.LENGTH_LONG).show()
Operations.log(app, e) Operations.log(app, e)
} finally { } finally {
importingBackup?.value = Progress(inProgress = false) importingBackup?.value = ImportProgress(inProgress = false)
} }
} }
@ -156,7 +180,7 @@ object Import {
zipFile: ZipFile, zipFile: ZipFile,
current: AtomicInteger, current: AtomicInteger,
total: Int, total: Int,
importingBackup: MutableLiveData<Progress>? = null, importingBackup: MutableLiveData<ImportProgress>? = null,
) { ) {
files.forEach { file -> files.forEach { file ->
try { try {
@ -170,8 +194,13 @@ object Import {
} catch (exception: Exception) { } catch (exception: Exception) {
Operations.log(app, exception) Operations.log(app, exception)
} finally { } finally {
importingBackup?.postValue(Progress(current.get(), total)) importingBackup?.postValue(
current.getAndIncrement() ImportProgress(
current.getAndIncrement(),
total,
stage = ImportStage.IMPORT_FILES,
)
)
} }
} }
} }

View file

@ -112,7 +112,6 @@
<string name="choose_another_folder">Vybrat jinou složku</string> <string name="choose_another_folder">Vybrat jinou složku</string>
<string name="cant_find_folder">Složka nebyla nalezena. Možná byla přesunuta nebo smazána.</string> <string name="cant_find_folder">Složka nebyla nalezena. Možná byla přesunuta nebo smazána.</string>
<string name="invalid_backup">Neplatný soubor zálohy</string> <string name="invalid_backup">Neplatný soubor zálohy</string>
<string name="imported_backup">Záloha importována</string>
<string name="exporting_backup">Vytváří se záloha</string> <string name="exporting_backup">Vytváří se záloha</string>
<string name="importing_backup">Importuje se záloha</string> <string name="importing_backup">Importuje se záloha</string>
<string name="calculating">Probíhá výpočet…</string> <string name="calculating">Probíhá výpočet…</string>

View file

@ -139,7 +139,6 @@
<string name="cant_find_folder">Kann den Ordner nicht finden. Er kann verschoben oder gelöscht worden sein</string> <string name="cant_find_folder">Kann den Ordner nicht finden. Er kann verschoben oder gelöscht worden sein</string>
<string name="invalid_backup">Ungültige Backup</string> <string name="invalid_backup">Ungültige Backup</string>
<string name="google_keep_help">Um deine Notizen aus Google Notizen zu importieren musst du deine Google Takeout ZIP Datei herunterladen\n\nFalls du das Takeout ZIP schon hast, klicke auf Import und wähle es aus.</string> <string name="google_keep_help">Um deine Notizen aus Google Notizen zu importieren musst du deine Google Takeout ZIP Datei herunterladen\n\nFalls du das Takeout ZIP schon hast, klicke auf Import und wähle es aus.</string>
<string name="imported_backup">Backup importiert</string>
<string name="exporting_backup">Backup Exportieren</string> <string name="exporting_backup">Backup Exportieren</string>
<string name="importing_backup">Backup Importieren</string> <string name="importing_backup">Backup Importieren</string>
<string name="calculating">Berechne…</string> <string name="calculating">Berechne…</string>

View file

@ -68,7 +68,6 @@
<!-- Messages --> <!-- Messages -->
<string name="invalid_backup">Copia de seguridad inválida</string> <string name="invalid_backup">Copia de seguridad inválida</string>
<string name="imported_backup">Copia de seguridad importada</string>
<string name="install_an_email">Instale una aplicación de correo electrónico para enviar comentarios</string> <string name="install_an_email">Instale una aplicación de correo electrónico para enviar comentarios</string>
<string name="install_a_browser">Instala un navegador para abrir este enlace</string> <string name="install_a_browser">Instala un navegador para abrir este enlace</string>

View file

@ -112,7 +112,6 @@
<string name="choose_another_folder">Choisir un autre dossier</string> <string name="choose_another_folder">Choisir un autre dossier</string>
<string name="cant_find_folder">Impossible de trouver le dossier. Il a peut-être été déplacé ou supprimé.</string> <string name="cant_find_folder">Impossible de trouver le dossier. Il a peut-être été déplacé ou supprimé.</string>
<string name="invalid_backup">Sauvegarde invalide</string> <string name="invalid_backup">Sauvegarde invalide</string>
<string name="imported_backup">Sauvegarde importée</string>
<string name="exporting_backup">Exportation de la sauvegarde</string> <string name="exporting_backup">Exportation de la sauvegarde</string>
<string name="importing_backup">Importation de la sauvegarde</string> <string name="importing_backup">Importation de la sauvegarde</string>
<string name="calculating">Calcul…</string> <string name="calculating">Calcul…</string>

View file

@ -67,7 +67,6 @@
<string name="empty_list">Daftar kosong</string> <string name="empty_list">Daftar kosong</string>
<string name="cant_open_link">Tidak bisa membuka tautan</string> <string name="cant_open_link">Tidak bisa membuka tautan</string>
<string name="invalid_backup">Cadangan tidak valid</string> <string name="invalid_backup">Cadangan tidak valid</string>
<string name="imported_backup">Cadangan yang diimpor</string>
<string name="something_went_wrong">Telah terjadi kesalahan. Silakan coba lagi.</string> <string name="something_went_wrong">Telah terjadi kesalahan. Silakan coba lagi.</string>
<string name="install_a_browser">Pasang peramban untuk membuka tautan ini</string> <string name="install_a_browser">Pasang peramban untuk membuka tautan ini</string>

View file

@ -68,7 +68,6 @@
<string name="empty_list">Lista vuota</string> <string name="empty_list">Lista vuota</string>
<string name="cant_open_link">Impossibile aprire il link</string> <string name="cant_open_link">Impossibile aprire il link</string>
<string name="invalid_backup">Backup invalido</string> <string name="invalid_backup">Backup invalido</string>
<string name="imported_backup">Backup importato</string>
<string name="something_went_wrong">Qualcosa è andato storto. Per favore riprova.</string> <string name="something_went_wrong">Qualcosa è andato storto. Per favore riprova.</string>
<!-- Settings Page --> <!-- Settings Page -->

View file

@ -71,7 +71,6 @@
<string name="cant_open_link">လင့်ကိုမဖွင့်နိုင်ပါ။</string> <string name="cant_open_link">လင့်ကိုမဖွင့်နိုင်ပါ။</string>
<string name="invalid_image">ရုပ်ပုံမှား​နေသည်။</string> <string name="invalid_image">ရုပ်ပုံမှား​နေသည်။</string>
<string name="invalid_backup">အရန်သိမ်းဖိုင်မှားနေသည်။</string> <string name="invalid_backup">အရန်သိမ်းဖိုင်မှားနေသည်။</string>
<string name="imported_backup">အရန်သိမ်းထားသည်များ သွင်းမည်။</string>
<string name="something_went_wrong">တစ်စုံတစ်ခုမှားယွင်း​နေသည်။ထပ်မံကြိုးစားပါ။</string> <string name="something_went_wrong">တစ်စုံတစ်ခုမှားယွင်း​နေသည်။ထပ်မံကြိုးစားပါ။</string>
<string name="image_format_not_supported">ဤရုပ်ပုံဖိုင်အမျိုးအစားအား လက်မခံပါ။</string> <string name="image_format_not_supported">ဤရုပ်ပုံဖိုင်အမျိုးအစားအား လက်မခံပါ။</string>
<string name="install_an_email">တုံ့ပြန်ချက်​ပေးပို့ရန် အီး​မေးလ်အက်ပ် တစ်ခုအရင်သွင်းပါ</string> <string name="install_an_email">တုံ့ပြန်ချက်​ပေးပို့ရန် အီး​မေးလ်အက်ပ် တစ်ခုအရင်သွင်းပါ</string>

View file

@ -110,7 +110,6 @@
<string name="choose_another_folder">Velg en annen mappe</string> <string name="choose_another_folder">Velg en annen mappe</string>
<string name="cant_find_folder">Finner ikke mappen. Den kan ha blitt flyttet eller slettet</string> <string name="cant_find_folder">Finner ikke mappen. Den kan ha blitt flyttet eller slettet</string>
<string name="invalid_backup">Ugyldig sikkerhetskopi</string> <string name="invalid_backup">Ugyldig sikkerhetskopi</string>
<string name="imported_backup">Importert sikkerhetskopi</string>
<string name="exporting_backup">Eksporterer sikkerhetskopi</string> <string name="exporting_backup">Eksporterer sikkerhetskopi</string>
<string name="importing_backup">Importerer sikkerhetskopi</string> <string name="importing_backup">Importerer sikkerhetskopi</string>
<string name="calculating">Beregner…</string> <string name="calculating">Beregner…</string>

View file

@ -165,7 +165,6 @@
<string name="choose_another_folder">Kies een andere map</string> <string name="choose_another_folder">Kies een andere map</string>
<string name="cant_find_folder">Kan map niet vinden. Deze is mogelijk verplaatst of verwijderd</string> <string name="cant_find_folder">Kan map niet vinden. Deze is mogelijk verplaatst of verwijderd</string>
<string name="invalid_backup">Ongeldige back-up</string> <string name="invalid_backup">Ongeldige back-up</string>
<string name="imported_backup">Geïmporteerde back-up</string>
<string name="exporting_backup">Back-up exporteren</string> <string name="exporting_backup">Back-up exporteren</string>
<string name="importing_backup">Back-up importeren</string> <string name="importing_backup">Back-up importeren</string>
<string name="calculating">Berekenen…</string> <string name="calculating">Berekenen…</string>

View file

@ -110,7 +110,6 @@
<string name="choose_another_folder">Vel ei anna mappe</string> <string name="choose_another_folder">Vel ei anna mappe</string>
<string name="cant_find_folder">Finn ikkje mappa. Den kan ha blitt flytta eller sletta</string> <string name="cant_find_folder">Finn ikkje mappa. Den kan ha blitt flytta eller sletta</string>
<string name="invalid_backup">Ugyldeg sikkerheitskopi</string> <string name="invalid_backup">Ugyldeg sikkerheitskopi</string>
<string name="imported_backup">Importert sikkerheitskopi</string>
<string name="exporting_backup">Eksporterer sikkerheitskopi</string> <string name="exporting_backup">Eksporterer sikkerheitskopi</string>
<string name="importing_backup">Importerer sikkerheitskopi</string> <string name="importing_backup">Importerer sikkerheitskopi</string>
<string name="calculating">Bereknar…</string> <string name="calculating">Bereknar…</string>

View file

@ -112,7 +112,6 @@
<string name="choose_another_folder">Wybierz inny katalog</string> <string name="choose_another_folder">Wybierz inny katalog</string>
<string name="cant_find_folder">Nie można znaleźć katalogu. Być może został przeniesiony lub usunięty</string> <string name="cant_find_folder">Nie można znaleźć katalogu. Być może został przeniesiony lub usunięty</string>
<string name="invalid_backup">Niewłaściwy plik kopii zapasowej</string> <string name="invalid_backup">Niewłaściwy plik kopii zapasowej</string>
<string name="imported_backup">Kopia zapasowa została przywrócona</string>
<string name="exporting_backup">Wykonywanie kopii zapasowej</string> <string name="exporting_backup">Wykonywanie kopii zapasowej</string>
<string name="importing_backup">Przywracanie kopii zapasowej</string> <string name="importing_backup">Przywracanie kopii zapasowej</string>
<string name="calculating">Obliczanie…</string> <string name="calculating">Obliczanie…</string>

View file

@ -72,7 +72,6 @@
<string name="cant_open_link">Nu se poate deschide link-ul</string> <string name="cant_open_link">Nu se poate deschide link-ul</string>
<string name="invalid_image">Imagine invalidă</string> <string name="invalid_image">Imagine invalidă</string>
<string name="invalid_backup">Backup invalid</string> <string name="invalid_backup">Backup invalid</string>
<string name="imported_backup">Backup importat</string>
<string name="something_went_wrong">Ceva a mers nu cum trebuie. Vă rog să încercați din nou.</string> <string name="something_went_wrong">Ceva a mers nu cum trebuie. Vă rog să încercați din nou.</string>
<string name="image_format_not_supported">Formatul imaginii nu este acceptat</string> <string name="image_format_not_supported">Formatul imaginii nu este acceptat</string>
<string name="install_an_email">Instalați o aplicație de e-mail pentru a trimite feedback</string> <string name="install_an_email">Instalați o aplicație de e-mail pentru a trimite feedback</string>

View file

@ -113,7 +113,6 @@
<string name="choose_another_folder">Izberite drugo mapo</string> <string name="choose_another_folder">Izberite drugo mapo</string>
<string name="cant_find_folder">Mapa ne obstaja. Morda je bila premaknjena ali izbrisana.</string> <string name="cant_find_folder">Mapa ne obstaja. Morda je bila premaknjena ali izbrisana.</string>
<string name="invalid_backup">Neveljavna varnostna kopija</string> <string name="invalid_backup">Neveljavna varnostna kopija</string>
<string name="imported_backup">Varnostna kopija je bila uvožena</string>
<string name="exporting_backup">Izvažanje varnostne kopije</string> <string name="exporting_backup">Izvažanje varnostne kopije</string>
<string name="importing_backup">Uvažanje varnostne kopije</string> <string name="importing_backup">Uvažanje varnostne kopije</string>
<string name="calculating">Izračun…</string> <string name="calculating">Izračun…</string>

View file

@ -111,7 +111,6 @@
<string name="choose_another_folder">Chọn thư mục khác</string> <string name="choose_another_folder">Chọn thư mục khác</string>
<string name="cant_find_folder">Không tìm thấy thư mục. Có lẽ nó đã bị đổi tên hoặc xóa</string> <string name="cant_find_folder">Không tìm thấy thư mục. Có lẽ nó đã bị đổi tên hoặc xóa</string>
<string name="invalid_backup">Bản sao lưu không hợp lệ</string> <string name="invalid_backup">Bản sao lưu không hợp lệ</string>
<string name="imported_backup">Đã nhập bản sao lưu</string>
<string name="exporting_backup">Đã xuất bản sao lưu</string> <string name="exporting_backup">Đã xuất bản sao lưu</string>
<string name="calculating">Đang xử lý…</string> <string name="calculating">Đang xử lý…</string>

View file

@ -116,7 +116,6 @@
<string name="choose_another_folder">选择另一个文件夹</string> <string name="choose_another_folder">选择另一个文件夹</string>
<string name="cant_find_folder">找不到该文件夹。可能被移动或删除了。</string> <string name="cant_find_folder">找不到该文件夹。可能被移动或删除了。</string>
<string name="invalid_backup">无效的备份</string> <string name="invalid_backup">无效的备份</string>
<string name="imported_backup">已导入备份</string>
<!-- Widget --> <!-- Widget -->
<string name="single_note_or_list">单条笔记</string> <string name="single_note_or_list">单条笔记</string>

View file

@ -186,9 +186,15 @@
<string name="help">Help</string> <string name="help">Help</string>
<string name="google_keep_help">In order to import your Notes from Google Keep you must download your Google Takeout ZIP file. Click Help to get more information.\n\nIf you already have a Takeout ZIP file, click Import and choose the ZIP file.</string> <string name="google_keep_help">In order to import your Notes from Google Keep you must download your Google Takeout ZIP file. Click Help to get more information.\n\nIf you already have a Takeout ZIP file, click Import and choose the ZIP file.</string>
<string name="evernote_help">In order to import your Notes from Evernote you must export your Evernote Notebook as ENEX. Click Help to get more information.\n\nIf you already have a ENEX file, click Import and choose it.</string> <string name="evernote_help">In order to import your Notes from Evernote you must export your Evernote Notebook as ENEX. Click Help to get more information.\n\nIf you already have a ENEX file, click Import and choose it.</string>
<string name="imported_backup">Imported backup</string> <plurals name="imported_notes">
<item quantity="one">Imported %s Note</item>
<item quantity="other">Imported %s Notes</item>
</plurals>
<string name="exporting_backup">Exporting backup</string> <string name="exporting_backup">Exporting backup</string>
<string name="importing_backup">Importing backup</string> <string name="importing_backup">Importing backup</string>
<string name="extracted_files">Files extracted</string>
<string name="imported_files">Files imported</string>
<string name="imported_notes">Notes imported</string>
<string name="calculating">Calculating…</string> <string name="calculating">Calculating…</string>
<string name="clear_data_message">All Notes, Images, Files, Audios will be permanently deleted</string> <string name="clear_data_message">All Notes, Images, Files, Audios will be permanently deleted</string>

View file

@ -46,7 +46,7 @@ class NotesImporterTest {
private fun testImport(importSource: ImportSource, expectedAmountNotes: Int) { private fun testImport(importSource: ImportSource, expectedAmountNotes: Int) {
val importSourceFile = prepareImportSources(importSource) val importSourceFile = prepareImportSources(importSource)
val importOutputFolder = prepareMediaFolder(importSource) val importOutputFolder = prepareMediaFolder()
runBlocking { runBlocking {
NotesImporter(application, database).import(importSourceFile.toUri(), importSource) NotesImporter(application, database).import(importSourceFile.toUri(), importSource)
@ -108,12 +108,13 @@ class NotesImporterTest {
} }
} }
private fun prepareMediaFolder(importSource: ImportSource): String { private fun prepareMediaFolder(): String {
val dir = val dir =
File.createTempFile("notallyxNotesImporterTest", importSource.folderName).apply { File.createTempFile("notallyxNotesImporterTest", NotesImporter.IMPORT_CACHE_FOLDER)
delete() .apply {
mkdirs() delete()
} mkdirs()
}
val path = dir.toPath().toString() val path = dir.toPath().toString()
ShadowEnvironment.addExternalDir(path) ShadowEnvironment.addExternalDir(path)
println("Output folder: $path") println("Output folder: $path")
@ -121,8 +122,8 @@ class NotesImporterTest {
} }
private fun prepareImportSources(importSource: ImportSource): File { private fun prepareImportSources(importSource: ImportSource): File {
val tempDir = Files.createTempDirectory("imports-${importSource.folderName}").toFile() val tempDir = Files.createTempDirectory("imports-${importSource.name.lowercase()}").toFile()
copyTestFilesToTempDir("imports/${importSource.folderName}", tempDir) copyTestFilesToTempDir("imports/${importSource.name.lowercase()}", tempDir)
println("Input folder: ${tempDir.absolutePath}") println("Input folder: ${tempDir.absolutePath}")
return when (importSource) { return when (importSource) {
ImportSource.GOOGLE_KEEP -> File(tempDir, "Takeout.zip") ImportSource.GOOGLE_KEEP -> File(tempDir, "Takeout.zip")

View file

@ -49,7 +49,7 @@ class GoogleKeepImporterTest {
labels = listOf("Label1", "Label2"), labels = listOf("Label1", "Label2"),
body = "This is some note, nothing special", body = "This is some note, nothing special",
) )
val actual = importer.parseToBaseNote(json) val actual = with(importer) { json.parseToBaseNote() }
assertEquals(expected, actual) assertEquals(expected, actual)
} }
@ -66,7 +66,7 @@ class GoogleKeepImporterTest {
""" """
.trimIndent() .trimIndent()
val actual = importer.parseToBaseNote(json) val actual = with(importer) { json.parseToBaseNote() }
assertThat(actual) assertThat(actual)
.extracting("title", "folder") .extracting("title", "folder")
@ -85,7 +85,7 @@ class GoogleKeepImporterTest {
""" """
.trimIndent() .trimIndent()
val actual = importer.parseToBaseNote(json) val actual = with(importer) { json.parseToBaseNote() }
assertThat(actual) assertThat(actual)
.extracting("title", "folder") .extracting("title", "folder")
@ -103,7 +103,7 @@ class GoogleKeepImporterTest {
""" """
.trimIndent() .trimIndent()
val actual = importer.parseToBaseNote(json) val actual = with(importer) { json.parseToBaseNote() }
assertThat(actual).extracting("title", "pinned").containsExactly("Pinned Note", true) assertThat(actual).extracting("title", "pinned").containsExactly("Pinned Note", true)
} }
@ -128,7 +128,7 @@ class GoogleKeepImporterTest {
""" """
.trimIndent() .trimIndent()
val actual = importer.parseToBaseNote(json) val actual = with(importer) { json.parseToBaseNote() }
assertThat(actual) assertThat(actual)
.extracting("type", "title", "items") .extracting("type", "title", "items")
@ -158,7 +158,7 @@ class GoogleKeepImporterTest {
""" """
.trimIndent() .trimIndent()
val actual = importer.parseToBaseNote(json) val actual = with(importer) { json.parseToBaseNote() }
assertThat(actual) assertThat(actual)
.extracting("title", "images") .extracting("title", "images")
@ -184,7 +184,7 @@ class GoogleKeepImporterTest {
""" """
.trimIndent() .trimIndent()
val actual = importer.parseToBaseNote(json) val actual = with(importer) { json.parseToBaseNote() }
assertThat(actual) assertThat(actual)
.extracting("title", "files") .extracting("title", "files")
@ -210,7 +210,7 @@ class GoogleKeepImporterTest {
""" """
.trimIndent() .trimIndent()
val actual = importer.parseToBaseNote(json) val actual = with(importer) { json.parseToBaseNote() }
assertThat(actual.title).isEqualTo("Audio Note") assertThat(actual.title).isEqualTo("Audio Note")
assertThat(actual.audios[0].name).isEqualTo("audio.3gp") assertThat(actual.audios[0].name).isEqualTo("audio.3gp")