Merge pull request #2027 from Naveen3Singh/feature_automatic_backups

Implement automatic backups
This commit is contained in:
Tibor Kaputa
2023-04-07 08:42:07 +02:00
committed by GitHub
17 changed files with 625 additions and 79 deletions

View File

@@ -283,9 +283,14 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
android:name=".receivers.AutomaticBackupReceiver"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View File

@@ -22,8 +22,8 @@ import com.simplemobiletools.calendar.pro.adapters.EventListAdapter
import com.simplemobiletools.calendar.pro.adapters.QuickFilterEventTypeAdapter
import com.simplemobiletools.calendar.pro.databases.EventsDatabase
import com.simplemobiletools.calendar.pro.dialogs.ExportEventsDialog
import com.simplemobiletools.calendar.pro.dialogs.FilterEventTypesDialog
import com.simplemobiletools.calendar.pro.dialogs.ImportEventsDialog
import com.simplemobiletools.calendar.pro.dialogs.SelectEventTypesDialog
import com.simplemobiletools.calendar.pro.dialogs.SetRemindersDialog
import com.simplemobiletools.calendar.pro.extensions.*
import com.simplemobiletools.calendar.pro.fragments.*
@@ -526,12 +526,16 @@ class MainActivity : SimpleActivity(), RefreshRecyclerViewListener {
}
private fun showFilterDialog() {
FilterEventTypesDialog(this) {
SelectEventTypesDialog(this, config.displayEventTypes) {
if (config.displayEventTypes != it) {
config.displayEventTypes = it
refreshViewPager()
setupQuickFilter()
updateWidgets()
}
}
}
fun toggleGoToTodayVisibility(beVisible: Boolean) {
shouldGoToTodayBeVisible = beVisible
@@ -1149,7 +1153,7 @@ class MainActivity : SimpleActivity(), RefreshRecyclerViewListener {
private fun exportEventsTo(eventTypes: ArrayList<Long>, 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 {

View File

@@ -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<String, Any>().apply {

View File

@@ -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)
}
}
}

View File

@@ -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))
}
}
}
}
}

View File

@@ -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<String>, val callback: (HashSet<String>) -> 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()
}
}

View File

@@ -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()
}
}

View File

@@ -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<Long>,
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)

View File

@@ -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<String>
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()
}

View File

@@ -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)
}

View File

@@ -519,22 +519,22 @@ class EventsHelper(val context: Context) {
return events
}
fun getEventsToExport(eventTypes: ArrayList<Long>): ArrayList<Event> {
fun getEventsToExport(eventTypes: ArrayList<Long>, exportEvents: Boolean, exportTasks: Boolean, exportPastEntries: Boolean): ArrayList<Event> {
val currTS = getNowSeconds()
var events = ArrayList<Event>()
val tasks = ArrayList<Event>()
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))
}
}

View File

@@ -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<CalDAVCalendar>()
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") }

View File

@@ -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()
}
}

View File

@@ -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()
}
}
}

View File

@@ -921,6 +921,47 @@
android:id="@+id/settings_tasks_divider"
layout="@layout/divider" />
<TextView
android:id="@+id/settings_backups_label"
style="@style/SettingsSectionLabelStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/backups" />
<RelativeLayout
android:id="@+id/settings_enable_automatic_backups_holder"
style="@style/SettingsHolderCheckboxStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/settings_enable_automatic_backups"
style="@style/SettingsCheckboxStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/enable_automatic_backups" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/settings_manage_automatic_backups_holder"
style="@style/SettingsHolderTextViewOneLinerStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/settings_manage_automatic_backups"
style="@style/SettingsTextLabelStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/manage_automatic_backups" />
</RelativeLayout>
<include
android:id="@+id/settings_backups_divider"
layout="@layout/divider" />
<TextView
android:id="@+id/settings_migrating_label"
style="@style/SettingsSectionLabelStyle"

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<com.simplemobiletools.commons.views.MyTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/date_time_pattern_info"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="@dimen/big_margin"
android:paddingTop="@dimen/big_margin"
android:text="@string/date_time_pattern_info"
android:textIsSelectable="true" />

View File

@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/export_events_scrollview"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/backup_events_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/activity_margin">
<com.simplemobiletools.commons.views.MyTextInputLayout
android:id="@+id/backup_events_folder_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginBottom="@dimen/activity_margin"
android:hint="@string/folder">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/backup_events_folder"
style="@style/UnclickableEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.simplemobiletools.commons.views.MyTextInputLayout>
<com.simplemobiletools.commons.views.MyTextInputLayout
android:id="@+id/backup_events_filename_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginEnd="@dimen/activity_margin"
android:hint="@string/filename_without_ics"
app:endIconDrawable="@drawable/ic_info_vector"
app:endIconMode="custom">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/backup_events_filename"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords"
android:singleLine="true"
android:textCursorDrawable="@null"
android:textSize="@dimen/bigger_text_size" />
</com.simplemobiletools.commons.views.MyTextInputLayout>
<RelativeLayout
android:id="@+id/backup_events_checkbox_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/medium_margin"
android:background="?attr/selectableItemBackground"
android:paddingVertical="@dimen/smaller_margin"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/activity_margin">
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/backup_events_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:checked="true"
android:clickable="false"
android:layoutDirection="rtl"
android:paddingHorizontal="@dimen/medium_margin"
android:text="@string/export_events" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/backup_tasks_checkbox_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingVertical="@dimen/smaller_margin"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/activity_margin">
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/backup_tasks_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:checked="true"
android:clickable="false"
android:layoutDirection="rtl"
android:paddingHorizontal="@dimen/medium_margin"
android:text="@string/export_tasks" />
</RelativeLayout>
<ImageView
android:id="@+id/backup_past_entries_divider"
android:layout_width="match_parent"
android:layout_height="@dimen/divider_height"
android:layout_marginStart="@dimen/activity_margin"
android:background="@color/divider_grey"
android:importantForAccessibility="no" />
<RelativeLayout
android:id="@+id/backup_past_events_checkbox_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingVertical="@dimen/smaller_margin"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/activity_margin">
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/backup_past_events_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:layoutDirection="rtl"
android:paddingHorizontal="@dimen/medium_margin"
android:text="@string/export_past_entries" />
</RelativeLayout>
<ImageView
android:id="@+id/select_event_types_divider"
android:layout_width="match_parent"
android:layout_height="@dimen/divider_height"
android:layout_marginStart="@dimen/activity_margin"
android:background="@color/divider_grey"
android:importantForAccessibility="no" />
<RelativeLayout
android:id="@+id/manage_event_types_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingVertical="@dimen/medium_margin"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/activity_margin">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/manage_event_types"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:paddingHorizontal="@dimen/medium_margin"
android:paddingVertical="@dimen/normal_margin"
android:text="@string/manage_event_types"
android:textSize="@dimen/normal_text_size" />
</RelativeLayout>
</LinearLayout>
</ScrollView>