diff --git a/app/build.gradle b/app/build.gradle
index c2f55d37..e21d2d02 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -66,7 +66,7 @@ android {
}
dependencies {
- implementation 'com.github.SimpleMobileTools:Simple-Commons:f737f6c38b'
+ implementation 'com.github.SimpleMobileTools:Simple-Commons:91763fe00f'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6fbba142..29ccdc42 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,10 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{ } }
+ .apply {
+ activity.setupDialogStuff(view, this)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ManageAutoBackupsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ManageAutoBackupsDialog.kt
new file mode 100644
index 00000000..b03cc3e2
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ManageAutoBackupsDialog.kt
@@ -0,0 +1,99 @@
+package com.simplemobiletools.notes.pro.dialogs
+
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import com.simplemobiletools.commons.dialogs.FilePickerDialog
+import com.simplemobiletools.commons.extensions.*
+import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.notes.pro.R
+import com.simplemobiletools.notes.pro.activities.SimpleActivity
+import com.simplemobiletools.notes.pro.extensions.config
+import kotlinx.android.synthetic.main.dialog_manage_automatic_backups.view.backup_notes_filename
+import kotlinx.android.synthetic.main.dialog_manage_automatic_backups.view.backup_notes_filename_hint
+import kotlinx.android.synthetic.main.dialog_manage_automatic_backups.view.backup_notes_folder
+import java.io.File
+
+class ManageAutoBackupsDialog(private val activity: SimpleActivity, onSuccess: () -> Unit) {
+ private val view = (activity.layoutInflater.inflate(R.layout.dialog_manage_automatic_backups, null) as ViewGroup)
+ private val config = activity.config
+ private var backupFolder = config.autoBackupFolder
+
+ init {
+ view.apply {
+ backup_notes_folder.setText(activity.humanizePath(backupFolder))
+ val filename = config.autoBackupFilename.ifEmpty {
+ "${activity.getString(R.string.notes)}_%Y%M%D_%h%m%s"
+ }
+
+ backup_notes_filename.setText(filename)
+ backup_notes_filename_hint.setEndIconOnClickListener {
+ DateTimePatternInfoDialog(activity)
+ }
+
+ backup_notes_filename_hint.setEndIconOnLongClickListener {
+ DateTimePatternInfoDialog(activity)
+ true
+ }
+
+ backup_notes_folder.setOnClickListener {
+ selectBackupFolder()
+ }
+ }
+
+ activity.getAlertDialogBuilder()
+ .setPositiveButton(R.string.ok, null)
+ .setNegativeButton(R.string.cancel, null)
+ .apply {
+ activity.setupDialogStuff(view, this, R.string.manage_automatic_backups) { dialog ->
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
+ val filename = view.backup_notes_filename.value
+ when {
+ filename.isEmpty() -> activity.toast(R.string.empty_name)
+ filename.isAValidFilename() -> {
+ val file = File(backupFolder, "$filename.json")
+ if (file.exists() && !file.canWrite()) {
+ activity.toast(R.string.name_taken)
+ return@setOnClickListener
+ }
+
+ ensureBackgroundThread {
+ config.apply {
+ autoBackupFolder = backupFolder
+ autoBackupFilename = filename
+ }
+
+ activity.runOnUiThread {
+ onSuccess()
+ }
+
+ dialog.dismiss()
+ }
+ }
+
+ else -> activity.toast(R.string.invalid_name)
+ }
+ }
+ }
+ }
+ }
+
+ private fun selectBackupFolder() {
+ activity.hideKeyboard(view.backup_notes_filename)
+ FilePickerDialog(activity, backupFolder, false, showFAB = true) { path ->
+ activity.handleSAFDialog(path) { grantedSAF ->
+ if (!grantedSAF) {
+ return@handleSAFDialog
+ }
+
+ activity.handleSAFDialogSdk30(path) { grantedSAF30 ->
+ if (!grantedSAF30) {
+ return@handleSAFDialogSdk30
+ }
+
+ backupFolder = path
+ view.backup_notes_folder.setText(activity.humanizePath(path))
+ }
+ }
+ }
+ }
+}
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 554728ef..38792a57 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
@@ -1,18 +1,28 @@
package com.simplemobiletools.notes.pro.extensions
+import android.app.AlarmManager
+import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import androidx.core.app.AlarmManagerCompat
import com.simplemobiletools.commons.activities.BaseSimpleActivity
+import com.simplemobiletools.commons.extensions.*
+import com.simplemobiletools.commons.helpers.ExportResult
+import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.commons.helpers.isRPlus
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.helpers.*
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.receivers.AutomaticBackupReceiver
+import org.joda.time.DateTime
+import java.io.File
+import java.io.FileOutputStream
val Context.config: Config get() = Config.newInstance(applicationContext)
@@ -43,3 +53,111 @@ fun BaseSimpleActivity.requestUnlockNotes(notes: List, callback: (unlocked
callback(emptyList())
}
}
+
+fun Context.getAutomaticBackupIntent(): PendingIntent {
+ val intent = Intent(this, AutomaticBackupReceiver::class.java)
+ return PendingIntent.getBroadcast(this, AUTOMATIC_BACKUP_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+}
+
+fun Context.scheduleNextAutomaticBackup() {
+ if (config.autoBackup) {
+ val backupAtMillis = getNextAutoBackupTime().millis
+ val pendingIntent = getAutomaticBackupIntent()
+ val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ try {
+ AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, backupAtMillis, pendingIntent)
+ } catch (e: Exception) {
+ showErrorToast(e)
+ }
+ }
+}
+
+fun Context.cancelScheduledAutomaticBackup() {
+ val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ alarmManager.cancel(getAutomaticBackupIntent())
+}
+
+fun Context.checkAndBackupNotesOnBoot() {
+ if (config.autoBackup) {
+ val previousRealBackupTime = config.lastAutoBackupTime
+ val previousScheduledBackupTime = getPreviousAutoBackupTime().millis
+ val missedPreviousBackup = previousRealBackupTime < previousScheduledBackupTime
+ if (missedPreviousBackup) {
+ // device was probably off at the scheduled time so backup now
+ backupNotes()
+ }
+ }
+}
+
+fun Context.backupNotes() {
+ require(isRPlus())
+ ensureBackgroundThread {
+ val config = config
+ NotesHelper(this).getNotes { notesToBackup ->
+ if (notesToBackup.isEmpty()) {
+ toast(R.string.no_entries_for_exporting)
+ config.lastAutoBackupTime = DateTime.now().millis
+ scheduleNextAutomaticBackup()
+ return@getNotes
+ }
+
+
+ val now = DateTime.now()
+ val year = now.year.toString()
+ val month = now.monthOfYear.ensureTwoDigits()
+ val day = now.dayOfMonth.ensureTwoDigits()
+ val hours = now.hourOfDay.ensureTwoDigits()
+ val minutes = now.minuteOfHour.ensureTwoDigits()
+ val seconds = now.secondOfMinute.ensureTwoDigits()
+
+ val filename = config.autoBackupFilename
+ .replace("%Y", year, false)
+ .replace("%M", month, false)
+ .replace("%D", day, false)
+ .replace("%h", hours, false)
+ .replace("%m", minutes, false)
+ .replace("%s", seconds, false)
+
+ val outputFolder = File(config.autoBackupFolder).apply {
+ mkdirs()
+ }
+
+ var exportFile = File(outputFolder, "$filename.json")
+ var exportFilePath = exportFile.absolutePath
+ val outputStream = try {
+ if (hasProperStoredFirstParentUri(exportFilePath)) {
+ val exportFileUri = createDocumentUriUsingFirstParentTreeUri(exportFilePath)
+ if (!getDoesFilePathExist(exportFilePath)) {
+ createSAFFileSdk30(exportFilePath)
+ }
+ applicationContext.contentResolver.openOutputStream(exportFileUri, "wt") ?: FileOutputStream(exportFile)
+ } else {
+ var num = 0
+ while (getDoesFilePathExist(exportFilePath) && !exportFile.canWrite()) {
+ num++
+ exportFile = File(outputFolder, "${filename}_${num}.json")
+ exportFilePath = exportFile.absolutePath
+ }
+ FileOutputStream(exportFile)
+ }
+ } catch (e: Exception) {
+ showErrorToast(e)
+ scheduleNextAutomaticBackup()
+ return@getNotes
+ }
+
+ val exportResult = try {
+ NotesHelper(this).exportNotes(notesToBackup, outputStream)
+ } catch (e: Exception) {
+ showErrorToast(e)
+ }
+
+ if (exportResult == ExportResult.EXPORT_FAIL) {
+ toast(R.string.exporting_failed)
+ }
+
+ config.lastAutoBackupTime = DateTime.now().millis
+ scheduleNextAutomaticBackup()
+ }
+ }
+}
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 5d7e2b19..a61b28d5 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
@@ -1,6 +1,7 @@
package com.simplemobiletools.notes.pro.helpers
import android.graphics.Color
+import org.joda.time.DateTime
const val NOTE_ID = "note_id"
const val OPEN_NOTE_ID = "open_note_id"
@@ -39,6 +40,26 @@ const val FONT_SIZE_PERCENTAGE = "font_size_percentage"
const val EXPORT_MIME_TYPE = "text/plain"
const val ADD_NEW_CHECKLIST_ITEMS_TOP = "add_new_checklist_items_top"
+// auto backups
+const val AUTOMATIC_BACKUP_REQUEST_CODE = 10001
+const val AUTO_BACKUP_INTERVAL_IN_DAYS = 1
+
+// 6 am is the hardcoded automatic backup time, intervals shorter than 1 day are not yet supported.
+fun getNextAutoBackupTime(): DateTime {
+ val now = DateTime.now()
+ val sixHour = now.withHourOfDay(6)
+ return if (now.millis < sixHour.millis) {
+ sixHour
+ } else {
+ sixHour.plusDays(AUTO_BACKUP_INTERVAL_IN_DAYS)
+ }
+}
+
+fun getPreviousAutoBackupTime(): DateTime {
+ val nextBackupTime = getNextAutoBackupTime()
+ return nextBackupTime.minusDays(AUTO_BACKUP_INTERVAL_IN_DAYS)
+}
+
// gravity
const val GRAVITY_START = 0
const val GRAVITY_CENTER = 1
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 bf25adb5..53fb7227 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
@@ -4,6 +4,7 @@ import android.content.Context
import android.os.Handler
import android.os.Looper
import com.simplemobiletools.commons.activities.BaseSimpleActivity
+import com.simplemobiletools.commons.helpers.ExportResult
import com.simplemobiletools.commons.helpers.PROTECTION_NONE
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.notes.pro.R
@@ -11,7 +12,10 @@ 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 kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
import java.io.File
+import java.io.OutputStream
class NotesHelper(val context: Context) {
fun getNotes(callback: (notes: List) -> Unit) {
@@ -124,6 +128,18 @@ class NotesHelper(val context: Context) {
}
}
+ fun exportNotes(notesToBackup: List, outputStream: OutputStream): ExportResult {
+ return try {
+ val jsonString = Json.encodeToString(notesToBackup)
+ outputStream.use {
+ it.write(jsonString.toByteArray())
+ }
+ ExportResult.EXPORT_OK
+ } catch (_: Error) {
+ ExportResult.EXPORT_FAIL
+ }
+ }
+
enum class ImportResult {
IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL, IMPORT_NOTHING_NEW
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/AutomaticBackupReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/AutomaticBackupReceiver.kt
new file mode 100644
index 00000000..7d4949be
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/AutomaticBackupReceiver.kt
@@ -0,0 +1,17 @@
+package com.simplemobiletools.notes.pro.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.PowerManager
+import com.simplemobiletools.notes.pro.extensions.backupNotes
+
+class AutomaticBackupReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+ val wakelock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "simplenotes:automaticbackupreceiver")
+ wakelock.acquire(3000)
+ context.backupNotes()
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/BootCompletedReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/BootCompletedReceiver.kt
new file mode 100644
index 00000000..8cc03250
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/BootCompletedReceiver.kt
@@ -0,0 +1,18 @@
+package com.simplemobiletools.notes.pro.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.notes.pro.extensions.checkAndBackupNotesOnBoot
+
+class BootCompletedReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ ensureBackgroundThread {
+ context.apply {
+ checkAndBackupNotesOnBoot()
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 93fc0d18..857ee1ec 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -386,6 +386,47 @@
android:text="@string/import_notes" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/datetime_pattern_info_layout.xml b/app/src/main/res/layout/datetime_pattern_info_layout.xml
new file mode 100644
index 00000000..36ee34d1
--- /dev/null
+++ b/app/src/main/res/layout/datetime_pattern_info_layout.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/app/src/main/res/layout/dialog_manage_automatic_backups.xml b/app/src/main/res/layout/dialog_manage_automatic_backups.xml
new file mode 100644
index 00000000..9aef64b4
--- /dev/null
+++ b/app/src/main/res/layout/dialog_manage_automatic_backups.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+