Display backup warnings to user

* Warn if backups stored in private storage
* Warn if local backup is stale
* Warn if remote backup is stale
* Add option to disable warnings
pull/1136/head
Alex Baker 5 years ago
parent 40a764112c
commit 3e17bea70b

@ -0,0 +1,28 @@
package org.tasks.preferences
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import org.tasks.R
class IconPreference(context: Context?, attrs: AttributeSet?) : Preference(context, attrs) {
var iconVisible = false
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
super.onBindViewHolder(holder)
(holder?.findViewById(R.id.preference_icon) as ImageView?)?.visibility = if (iconVisible) {
View.VISIBLE
} else {
View.GONE
}
}
init {
widgetLayoutResource = R.layout.preference_icon
}
}

@ -36,6 +36,8 @@ class Preferences @JvmOverloads constructor(private val context: Context, name:
fun androidBackupServiceEnabled() = getBoolean(R.string.p_backups_android_backup_enabled, true)
fun showBackupWarnings() = !getBoolean(R.string.p_backups_ignore_warnings, false)
fun addTasksToTop(): Boolean {
return getBoolean(R.string.p_add_to_top, true)
}
@ -330,6 +332,9 @@ class Preferences @JvmOverloads constructor(private val context: Context, name:
val backupDirectory: Uri?
get() = getDirectory(R.string.p_backup_dir, "backups")
val externalStorage: Uri
get() = root.uri
val attachmentsDirectory: Uri?
get() = getDirectory(R.string.p_attachment_dir, TaskAttachment.FILES_DIRECTORY_DEFAULT)
@ -351,16 +356,18 @@ class Preferences @JvmOverloads constructor(private val context: Context, name:
}
}
}
val documentFile = DocumentFile.fromFile(context.getExternalFilesDir(null)!!).createDirectory(name)
if (documentFile != null) {
return documentFile.uri
}
val file = getDefaultFileLocation(name)
return if (file != null) {
Uri.fromFile(file)
} else null
return getDefaultDirectory(name)
}
private fun getDefaultDirectory(name: String): Uri? =
root
.createDirectory(name)
?.uri
?: getDefaultFileLocation(name)?.let { Uri.fromFile(it) }
private val root: DocumentFile
get() = DocumentFile.fromFile(context.getExternalFilesDir(null)!!)
private fun getDefaultFileLocation(type: String): File? {
val externalFilesDir = context.getExternalFilesDir(null) ?: return null
val path = String.format("%s/%s", externalFilesDir.absolutePath, type)

@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.R
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.drive.DriveInvoker
import org.tasks.gtasks.GoogleAccountManager
import timber.log.Timber
@ -30,6 +31,22 @@ class PreferencesViewModel @ViewModelInject constructor(
val lastDriveBackup = MutableLiveData<Long?>()
val lastAndroidBackup = MutableLiveData<Long>()
private fun isStale(timestamp: Long?) =
timestamp != null
&& preferences.showBackupWarnings()
&& timestamp < newDateTime().startOfDay().minusDays(2).millis
val staleLocalBackup: Boolean
get() = isStale(lastBackup.value)
val staleRemoteBackup: Boolean
get() = isStale(lastDriveBackup.value) && isStale(lastAndroidBackup.value)
val usingPrivateStorage: Boolean
get() = preferences.backupDirectory.let {
it == null || it.toString().startsWith(preferences.externalStorage.toString())
}
val driveAccount: String?
get() {
val account = preferences.getStringValue(R.string.p_google_drive_backup_account)
@ -42,7 +59,7 @@ class PreferencesViewModel @ViewModelInject constructor(
fun updateDriveBackup() = viewModelScope.launch {
if (driveAccount.isNullOrBlank()) {
lastDriveBackup.value = null
lastDriveBackup.value = -1L
return@launch
}
val files = preferences.getStringValue(R.string.p_google_drive_backup_folder)
@ -56,7 +73,7 @@ class PreferencesViewModel @ViewModelInject constructor(
}
}
?: emptyList()
lastDriveBackup.value = files.firstOrNull()?.let { BackupConstants.getTimestamp(it) }
lastDriveBackup.value = files.firstOrNull()?.let { BackupConstants.getTimestamp(it) } ?: -1
}
fun updateLocalBackup() = viewModelScope.launch {
@ -78,13 +95,12 @@ class PreferencesViewModel @ViewModelInject constructor(
else -> emptyList()
}
}
lastBackup.value = timestamps?.maxOrNull()
lastBackup.value = timestamps?.maxOrNull() ?: -1L
}
private fun updateAndroidBackup() {
lastAndroidBackup.value = preferences
.getLong(R.string.p_backups_android_backup_last, -1L)
.takeIf { it >= 0 }
}
fun updateBackups() {

@ -43,7 +43,13 @@ class Backups : InjectingPreferenceFragment() {
override fun getPreferenceXml() = R.xml.preferences_backups
override suspend fun setupPreferences(savedInstanceState: Bundle?) {
initializeBackupDirectory()
findPreference(R.string.p_backup_dir)
.setOnPreferenceClickListener {
FileHelper.newDirectoryPicker(
this, REQUEST_CODE_BACKUP_DIR, preferences.backupDirectory
)
false
}
findPreference(R.string.backup_BAc_import)
.setOnPreferenceClickListener {
@ -63,42 +69,73 @@ class Backups : InjectingPreferenceFragment() {
findPreference(R.string.google_drive_backup)
.setOnPreferenceChangeListener(this@Backups::onGoogleDriveCheckChanged)
findPreference(R.string.p_google_drive_backup_account)
.setOnPreferenceClickListener {
requestGoogleDriveLogin()
false
}
findPreference(R.string.p_backups_android_backup_enabled)
.setOnPreferenceChangeListener(this@Backups::onAndroidBackupCheckChanged)
findPreference(R.string.p_backups_ignore_warnings).setOnPreferenceChangeListener { _, newValue ->
if (newValue is Boolean) {
preferences.setBoolean(R.string.p_backups_ignore_warnings, newValue)
updateWarnings()
true
} else {
false
}
}
viewModel.lastBackup.observe(this, this::updateLastBackup)
viewModel.lastDriveBackup.observe(this, this::updateDriveBackup)
viewModel.lastAndroidBackup.observe(this, this::updateAndroidBackup)
}
private fun updateLastBackup(timestamp: Long?) {
private fun updateLastBackup(timestamp: Long? = viewModel.lastBackup.value) {
findPreference(R.string.backup_BAc_export).summary =
getString(
R.string.last_backup,
timestamp
?.takeIf { it >= 0 }
?.let { DateUtilities.getLongDateStringWithTime(it, locale) }
?: getString(R.string.last_backup_never)
)
}
private fun updateDriveBackup(timestamp: Long?) {
findPreference(R.string.google_drive_backup).summary =
private fun updateDriveBackup(timestamp: Long? = viewModel.lastDriveBackup.value) {
val pref = findPreference(R.string.google_drive_backup)
if (viewModel.staleRemoteBackup) {
pref.setIcon(R.drawable.ic_outline_error_outline_24px)
tintIcons(pref, requireContext().getColor(R.color.overdue))
} else {
pref.icon = null
}
pref.summary =
getString(
R.string.last_backup,
timestamp
?.takeIf { it >= 0 }
?.let { DateUtilities.getLongDateStringWithTime(it, locale) }
?: getString(R.string.last_backup_never)
)
}
private fun updateAndroidBackup(timestamp: Long?) {
findPreference(R.string.p_backups_android_backup_enabled).summary =
private fun updateAndroidBackup(timestamp: Long? = viewModel.lastAndroidBackup.value) {
val pref = findPreference(R.string.p_backups_android_backup_enabled) as SwitchPreferenceCompat
if (viewModel.staleRemoteBackup) {
pref.setIcon(R.drawable.ic_outline_error_outline_24px)
tintIcons(pref, requireContext().getColor(R.color.overdue))
} else {
pref.icon = null
}
pref.summary =
getString(
R.string.last_backup,
timestamp
?.takeIf { it >= 0 }
?.let { DateUtilities.getLongDateStringWithTime(it, locale) }
?: getString(R.string.last_backup_never)
)
@ -107,6 +144,7 @@ class Backups : InjectingPreferenceFragment() {
override fun onResume() {
super.onResume()
updateWarnings()
updateDriveAccount()
val driveBackup = findPreference(R.string.google_drive_backup) as SwitchPreferenceCompat
@ -114,6 +152,13 @@ class Backups : InjectingPreferenceFragment() {
driveBackup.isChecked = driveAccount != null
}
private fun updateWarnings() {
updateLastBackup()
updateDriveBackup()
updateAndroidBackup()
updateBackupDirectory()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
@ -171,6 +216,14 @@ class Backups : InjectingPreferenceFragment() {
}
}
private fun onAndroidBackupCheckChanged(preference: Preference, newValue: Any?): Boolean {
if (newValue is Boolean) {
(preference as SwitchPreferenceCompat).isChecked = newValue
updateAndroidBackup()
}
return true
}
private fun onGoogleDriveCheckChanged(preference: Preference, newValue: Any?) = when {
newValue as Boolean -> {
requestGoogleDriveLogin()
@ -178,8 +231,10 @@ class Backups : InjectingPreferenceFragment() {
}
else -> {
preference.summary = null
preference.icon = null
preferences.setString(R.string.p_google_drive_backup_account, null)
updateDriveAccount()
viewModel.updateDriveBackup()
true
}
}
@ -203,19 +258,21 @@ class Backups : InjectingPreferenceFragment() {
}
}
private fun initializeBackupDirectory() {
findPreference(R.string.p_backup_dir)
.setOnPreferenceClickListener {
FileHelper.newDirectoryPicker(
this, REQUEST_CODE_BACKUP_DIR, preferences.backupDirectory
)
false
}
updateBackupDirectory()
}
private fun updateBackupDirectory() {
findPreference(R.string.p_backup_dir).summary =
FileHelper.uri2String(preferences.backupDirectory)
val pref = findPreference(R.string.p_backup_dir)
val location = FileHelper.uri2String(preferences.backupDirectory)
pref.summary = location
if (preferences.showBackupWarnings() && viewModel.usingPrivateStorage) {
pref.setIcon(R.drawable.ic_outline_error_outline_24px)
tintIcons(pref, requireContext().getColor(R.color.overdue))
pref.summary = """
$location
${requireContext().getString(R.string.backup_location_warning, FileHelper.uri2String(preferences.externalStorage))}
""".trimIndent()
} else {
pref.icon = null
pref.summary = location
}
}
}

@ -1,10 +1,14 @@
package org.tasks.preferences.fragments
import android.os.Bundle
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.preferences.IconPreference
import org.tasks.preferences.Preferences
import org.tasks.preferences.PreferencesViewModel
import org.tasks.widget.AppWidgetManager
import javax.inject.Inject
@ -12,12 +16,38 @@ import javax.inject.Inject
class MainSettingsFragment : InjectingPreferenceFragment() {
@Inject lateinit var appWidgetManager: AppWidgetManager
@Inject lateinit var preferences: Preferences
private val viewModel: PreferencesViewModel by activityViewModels()
override fun getPreferenceXml() = R.xml.preferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.lastBackup.observe(this) { updateBackupWarning() }
viewModel.lastAndroidBackup.observe(this) { updateBackupWarning() }
viewModel.lastDriveBackup.observe(this) { updateBackupWarning() }
}
override fun onResume() {
super.onResume()
updateBackupWarning()
}
override suspend fun setupPreferences(savedInstanceState: Bundle?) {
requires(BuildConfig.DEBUG, R.string.debug)
requires(appWidgetManager.widgetIds.isNotEmpty(), R.string.widget_settings)
}
private fun updateBackupWarning() {
val backupWarning =
preferences.showBackupWarnings()
&& (viewModel.usingPrivateStorage
|| viewModel.staleLocalBackup
|| viewModel.staleRemoteBackup)
(findPreference(R.string.backup_BPr_header) as IconPreference).iconVisible = backupWarning
}
}

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:background="?android:attr/selectableItemBackground"
android:clipToPadding="false"
android:baselineAligned="false">
<include layout="@layout/image_frame"/>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
android:ellipsize="marquee"/>
<TextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:layout_alignLeft="@android:id/title"
android:layout_alignStart="@android:id/title"
android:layout_gravity="start"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="10"
style="@style/PreferenceSummaryTextStyle"/>
</RelativeLayout>
<!-- Preference should place its actual preference widget here. -->
<ImageView
android:src="@drawable/ic_outline_error_outline_24px"
android:id="@+id/preference_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="end|center_vertical"
android:paddingStart="16dp"
android:paddingEnd="0dp"
android:alpha="@dimen/alpha_primary"
android:visibility="gone"
app:tint="@color/overdue"/>
</LinearLayout>

@ -374,6 +374,7 @@
<string name="p_just_updated">just_updated</string>
<string name="p_clicked_rate">clicked_rate</string>
<string name="p_backups_enabled">backups_enabled</string>
<string name="p_backups_ignore_warnings">backups_ignore_warnings</string>
<string name="p_backups_android_backup_enabled">backups_android_backup_enabled</string>
<string name="p_backups_android_backup_last">backups_android_backup_last</string>
<string name="p_current_version">cv</string>

@ -644,4 +644,7 @@ File %1$s contained %2$s.\n\n
<string name="account">Account</string>
<string name="foreground_location">Foreground location</string>
<string name="background_location">Background location</string>
<string name="backups_ignore_warnings">Ignore warnings</string>
<string name="backups_ignore_warnings_summary">Ignore backup warnings if you do not need backups or have your own backup solution</string>
<string name="backup_location_warning">WARNING: Files located in %s will be deleted if Tasks is uninstalled! Please choose a custom location to prevent Android from deleting your files.</string>
</resources>

@ -32,7 +32,9 @@
app:icon="@drawable/ic_outline_menu_24px"
app:title="@string/navigation_drawer" />
<Preference
<org.tasks.preferences.IconPreference
android:key="@string/backup_BPr_header"
android:layout="@layout/preference_icon"
app:fragment="org.tasks.preferences.fragments.Backups"
app:icon="@drawable/ic_outline_sd_storage_24px"
app:title="@string/backup_BPr_header" />

@ -66,4 +66,15 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/preferences_advanced">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/p_backups_ignore_warnings"
android:title="@string/backups_ignore_warnings"
android:summary="@string/backups_ignore_warnings_summary" />
</PreferenceCategory>
</PreferenceScreen>
Loading…
Cancel
Save