Merge pull request #998 from Merkost/backup

Scheduled backup
This commit is contained in:
Tibor Kaputa 2023-07-20 09:14:50 +02:00 committed by GitHub
commit 9bf842c9f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 569 additions and 11 deletions

View file

@ -4,11 +4,15 @@
package="com.simplemobiletools.contacts.pro" package="com.simplemobiletools.contacts.pro"
android:installLocation="auto"> android:installLocation="auto">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
@ -206,6 +210,22 @@
android:label="@string/customize_colors" android:label="@string/customize_colors"
android:parentActivityName=".activities.SettingsActivity" /> android:parentActivityName=".activities.SettingsActivity" />
<receiver
android:name=".receivers.BootCompletedReceiver"
android:exported="true">
<intent-filter>
<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 <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"

View file

@ -6,11 +6,15 @@ import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.contacts.pro.R import com.simplemobiletools.contacts.pro.R
import com.simplemobiletools.contacts.pro.dialogs.ManageAutoBackupsDialog
import com.simplemobiletools.contacts.pro.dialogs.ManageVisibleFieldsDialog import com.simplemobiletools.contacts.pro.dialogs.ManageVisibleFieldsDialog
import com.simplemobiletools.contacts.pro.dialogs.ManageVisibleTabsDialog import com.simplemobiletools.contacts.pro.dialogs.ManageVisibleTabsDialog
import com.simplemobiletools.contacts.pro.extensions.cancelScheduledAutomaticBackup
import com.simplemobiletools.contacts.pro.extensions.config import com.simplemobiletools.contacts.pro.extensions.config
import com.simplemobiletools.contacts.pro.extensions.scheduleNextAutomaticBackup
import kotlinx.android.synthetic.main.activity_settings.* import kotlinx.android.synthetic.main.activity_settings.*
import java.util.* import java.util.Locale
import kotlin.system.exitProcess
class SettingsActivity : SimpleActivity() { class SettingsActivity : SimpleActivity() {
@ -42,9 +46,17 @@ class SettingsActivity : SimpleActivity() {
setupShowPrivateContacts() setupShowPrivateContacts()
setupOnContactClick() setupOnContactClick()
setupDefaultTab() setupDefaultTab()
setupEnableAutomaticBackups()
setupManageAutomaticBackups()
updateTextColors(settings_holder) updateTextColors(settings_holder)
arrayOf(settings_color_customization_section_label, settings_general_settings_label, settings_main_screen_label, settings_list_view_label).forEach { arrayOf(
settings_color_customization_section_label,
settings_general_settings_label,
settings_main_screen_label,
settings_list_view_label,
settings_backups_label
).forEach {
it.setTextColor(getProperPrimaryColor()) it.setTextColor(getProperPrimaryColor())
} }
} }
@ -116,7 +128,7 @@ class SettingsActivity : SimpleActivity() {
settings_use_english_holder.setOnClickListener { settings_use_english_holder.setOnClickListener {
settings_use_english.toggle() settings_use_english.toggle()
config.useEnglish = settings_use_english.isChecked config.useEnglish = settings_use_english.isChecked
System.exit(0) exitProcess(0)
} }
} }
@ -215,4 +227,43 @@ class SettingsActivity : SimpleActivity() {
config.mergeDuplicateContacts = settings_merge_duplicate_contacts.isChecked config.mergeDuplicateContacts = settings_merge_duplicate_contacts.isChecked
} }
} }
private fun setupEnableAutomaticBackups() {
settings_backups_label.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) {
ManageAutoBackupsDialog(
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 {
ManageAutoBackupsDialog(
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)
}
} }

View file

@ -15,7 +15,7 @@ import kotlinx.android.synthetic.main.item_filter_contact_source.view.*
class FilterContactSourcesAdapter( class FilterContactSourcesAdapter(
val activity: SimpleActivity, val activity: SimpleActivity,
private val contactSources: List<ContactSource>, private val contactSources: List<ContactSource>,
private val displayContactSources: ArrayList<String> private val displayContactSources: List<String>
) : RecyclerView.Adapter<FilterContactSourcesAdapter.ViewHolder>() { ) : RecyclerView.Adapter<FilterContactSourcesAdapter.ViewHolder>() {
private val selectedKeys = HashSet<Int>() private val selectedKeys = HashSet<Int>()

View file

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

View file

@ -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<ContactSource>()
private var selectedContactSources = config.autoBackupContactSources
private var contacts = ArrayList<Contact>()
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<ContactSource>()
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))
}
}
}
}
}

View file

