package com.vgmlr.wedge
import android.content.Context
import java.io.File
import android.net.Uri
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import kotlinx.coroutines.flow.first
import java.util.concurrent.TimeUnit
object WedgeBackup {
private const val DIR = "backups"
private const val FILE = "wedge.bk"
private const val MIME = "application/octet-stream"
const val WORK_NAME = "WedgeBackupJob"
suspend fun run(context: Context) {
val db = AppDatabase.getInstance(context)
val prefs = PreferenceManager(context)
val note = db.noteDao().getNoteSync()
var content = note?.content ?: ""
val phrase = prefs.backupPhrase.first()
if (prefs.backupEncrypt.first() && phrase.isNotEmpty() && !WedgeSecurity.isEncrypted(content)) {
content = WedgeSecurity.encrypt(content, phrase)
}
val location = prefs.backupLocation.first()
if (location.isNotEmpty()) {
writeToTree(context, location.toUri(), content)
} else {
val folder = File(context.getExternalFilesDir(null), DIR)
if (!folder.exists()) folder.mkdirs()
File(folder, FILE).writeText(content)
}
prefs.setBackupStamp(System.currentTimeMillis())
}
suspend fun restore(context: Context): Boolean {
val prefs = PreferenceManager(context)
var content = read(context, prefs.backupLocation.first()) ?: return false
if (WedgeSecurity.isEncrypted(content)) {
val phrase = prefs.backupPhrase.first()
if (phrase.isEmpty()) return false
content = WedgeSecurity.decrypt(content, phrase) ?: return false
}
AppDatabase.getInstance(context).noteDao().save(NoteEntity(id = 1, content = content))
NoteWidgetProvider.triggerUpdate(context)
return true
}
fun schedule(context: Context, minutes: Int) {
if (minutes <= WedgeConfig.BACKUP_INTERVAL_NONE) {
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
return
}
val constraints = Constraints.Builder().setRequiresStorageNotLow(true).build()
val request = PeriodicWorkRequestBuilder<WedgeBackupWorker>(
minutes.toLong().coerceAtLeast(15L), TimeUnit.MINUTES
).setConstraints(constraints).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, request
)
}
private fun resolveBackupFile(dir: DocumentFile, create: Boolean): DocumentFile? {
val matches = dir.listFiles().filter { f ->
val n = f.name
f.isFile && n != null &&
(n == FILE || n.startsWith("$FILE.") || n.startsWith("$FILE ("))
}.sortedByDescending { it.lastModified() }
matches.drop(1).forEach { it.delete() }
return matches.firstOrNull() ?: if (create) dir.createFile(MIME, FILE) else null
}
private fun writeToTree(context: Context, tree: Uri, content: String) {
val dir = DocumentFile.fromTreeUri(context, tree) ?: return
val file = resolveBackupFile(dir, create = true) ?: return
context.contentResolver.openOutputStream(file.uri, "wt")?.use {
it.write(content.toByteArray(Charsets.UTF_8))
}
}
private fun read(context: Context, location: String): String? = try {
if (location.isNotEmpty()) {
val dir = DocumentFile.fromTreeUri(context, location.toUri())
val file = dir?.let { resolveBackupFile(it, create = false) }
file?.let {
context.contentResolver.openInputStream(it.uri)
?.bufferedReader()?.use { r -> r.readText() }
}
} else {
val f = File(File(context.getExternalFilesDir(null), DIR), FILE)
if (f.exists()) f.readText() else null
}
} catch (_: Exception) {
null
}
}