diff --git a/app/src/main/java/org/tasks/preferences/IconPreference.kt b/app/src/main/java/org/tasks/preferences/IconPreference.kt new file mode 100644 index 000000000..a165b640e --- /dev/null +++ b/app/src/main/java/org/tasks/preferences/IconPreference.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/preferences/Preferences.kt b/app/src/main/java/org/tasks/preferences/Preferences.kt index 16ad52999..bcb4c7427 100644 --- a/app/src/main/java/org/tasks/preferences/Preferences.kt +++ b/app/src/main/java/org/tasks/preferences/Preferences.kt @@ -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) diff --git a/app/src/main/java/org/tasks/preferences/PreferencesViewModel.kt b/app/src/main/java/org/tasks/preferences/PreferencesViewModel.kt index 334df1805..e38b02266 100644 --- a/app/src/main/java/org/tasks/preferences/PreferencesViewModel.kt +++ b/app/src/main/java/org/tasks/preferences/PreferencesViewModel.kt @@ -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() val lastAndroidBackup = MutableLiveData() + 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() { diff --git a/app/src/main/java/org/tasks/preferences/fragments/Backups.kt b/app/src/main/java/org/tasks/preferences/fragments/Backups.kt index 143db0190..bdab173f0 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/Backups.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/Backups.kt @@ -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, @@ -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 + } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt b/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt index 9ea856734..d7bd7662a 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt @@ -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 + } } diff --git a/app/src/main/res/layout/preference_icon.xml b/app/src/main/res/layout/preference_icon.xml new file mode 100644 index 000000000..76b161c55 --- /dev/null +++ b/app/src/main/res/layout/preference_icon.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 2c6769363..5ddd7bd2c 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -374,6 +374,7 @@ just_updated clicked_rate backups_enabled + backups_ignore_warnings backups_android_backup_enabled backups_android_backup_last cv diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 452a2e9c7..93368a421 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -644,4 +644,7 @@ File %1$s contained %2$s.\n\n Account Foreground location Background location + Ignore warnings + Ignore backup warnings if you do not need backups or have your own backup solution + WARNING: Files located in %s will be deleted if Tasks is uninstalled! Please choose a custom location to prevent Android from deleting your files. diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 5e2e03b51..5934e6a37 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -32,7 +32,9 @@ app:icon="@drawable/ic_outline_menu_24px" app:title="@string/navigation_drawer" /> - diff --git a/app/src/main/res/xml/preferences_backups.xml b/app/src/main/res/xml/preferences_backups.xml index fc949b796..17dbb2405 100644 --- a/app/src/main/res/xml/preferences_backups.xml +++ b/app/src/main/res/xml/preferences_backups.xml @@ -66,4 +66,15 @@ + + + + + + \ No newline at end of file