diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9ca3096ea..5eca4365f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -283,9 +283,14 @@ + + + , outputStream: OutputStream?) { ensureBackgroundThread { - val events = eventsHelper.getEventsToExport(eventTypes) + val events = eventsHelper.getEventsToExport(eventTypes, config.exportEvents, config.exportTasks, config.exportPastEntries) if (events.isEmpty()) { toast(R.string.no_entries_for_exporting) } else { diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/activities/SettingsActivity.kt index d0fda2ba8..21ef9d51e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/activities/SettingsActivity.kt @@ -11,9 +11,10 @@ import android.widget.Toast import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat import com.simplemobiletools.calendar.pro.R +import com.simplemobiletools.calendar.pro.dialogs.ManageAutomaticBackupsDialog import com.simplemobiletools.calendar.pro.dialogs.SelectCalendarsDialog import com.simplemobiletools.calendar.pro.dialogs.SelectEventTypeDialog -import com.simplemobiletools.calendar.pro.dialogs.SelectQuickFilterEventTypesDialog +import com.simplemobiletools.calendar.pro.dialogs.SelectEventTypesDialog import com.simplemobiletools.calendar.pro.extensions.* import com.simplemobiletools.calendar.pro.helpers.* import com.simplemobiletools.calendar.pro.helpers.Formatter @@ -99,6 +100,8 @@ class SettingsActivity : SimpleActivity() { setupAllowChangingTimeZones() updateTextColors(settings_holder) checkPrimaryColor() + setupEnableAutomaticBackups() + setupManageAutomaticBackups() setupExportSettings() setupImportSettings() @@ -114,6 +117,7 @@ class SettingsActivity : SimpleActivity() { settings_widgets_label, settings_events_label, settings_tasks_label, + settings_backups_label, settings_migrating_label ).forEach { it.setTextColor(getProperPrimaryColor()) @@ -334,7 +338,9 @@ class SettingsActivity : SimpleActivity() { } private fun showQuickFilterPicker() { - SelectQuickFilterEventTypesDialog(this) + SelectEventTypesDialog(this, config.quickFilterEventTypes) { + config.quickFilterEventTypes = it + } } private fun setupSundayFirst() { @@ -830,6 +836,46 @@ class SettingsActivity : SimpleActivity() { } } + private fun setupEnableAutomaticBackups() { + settings_backups_label.beVisibleIf(isRPlus()) + settings_backups_divider.beVisibleIf(isRPlus()) + settings_enable_automatic_backups_holder.beVisibleIf(isRPlus()) + settings_enable_automatic_backups.isChecked = config.autoBackup + settings_enable_automatic_backups_holder.setOnClickListener { + val wasBackupDisabled = !config.autoBackup + if (wasBackupDisabled) { + ManageAutomaticBackupsDialog( + activity = this, + onSuccess = { + enableOrDisableAutomaticBackups(true) + scheduleNextAutomaticBackup() + } + ) + } else { + cancelScheduledAutomaticBackup() + enableOrDisableAutomaticBackups(false) + } + } + } + + private fun setupManageAutomaticBackups() { + settings_manage_automatic_backups_holder.beVisibleIf(isRPlus() && config.autoBackup) + settings_manage_automatic_backups_holder.setOnClickListener { + ManageAutomaticBackupsDialog( + activity = this, + onSuccess = { + scheduleNextAutomaticBackup() + } + ) + } + } + + private fun enableOrDisableAutomaticBackups(enable: Boolean) { + config.autoBackup = enable + settings_enable_automatic_backups.isChecked = enable + settings_manage_automatic_backups_holder.beVisibleIf(enable) + } + private fun setupExportSettings() { settings_export_holder.setOnClickListener { val configItems = LinkedHashMap().apply { diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/DateTimePatternInfoDialog.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/DateTimePatternInfoDialog.kt new file mode 100644 index 000000000..a49859a09 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/DateTimePatternInfoDialog.kt @@ -0,0 +1,18 @@ +package com.simplemobiletools.calendar.pro.dialogs + +import com.simplemobiletools.calendar.pro.R +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.getAlertDialogBuilder +import com.simplemobiletools.commons.extensions.setupDialogStuff + +class DateTimePatternInfoDialog(activity: BaseSimpleActivity) { + + init { + val view = activity.layoutInflater.inflate(R.layout.datetime_pattern_info_layout, null) + activity.getAlertDialogBuilder() + .setPositiveButton(R.string.ok) { _, _ -> { } } + .apply { + activity.setupDialogStuff(view, this) + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/ManageAutomaticBackupsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/ManageAutomaticBackupsDialog.kt new file mode 100644 index 000000000..a744f291e --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/ManageAutomaticBackupsDialog.kt @@ -0,0 +1,131 @@ +package com.simplemobiletools.calendar.pro.dialogs + +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import com.simplemobiletools.calendar.pro.R +import com.simplemobiletools.calendar.pro.activities.SimpleActivity +import com.simplemobiletools.calendar.pro.extensions.config +import com.simplemobiletools.commons.dialogs.FilePickerDialog +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.ensureBackgroundThread +import kotlinx.android.synthetic.main.dialog_manage_automatic_backups.view.* +import java.io.File + +class ManageAutomaticBackupsDialog(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 + private var selectedEventTypes = config.autoBackupEventTypes.ifEmpty { config.displayEventTypes } + + init { + view.apply { + backup_events_folder.setText(activity.humanizePath(backupFolder)) + val filename = config.autoBackupFilename.ifEmpty { + "${activity.getString(R.string.events)}_%Y%M%D_%h%m%s" + } + + backup_events_filename.setText(filename) + backup_events_filename_hint.setEndIconOnClickListener { + DateTimePatternInfoDialog(activity) + } + + backup_events_filename_hint.setEndIconOnLongClickListener { + DateTimePatternInfoDialog(activity) + true + } + + backup_events_checkbox.isChecked = config.autoBackupEvents + backup_events_checkbox_holder.setOnClickListener { + backup_events_checkbox.toggle() + } + + backup_tasks_checkbox.isChecked = config.autoBackupTasks + backup_tasks_checkbox_holder.setOnClickListener { + backup_tasks_checkbox.toggle() + } + + backup_past_events_checkbox.isChecked = config.autoBackupPastEntries + backup_past_events_checkbox_holder.setOnClickListener { + backup_past_events_checkbox.toggle() + } + + backup_events_folder.setOnClickListener { + selectBackupFolder() + } + + manage_event_types_holder.setOnClickListener { + SelectEventTypesDialog(activity, selectedEventTypes) { + selectedEventTypes = it + } + } + } + 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_events_filename.value + when { + filename.isEmpty() -> activity.toast(R.string.empty_name) + filename.isAValidFilename() -> { + val file = File(backupFolder, "$filename.ics") + if (file.exists() && !file.canWrite()) { + activity.toast(R.string.name_taken) + return@setOnClickListener + } + + val backupEventsChecked = view.backup_events_checkbox.isChecked + val backupTasksChecked = view.backup_tasks_checkbox.isChecked + if (!backupEventsChecked && !backupTasksChecked || selectedEventTypes.isEmpty()) { + activity.toast(R.string.no_entries_for_exporting) + return@setOnClickListener + } + + ensureBackgroundThread { + config.apply { + autoBackupFolder = backupFolder + autoBackupFilename = filename + autoBackupEvents = backupEventsChecked + autoBackupTasks = backupTasksChecked + autoBackupPastEntries = view.backup_past_events_checkbox.isChecked + if (autoBackupEventTypes != selectedEventTypes) { + autoBackupEventTypes = selectedEventTypes + } + } + + activity.runOnUiThread { + onSuccess() + } + + dialog.dismiss() + } + } + else -> activity.toast(R.string.invalid_name) + } + } + } + } + } + + private fun selectBackupFolder() { + activity.hideKeyboard(view.backup_events_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_events_folder.setText(activity.humanizePath(path)) + } + } + } + } +} + diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/FilterEventTypesDialog.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventTypesDialog.kt similarity index 64% rename from app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/FilterEventTypesDialog.kt rename to app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventTypesDialog.kt index 8e8f6da7f..e69a32fae 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/FilterEventTypesDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectEventTypesDialog.kt @@ -4,23 +4,21 @@ import androidx.appcompat.app.AlertDialog import com.simplemobiletools.calendar.pro.R import com.simplemobiletools.calendar.pro.activities.SimpleActivity import com.simplemobiletools.calendar.pro.adapters.FilterEventTypeAdapter -import com.simplemobiletools.calendar.pro.extensions.config import com.simplemobiletools.calendar.pro.extensions.eventsHelper import com.simplemobiletools.commons.extensions.getAlertDialogBuilder import com.simplemobiletools.commons.extensions.setupDialogStuff import kotlinx.android.synthetic.main.dialog_filter_event_types.view.* -class FilterEventTypesDialog(val activity: SimpleActivity, val callback: () -> Unit) { +class SelectEventTypesDialog(val activity: SimpleActivity, selectedEventTypes: Set, val callback: (HashSet) -> Unit) { private var dialog: AlertDialog? = null private val view = activity.layoutInflater.inflate(R.layout.dialog_filter_event_types, null) init { activity.eventsHelper.getEventTypes(activity, false) { - val displayEventTypes = activity.config.displayEventTypes - view.filter_event_types_list.adapter = FilterEventTypeAdapter(activity, it, displayEventTypes) + view.filter_event_types_list.adapter = FilterEventTypeAdapter(activity, it, selectedEventTypes) activity.getAlertDialogBuilder() - .setPositiveButton(R.string.ok) { dialogInterface, i -> confirmEventTypes() } + .setPositiveButton(R.string.ok) { _, _ -> confirmEventTypes() } .setNegativeButton(R.string.cancel, null) .apply { activity.setupDialogStuff(view, this) { alertDialog -> @@ -31,11 +29,11 @@ class FilterEventTypesDialog(val activity: SimpleActivity, val callback: () -> U } private fun confirmEventTypes() { - val selectedItems = (view.filter_event_types_list.adapter as FilterEventTypeAdapter).getSelectedItemsList().map { it.toString() }.toHashSet() - if (activity.config.displayEventTypes != selectedItems) { - activity.config.displayEventTypes = selectedItems - callback() - } + val adapter = view.filter_event_types_list.adapter as FilterEventTypeAdapter + val selectedItems = adapter.getSelectedItemsList() + .map { it.toString() } + .toHashSet() + callback(selectedItems) dialog?.dismiss() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectQuickFilterEventTypesDialog.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectQuickFilterEventTypesDialog.kt deleted file mode 100644 index 17c29a8dd..000000000 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/dialogs/SelectQuickFilterEventTypesDialog.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.simplemobiletools.calendar.pro.dialogs - -import androidx.appcompat.app.AlertDialog -import com.simplemobiletools.calendar.pro.R -import com.simplemobiletools.calendar.pro.activities.SimpleActivity -import com.simplemobiletools.calendar.pro.adapters.FilterEventTypeAdapter -import com.simplemobiletools.calendar.pro.extensions.config -import com.simplemobiletools.calendar.pro.extensions.eventsHelper -import com.simplemobiletools.commons.extensions.getAlertDialogBuilder -import com.simplemobiletools.commons.extensions.setupDialogStuff -import kotlinx.android.synthetic.main.dialog_filter_event_types.view.* - -class SelectQuickFilterEventTypesDialog(val activity: SimpleActivity) { - private var dialog: AlertDialog? = null - private val view = activity.layoutInflater.inflate(R.layout.dialog_filter_event_types, null) - - init { - activity.eventsHelper.getEventTypes(activity, false) { - val quickFilterEventTypes = activity.config.quickFilterEventTypes - view.filter_event_types_list.adapter = FilterEventTypeAdapter(activity, it, quickFilterEventTypes) - - activity.getAlertDialogBuilder() - .setPositiveButton(R.string.ok) { dialogInterface, i -> confirmEventTypes() } - .setNegativeButton(R.string.cancel, null) - .apply { - activity.setupDialogStuff(view, this) { alertDialog -> - dialog = alertDialog - } - } - } - } - - private fun confirmEventTypes() { - val selectedItems = (view.filter_event_types_list.adapter as FilterEventTypeAdapter).getSelectedItemsList().map { - it.toString() - }.toHashSet() - - if (activity.config.quickFilterEventTypes != selectedItems) { - activity.config.quickFilterEventTypes = selectedItems - } - dialog?.dismiss() - } -} diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/extensions/Context.kt index 36538d89f..de9e5ad25 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/extensions/Context.kt @@ -13,6 +13,7 @@ import android.content.res.Resources import android.database.Cursor import android.graphics.Bitmap import android.media.AudioAttributes +import android.media.MediaScannerConnection import android.net.Uri import android.os.Bundle import android.provider.CalendarContract @@ -37,6 +38,7 @@ import com.simplemobiletools.calendar.pro.interfaces.EventsDao import com.simplemobiletools.calendar.pro.interfaces.TasksDao import com.simplemobiletools.calendar.pro.interfaces.WidgetsDao import com.simplemobiletools.calendar.pro.models.* +import com.simplemobiletools.calendar.pro.receivers.AutomaticBackupReceiver import com.simplemobiletools.calendar.pro.receivers.CalDAVSyncReceiver import com.simplemobiletools.calendar.pro.receivers.NotificationReceiver import com.simplemobiletools.calendar.pro.services.MarkCompletedService @@ -47,6 +49,7 @@ import kotlinx.android.synthetic.main.day_monthly_event_view.view.* import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.LocalDate +import java.io.File import java.util.* val Context.config: Config get() = Config.newInstance(applicationContext) @@ -183,6 +186,103 @@ fun Context.cancelPendingIntent(id: Long) { PendingIntent.getBroadcast(this, id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE).cancel() } +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.checkAndBackupEventsOnBoot() { + if (config.autoBackup) { + val previousRealBackupTime = config.lastAutoBackupTime + val previousScheduledBackupTime = getPreviousAutoBackupTime().seconds() + val missedPreviousBackup = previousRealBackupTime < previousScheduledBackupTime + if (missedPreviousBackup) { + // device was probably off at the scheduled time so backup now + backupEventsAndTasks() + } + } +} + +fun Context.backupEventsAndTasks() { + ensureBackgroundThread { + val config = config + val events = eventsHelper.getEventsToExport( + eventTypes = config.autoBackupEventTypes.map { it.toLong() } as ArrayList, + exportEvents = config.autoBackupEvents, + exportTasks = config.autoBackupTasks, + exportPastEntries = config.autoBackupPastEntries + ) + if (events.isEmpty()) { + toast(R.string.no_entries_for_exporting) + config.lastAutoBackupTime = getNowSeconds() + scheduleNextAutomaticBackup() + return@ensureBackgroundThread + } + + 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() + } + + val exportFile = File(outputFolder, "$filename.ics") + val outputStream = try { + exportFile.outputStream() + } catch (e: Exception) { + showErrorToast(e) + null + } + + IcsExporter(this).exportEvents(outputStream, events, showExportingToast = false) { result -> + when (result) { + IcsExporter.ExportResult.EXPORT_PARTIAL -> toast(R.string.exporting_some_entries_failed) + IcsExporter.ExportResult.EXPORT_FAIL -> toast(R.string.exporting_failed) + else -> {} + } + MediaScannerConnection.scanFile( + this, + arrayOf(exportFile.absolutePath), + arrayOf(exportFile.getMimeType()) + ) { _, _ -> } + + config.lastAutoBackupTime = getNowSeconds() + } + scheduleNextAutomaticBackup() + } +} + fun Context.getRepetitionText(seconds: Int) = when (seconds) { 0 -> getString(R.string.no_repetition) DAY -> getString(R.string.daily) diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/Config.kt index 87726d5ea..09cfdafde 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/Config.kt @@ -3,6 +3,8 @@ package com.simplemobiletools.calendar.pro.helpers import android.content.Context import android.media.AudioManager import android.media.RingtoneManager +import android.os.Environment +import android.os.Environment.DIRECTORY_DOWNLOADS import com.simplemobiletools.calendar.pro.R import com.simplemobiletools.calendar.pro.extensions.config import com.simplemobiletools.calendar.pro.extensions.scheduleCalDAVSync @@ -258,4 +260,36 @@ class Config(context: Context) : BaseConfig(context) { var wasFilteredOutWarningShown: Boolean get() = prefs.getBoolean(WAS_FILTERED_OUT_WARNING_SHOWN, false) set(wasFilteredOutWarningShown) = prefs.edit().putBoolean(WAS_FILTERED_OUT_WARNING_SHOWN, wasFilteredOutWarningShown).apply() + + var autoBackup: Boolean + get() = prefs.getBoolean(AUTO_BACKUP, false) + set(enableAutomaticBackups) = prefs.edit().putBoolean(AUTO_BACKUP, enableAutomaticBackups).apply() + + var autoBackupFolder: String + get() = prefs.getString(AUTO_BACKUP_FOLDER, Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS).absolutePath)!! + set(autoBackupPath) = prefs.edit().putString(AUTO_BACKUP_FOLDER, autoBackupPath).apply() + + var autoBackupFilename: String + get() = prefs.getString(AUTO_BACKUP_FILENAME, "")!! + set(autoBackupFilename) = prefs.edit().putString(AUTO_BACKUP_FILENAME, autoBackupFilename).apply() + + var autoBackupEventTypes: Set + get() = prefs.getStringSet(AUTO_BACKUP_EVENT_TYPES, HashSet())!! + set(autoBackupEventTypes) = prefs.edit().remove(AUTO_BACKUP_EVENT_TYPES).putStringSet(AUTO_BACKUP_EVENT_TYPES, autoBackupEventTypes).apply() + + var autoBackupEvents: Boolean + get() = prefs.getBoolean(AUTO_BACKUP_EVENTS, true) + set(autoBackupEvents) = prefs.edit().putBoolean(AUTO_BACKUP_EVENTS, autoBackupEvents).apply() + + var autoBackupTasks: Boolean + get() = prefs.getBoolean(AUTO_BACKUP_TASKS, true) + set(autoBackupTasks) = prefs.edit().putBoolean(AUTO_BACKUP_TASKS, autoBackupTasks).apply() + + var autoBackupPastEntries: Boolean + get() = prefs.getBoolean(AUTO_BACKUP_PAST_ENTRIES, true) + set(autoBackupPastEntries) = prefs.edit().putBoolean(AUTO_BACKUP_PAST_ENTRIES, autoBackupPastEntries).apply() + + var lastAutoBackupTime: Long + get() = prefs.getLong(LAST_AUTO_BACKUP_TIME, 0L) + set(lastAutoBackupTime) = prefs.edit().putLong(LAST_AUTO_BACKUP_TIME, lastAutoBackupTime).apply() } diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/Constants.kt index 195a98df0..ef5af39d1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/Constants.kt @@ -3,12 +3,14 @@ package com.simplemobiletools.calendar.pro.helpers import com.simplemobiletools.calendar.pro.activities.EventActivity import com.simplemobiletools.calendar.pro.activities.TaskActivity import com.simplemobiletools.commons.helpers.MONTH_SECONDS +import org.joda.time.DateTime import java.util.* const val STORED_LOCALLY_ONLY = 0 const val ROW_COUNT = 6 const val COLUMN_COUNT = 7 const val SCHEDULE_CALDAV_REQUEST_CODE = 10000 +const val AUTOMATIC_BACKUP_REQUEST_CODE = 10001 const val FETCH_INTERVAL = 3 * MONTH_SECONDS const val MAX_SEARCH_YEAR = 2051218800L // 2035, limit search results for events repeating indefinitely @@ -72,6 +74,8 @@ const val YEAR = 31536000 const val EVENT_PERIOD_TODAY = -1 const val EVENT_PERIOD_CUSTOM = -2 +const val AUTO_BACKUP_INTERVAL_IN_DAYS = 1 + const val EVENT_LIST_PERIOD = "event_list_period" // Shared Preferences @@ -129,6 +133,14 @@ const val HIGHLIGHT_WEEKENDS_COLOR = "highlight_weekends_color" const val LAST_USED_EVENT_SPAN = "last_used_event_span" const val ALLOW_CREATING_TASKS = "allow_creating_tasks" const val WAS_FILTERED_OUT_WARNING_SHOWN = "was_filtered_out_warning_shown" +const val AUTO_BACKUP = "auto_backup" +const val AUTO_BACKUP_FOLDER = "auto_backup_folder" +const val AUTO_BACKUP_FILENAME = "auto_backup_filename" +const val AUTO_BACKUP_EVENT_TYPES = "auto_backup_event_types" +const val AUTO_BACKUP_EVENTS = "auto_backup_events" +const val AUTO_BACKUP_TASKS = "auto_backup_tasks" +const val AUTO_BACKUP_PAST_ENTRIES = "auto_backup_past_entries" +const val LAST_AUTO_BACKUP_TIME = "last_auto_backup_time" // repeat_rule for monthly and yearly repetition const val REPEAT_SAME_DAY = 1 // i.e. 25th every month, or 3rd june (if yearly repetition) @@ -264,3 +276,19 @@ fun getActivityToOpen(isTask: Boolean) = if (isTask) { fun generateImportId(): String { return UUID.randomUUID().toString().replace("-", "") + System.currentTimeMillis().toString() } + +// 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) +} diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/EventsHelper.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/EventsHelper.kt index bcd00625e..31c635149 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/EventsHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/EventsHelper.kt @@ -519,22 +519,22 @@ class EventsHelper(val context: Context) { return events } - fun getEventsToExport(eventTypes: ArrayList): ArrayList { + fun getEventsToExport(eventTypes: ArrayList, exportEvents: Boolean, exportTasks: Boolean, exportPastEntries: Boolean): ArrayList { val currTS = getNowSeconds() var events = ArrayList() val tasks = ArrayList() - if (config.exportPastEntries) { - if (config.exportEvents) { + if (exportPastEntries) { + if (exportEvents) { events.addAll(eventsDB.getAllEventsWithTypes(eventTypes)) } - if (config.exportTasks) { + if (exportTasks) { tasks.addAll(eventsDB.getAllTasksWithTypes(eventTypes)) } } else { - if (config.exportEvents) { + if (exportEvents) { events.addAll(eventsDB.getAllFutureEventsWithTypes(currTS, eventTypes)) } - if (config.exportTasks) { + if (exportTasks) { tasks.addAll(eventsDB.getAllFutureTasksWithTypes(currTS, eventTypes)) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/IcsExporter.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/IcsExporter.kt index 1c4969b8b..1d2e8c753 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/IcsExporter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/helpers/IcsExporter.kt @@ -1,5 +1,6 @@ package com.simplemobiletools.calendar.pro.helpers +import android.content.Context import android.provider.CalendarContract.Events import com.simplemobiletools.calendar.pro.R import com.simplemobiletools.calendar.pro.extensions.calDAVHelper @@ -9,7 +10,6 @@ import com.simplemobiletools.calendar.pro.helpers.IcsExporter.ExportResult.EXPOR import com.simplemobiletools.calendar.pro.helpers.IcsExporter.ExportResult.EXPORT_PARTIAL import com.simplemobiletools.calendar.pro.models.CalDAVCalendar import com.simplemobiletools.calendar.pro.models.Event -import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.commons.extensions.writeLn import com.simplemobiletools.commons.helpers.ensureBackgroundThread @@ -17,7 +17,7 @@ import java.io.BufferedWriter import java.io.OutputStream import java.io.OutputStreamWriter -class IcsExporter(private val activity: BaseSimpleActivity) { +class IcsExporter(private val context: Context) { enum class ExportResult { EXPORT_FAIL, EXPORT_OK, EXPORT_PARTIAL } @@ -26,7 +26,7 @@ class IcsExporter(private val activity: BaseSimpleActivity) { private var eventsExported = 0 private var eventsFailed = 0 private var calendars = ArrayList() - private val reminderLabel = activity.getString(R.string.reminder) + private val reminderLabel = context.getString(R.string.reminder) private val exportTime = Formatter.getExportedTime(System.currentTimeMillis()) fun exportEvents( @@ -41,9 +41,9 @@ class IcsExporter(private val activity: BaseSimpleActivity) { } ensureBackgroundThread { - calendars = activity.calDAVHelper.getCalDAVCalendars("", false) + calendars = context.calDAVHelper.getCalDAVCalendars("", false) if (showExportingToast) { - activity.toast(R.string.exporting) + context.toast(R.string.exporting) } object : BufferedWriter(OutputStreamWriter(outputStream, Charsets.UTF_8)) { @@ -133,8 +133,8 @@ class IcsExporter(private val activity: BaseSimpleActivity) { writeLn(BEGIN_EVENT) event.title.replace("\n", "\\n").let { if (it.isNotEmpty()) writeLn("$SUMMARY:$it") } event.importId.let { if (it.isNotEmpty()) writeLn("$UID$it") } - writeLn("$CATEGORY_COLOR${activity.eventTypesDB.getEventTypeWithId(event.eventType)?.color}") - writeLn("$CATEGORIES${activity.eventTypesDB.getEventTypeWithId(event.eventType)?.title}") + writeLn("$CATEGORY_COLOR${context.eventTypesDB.getEventTypeWithId(event.eventType)?.color}") + writeLn("$CATEGORIES${context.eventTypesDB.getEventTypeWithId(event.eventType)?.title}") writeLn("$LAST_MODIFIED:${Formatter.getExportedTime(event.lastUpdated)}") writeLn("$TRANSP${if (event.availability == Events.AVAILABILITY_FREE) TRANSPARENT else OPAQUE}") event.location.let { if (it.isNotEmpty()) writeLn("$LOCATION:$it") } @@ -166,8 +166,8 @@ class IcsExporter(private val activity: BaseSimpleActivity) { writeLn(BEGIN_TASK) task.title.replace("\n", "\\n").let { if (it.isNotEmpty()) writeLn("$SUMMARY:$it") } task.importId.let { if (it.isNotEmpty()) writeLn("$UID$it") } - writeLn("$CATEGORY_COLOR${activity.eventTypesDB.getEventTypeWithId(task.eventType)?.color}") - writeLn("$CATEGORIES${activity.eventTypesDB.getEventTypeWithId(task.eventType)?.title}") + writeLn("$CATEGORY_COLOR${context.eventTypesDB.getEventTypeWithId(task.eventType)?.color}") + writeLn("$CATEGORIES${context.eventTypesDB.getEventTypeWithId(task.eventType)?.title}") writeLn("$LAST_MODIFIED:${Formatter.getExportedTime(task.lastUpdated)}") task.location.let { if (it.isNotEmpty()) writeLn("$LOCATION:$it") } diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/receivers/AutomaticBackupReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/receivers/AutomaticBackupReceiver.kt new file mode 100644 index 000000000..79d717736 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/receivers/AutomaticBackupReceiver.kt @@ -0,0 +1,17 @@ +package com.simplemobiletools.calendar.pro.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.PowerManager +import com.simplemobiletools.calendar.pro.extensions.backupEventsAndTasks + +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, "simplecalendar:automaticbackupreceiver") + wakelock.acquire(3000) + context.backupEventsAndTasks() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/receivers/BootCompletedReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/receivers/BootCompletedReceiver.kt index e6cd7e523..cc50ec102 100644 --- a/app/src/main/kotlin/com/simplemobiletools/calendar/pro/receivers/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/calendar/pro/receivers/BootCompletedReceiver.kt @@ -3,9 +3,7 @@ package com.simplemobiletools.calendar.pro.receivers import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import com.simplemobiletools.calendar.pro.extensions.notifyRunningEvents -import com.simplemobiletools.calendar.pro.extensions.recheckCalDAVCalendars -import com.simplemobiletools.calendar.pro.extensions.scheduleAllEvents +import com.simplemobiletools.calendar.pro.extensions.* import com.simplemobiletools.commons.helpers.ensureBackgroundThread class BootCompletedReceiver : BroadcastReceiver() { @@ -16,6 +14,8 @@ class BootCompletedReceiver : BroadcastReceiver() { scheduleAllEvents() notifyRunningEvents() recheckCalDAVCalendars(true) {} + scheduleNextAutomaticBackup() + checkAndBackupEventsOnBoot() } } } diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 82e4ea7a5..0fa295391 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -921,6 +921,47 @@ android:id="@+id/settings_tasks_divider" layout="@layout/divider" /> + + + + + + + + + + + + + + + + + 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 000000000..3f0c97fcd --- /dev/null +++ b/app/src/main/res/layout/dialog_manage_automatic_backups.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +