diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5c4563d7..6bfa8854 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,11 +4,15 @@
package="com.simplemobiletools.contacts.pro"
android:installLocation="auto">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
- private val displayContactSources: ArrayList
+ private val displayContactSources: List
) : RecyclerView.Adapter() {
private val selectedKeys = HashSet()
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/DateTimePatternInfoDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/DateTimePatternInfoDialog.kt
new file mode 100644
index 00000000..ad5cd722
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/DateTimePatternInfoDialog.kt
@@ -0,0 +1,18 @@
+package com.simplemobiletools.contacts.pro.dialogs
+
+import com.simplemobiletools.commons.activities.BaseSimpleActivity
+import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
+import com.simplemobiletools.commons.extensions.setupDialogStuff
+import com.simplemobiletools.contacts.pro.R
+
+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/contacts/pro/dialogs/ManageAutoBackupsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/ManageAutoBackupsDialog.kt
new file mode 100644
index 00000000..9e999647
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/dialogs/ManageAutoBackupsDialog.kt
@@ -0,0 +1,157 @@
+package com.simplemobiletools.contacts.pro.dialogs
+
+import android.view.View
+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.ContactsHelper
+import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.commons.models.contacts.Contact
+import com.simplemobiletools.commons.models.contacts.ContactSource
+import com.simplemobiletools.contacts.pro.R
+import com.simplemobiletools.contacts.pro.activities.SimpleActivity
+import com.simplemobiletools.contacts.pro.adapters.FilterContactSourcesAdapter
+import com.simplemobiletools.contacts.pro.extensions.config
+import kotlinx.android.synthetic.main.dialog_manage_automatic_backups.view.backup_contact_sources_list
+import kotlinx.android.synthetic.main.dialog_manage_automatic_backups.view.backup_contacts_filename
+import kotlinx.android.synthetic.main.dialog_manage_automatic_backups.view.backup_contacts_filename_hint
+import kotlinx.android.synthetic.main.dialog_manage_automatic_backups.view.backup_contacts_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
+ private var contactSources = mutableListOf()
+ private var selectedContactSources = config.autoBackupContactSources
+ private var contacts = ArrayList()
+ private var isContactSourcesReady = false
+ private var isContactsReady = false
+
+ init {
+ view.apply {
+ backup_contacts_folder.setText(activity.humanizePath(backupFolder))
+ val filename = config.autoBackupFilename.ifEmpty {
+ "${activity.getString(R.string.contacts)}_%Y%M%D_%h%m%s"
+ }
+
+ backup_contacts_filename.setText(filename)
+ backup_contacts_filename_hint.setEndIconOnClickListener {
+ DateTimePatternInfoDialog(activity)
+ }
+
+ backup_contacts_filename_hint.setEndIconOnLongClickListener {
+ DateTimePatternInfoDialog(activity)
+ true
+ }
+
+ backup_contacts_folder.setOnClickListener {
+ selectBackupFolder()
+ }
+
+ ContactsHelper(activity).getContactSources { sources ->
+ contactSources = sources
+ isContactSourcesReady = true
+ processDataIfReady(this)
+ }
+
+ ContactsHelper(activity).getContacts(getAll = true) { receivedContacts ->
+ contacts = receivedContacts
+ isContactsReady = true
+ processDataIfReady(this)
+ }
+ }
+
+ 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 {
+ if (view.backup_contact_sources_list.adapter == null) {
+ return@setOnClickListener
+ }
+ val filename = view.backup_contacts_filename.value
+ when {
+ filename.isEmpty() -> activity.toast(R.string.empty_name)
+ filename.isAValidFilename() -> {
+ val file = File(backupFolder, "$filename.vcf")
+ if (file.exists() && !file.canWrite()) {
+ activity.toast(R.string.name_taken)
+ return@setOnClickListener
+ }
+
+ val selectedSources = (view.backup_contact_sources_list.adapter as FilterContactSourcesAdapter).getSelectedContactSources()
+ if (selectedSources.isEmpty()) {
+ activity.toast(R.string.no_entries_for_exporting)
+ return@setOnClickListener
+ }
+
+ config.autoBackupContactSources = selectedSources.map { it.name }.toSet()
+
+ ensureBackgroundThread {
+ config.apply {
+ autoBackupFolder = backupFolder
+ autoBackupFilename = filename
+ }
+
+ activity.runOnUiThread {
+ onSuccess()
+ }
+
+ dialog.dismiss()
+ }
+ }
+
+ else -> activity.toast(R.string.invalid_name)
+ }
+ }
+ }
+ }
+ }
+
+ private fun processDataIfReady(view: View) {
+ if (!isContactSourcesReady || !isContactsReady) {
+ return
+ }
+
+ if (selectedContactSources.isEmpty()) {
+ selectedContactSources = contactSources.map { it.name }.toSet()
+ }
+
+ val contactSourcesWithCount = mutableListOf()
+ for (source in contactSources) {
+ val count = contacts.count { it.source == source.name }
+ contactSourcesWithCount.add(source.copy(count = count))
+ }
+
+ contactSources.clear()
+ contactSources.addAll(contactSourcesWithCount)
+
+ activity.runOnUiThread {
+ view.backup_contact_sources_list.adapter = FilterContactSourcesAdapter(activity, contactSourcesWithCount, selectedContactSources.toList())
+ }
+ }
+
+ private fun selectBackupFolder() {
+ activity.hideKeyboard(view.backup_contacts_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_contacts_folder.setText(activity.humanizePath(path))
+ }
+ }
+ }
+ }
+}
+
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Context.kt
index b4f18f1a..1ed82973 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Context.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/extensions/Context.kt
@@ -1,18 +1,25 @@
package com.simplemobiletools.contacts.pro.extensions
import android.annotation.SuppressLint
+import android.app.AlarmManager
+import android.app.PendingIntent
import android.content.Context
+import android.content.Intent
import android.graphics.drawable.Drawable
+import androidx.core.app.AlarmManagerCompat
import androidx.core.content.FileProvider
-import com.simplemobiletools.commons.extensions.getCachePhoto
-import com.simplemobiletools.commons.helpers.SIGNAL_PACKAGE
-import com.simplemobiletools.commons.helpers.TELEGRAM_PACKAGE
-import com.simplemobiletools.commons.helpers.VIBER_PACKAGE
-import com.simplemobiletools.commons.helpers.WHATSAPP_PACKAGE
+import com.simplemobiletools.commons.extensions.*
+import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.contacts.pro.BuildConfig
import com.simplemobiletools.contacts.pro.R
+import com.simplemobiletools.contacts.pro.helpers.AUTOMATIC_BACKUP_REQUEST_CODE
import com.simplemobiletools.contacts.pro.helpers.Config
+import com.simplemobiletools.contacts.pro.helpers.getNextAutoBackupTime
+import com.simplemobiletools.contacts.pro.helpers.getPreviousAutoBackupTime
+import com.simplemobiletools.contacts.pro.receivers.AutomaticBackupReceiver
+import org.joda.time.DateTime
import java.io.File
+import java.io.FileOutputStream
val Context.config: Config get() = Config.newInstance(applicationContext)
fun Context.getCachePhotoUri(file: File = getCachePhoto()) = FileProvider.getUriForFile(this, "${BuildConfig.APPLICATION_ID}.provider", file)
@@ -29,3 +36,113 @@ fun Context.getPackageDrawable(packageName: String): Drawable {
}, theme
)
}
+
+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.checkAndBackupContactsOnBoot() {
+ 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
+ backupContacts()
+ }
+ }
+}
+
+fun Context.backupContacts() {
+ require(isRPlus())
+ ensureBackgroundThread {
+ val config = config
+ ContactsHelper(this).getContactsToExport(selectedContactSources = config.autoBackupContactSources) { contactsToBackup ->
+ if (contactsToBackup.isEmpty()) {
+ toast(R.string.no_entries_for_exporting)
+ config.lastAutoBackupTime = DateTime.now().millis
+ scheduleNextAutomaticBackup()
+ return@getContactsToExport
+ }
+
+
+ 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.vcf")
+ 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}.vcf")
+ exportFilePath = exportFile.absolutePath
+ }
+ FileOutputStream(exportFile)
+ }
+ } catch (e: Exception) {
+ showErrorToast(e)
+ scheduleNextAutomaticBackup()
+ return@getContactsToExport
+ }
+
+ val exportResult = try {
+ ContactsHelper(this).exportContacts(contactsToBackup, outputStream)
+ } catch (e: Exception) {
+ showErrorToast(e)
+ }
+
+ when (exportResult) {
+ ExportResult.EXPORT_OK -> toast(R.string.exporting_successful)
+ else -> toast(R.string.exporting_failed)
+ }
+
+ config.lastAutoBackupTime = DateTime.now().millis
+ scheduleNextAutomaticBackup()
+ }
+ }
+}
+
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Config.kt
index 6fd98956..21822187 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Config.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Config.kt
@@ -12,4 +12,9 @@ class Config(context: Context) : BaseConfig(context) {
var showTabs: Int
get() = prefs.getInt(SHOW_TABS, ALL_TABS_MASK)
set(showTabs) = prefs.edit().putInt(SHOW_TABS, showTabs).apply()
+
+ var autoBackupContactSources: Set
+ get() = prefs.getStringSet(AUTO_BACKUP_CONTACT_SOURCES, setOf())!!
+ set(autoBackupContactSources) = prefs.edit().remove(AUTO_BACKUP_CONTACT_SOURCES).putStringSet(AUTO_BACKUP_CONTACT_SOURCES, autoBackupContactSources).apply()
+
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Constants.kt
index 6ea62bfb..665ff482 100644
--- a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Constants.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/helpers/Constants.kt
@@ -3,16 +3,20 @@ package com.simplemobiletools.contacts.pro.helpers
import com.simplemobiletools.commons.helpers.TAB_CONTACTS
import com.simplemobiletools.commons.helpers.TAB_FAVORITES
import com.simplemobiletools.commons.helpers.TAB_GROUPS
+import org.joda.time.DateTime
const val GROUP = "group"
const val IS_FROM_SIMPLE_CONTACTS = "is_from_simple_contacts"
const val ADD_NEW_CONTACT_NUMBER = "add_new_contact_number"
-const val FIRST_CONTACT_ID = 1000000
-const val FIRST_GROUP_ID = 10000L
const val DEFAULT_FILE_NAME = "contacts.vcf"
const val AVOID_CHANGING_TEXT_TAG = "avoid_changing_text_tag"
const val AVOID_CHANGING_VISIBILITY_TAG = "avoid_changing_visibility_tag"
+const val AUTOMATIC_BACKUP_REQUEST_CODE = 10001
+const val AUTO_BACKUP_INTERVAL_IN_DAYS = 1
+
+const val AUTO_BACKUP_CONTACT_SOURCES = "auto_backup_contact_sources"
+
// extras used at third party intents
const val KEY_NAME = "name"
const val KEY_EMAIL = "email"
@@ -53,3 +57,20 @@ const val SIGNAL = "signal"
const val VIBER = "viber"
const val TELEGRAM = "telegram"
const val THREEMA = "threema"
+
+
+// 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/contacts/pro/receivers/AutomaticBackupReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/receivers/AutomaticBackupReceiver.kt
new file mode 100644
index 00000000..6de77a0d
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/receivers/AutomaticBackupReceiver.kt
@@ -0,0 +1,17 @@
+package com.simplemobiletools.contacts.pro.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.PowerManager
+import com.simplemobiletools.contacts.pro.extensions.backupContacts
+
+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, "simplecontacts:automaticbackupreceiver")
+ wakelock.acquire(3000)
+ context.backupContacts()
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/pro/receivers/BootCompletedReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/receivers/BootCompletedReceiver.kt
new file mode 100644
index 00000000..c0972986
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/contacts/pro/receivers/BootCompletedReceiver.kt
@@ -0,0 +1,18 @@
+package com.simplemobiletools.contacts.pro.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.contacts.pro.extensions.checkAndBackupContactsOnBoot
+
+class BootCompletedReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ ensureBackgroundThread {
+ context.apply {
+ checkAndBackupContactsOnBoot()
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index c120cb1a..d225ec4e 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -341,6 +341,48 @@
android:text="@string/start_name_with_surname" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..21690ff2
--- /dev/null
+++ b/app/src/main/res/layout/dialog_manage_automatic_backups.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+