@ -1,18 +1,25 @@
package com.simplemobiletools.contacts.pro.extensions package com.simplemobiletools.contacts.pro.extensions
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.core.app.AlarmManagerCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.simplemobiletools.commons.extensions.getCachePhoto import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.SIGNAL_PACKAGE import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.helpers.TELEGRAM_PACKAGE
import com.simplemobiletools.commons.helpers.VIBER_PACKAGE
import com.simplemobiletools.commons.helpers.WHATSAPP_PACKAGE
import com.simplemobiletools.contacts.pro.BuildConfig import com.simplemobiletools.contacts.pro.BuildConfig
import com.simplemobiletools.contacts.pro.R 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.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.File
import java.io.FileOutputStream
val Context.config: Config get() = Config.newInstance(applicationContext) val Context.config: Config get() = Config.newInstance(applicationContext)
fun Context.getCachePhotoUri(file: File = getCachePhoto()) = FileProvider.getUriForFile(this, "${BuildConfig.APPLICATION_ID}.provider", file) 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 }, 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()
}
}
}

View file

@ -12,4 +12,9 @@ class Config(context: Context) : BaseConfig(context) {
var showTabs: Int var showTabs: Int
get() = prefs.getInt(SHOW_TABS, ALL_TABS_MASK) get() = prefs.getInt(SHOW_TABS, ALL_TABS_MASK)
set(showTabs) = prefs.edit().putInt(SHOW_TABS, showTabs).apply() set(showTabs) = prefs.edit().putInt(SHOW_TABS, showTabs).apply()
var autoBackupContactSources: Set<String>
get() = prefs.getStringSet(AUTO_BACKUP_CONTACT_SOURCES, setOf())!!
set(autoBackupContactSources) = prefs.edit().remove(AUTO_BACKUP_CONTACT_SOURCES).putStringSet(AUTO_BACKUP_CONTACT_SOURCES, autoBackupContactSources).apply()
} }

View file

@ -3,16 +3,20 @@ package com.simplemobiletools.contacts.pro.helpers
import com.simplemobiletools.commons.helpers.TAB_CONTACTS import com.simplemobiletools.commons.helpers.TAB_CONTACTS
import com.simplemobiletools.commons.helpers.TAB_FAVORITES import com.simplemobiletools.commons.helpers.TAB_FAVORITES
import com.simplemobiletools.commons.helpers.TAB_GROUPS import com.simplemobiletools.commons.helpers.TAB_GROUPS
import org.joda.time.DateTime
const val GROUP = "group" const val GROUP = "group"
const val IS_FROM_SIMPLE_CONTACTS = "is_from_simple_contacts" const val IS_FROM_SIMPLE_CONTACTS = "is_from_simple_contacts"
const val ADD_NEW_CONTACT_NUMBER = "add_new_contact_number" 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 DEFAULT_FILE_NAME = "contacts.vcf"
const val AVOID_CHANGING_TEXT_TAG = "avoid_changing_text_tag" const val AVOID_CHANGING_TEXT_TAG = "avoid_changing_text_tag"
const val AVOID_CHANGING_VISIBILITY_TAG = "avoid_changing_visibility_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 // extras used at third party intents
const val KEY_NAME = "name" const val KEY_NAME = "name"
const val KEY_EMAIL = "email" const val KEY_EMAIL = "email"
@ -53,3 +57,20 @@ const val SIGNAL = "signal"
const val VIBER = "viber" const val VIBER = "viber"
const val TELEGRAM = "telegram" const val TELEGRAM = "telegram"
const val THREEMA = "threema" 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)
}

View file

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

View file

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

View file

@ -341,6 +341,48 @@
android:text="@string/start_name_with_surname" /> android:text="@string/start_name_with_surname" />
</RelativeLayout> </RelativeLayout>
<include
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>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

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,83 @@
<?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/backup_contacts_scrollview"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/backup_contacts_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_contacts_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_contacts_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_contacts_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_vcf"
app:endIconDrawable="@drawable/ic_info_vector"
app:endIconMode="custom">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/backup_contacts_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>
<ImageView
android:id="@+id/backup_contacts_divider"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginTop="@dimen/medium_margin"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginBottom="@dimen/medium_margin"
android:background="@color/divider_grey"
android:importantForAccessibility="no" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/backup_contacts_pick_sources_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/activity_margin"
android:paddingEnd="@dimen/activity_margin"
android:text="@string/include_contact_sources"
android:textSize="@dimen/smaller_text_size" />
<com.simplemobiletools.commons.views.MyRecyclerView
android:id="@+id/backup_contact_sources_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingTop="@dimen/medium_margin"
app:layoutManager="com.simplemobiletools.commons.views.MyLinearLayoutManager" />
</LinearLayout>
</ScrollView>