Automatic backups: Added all the functionality and tests

pull/2207/head
mihanentalpo 1 month ago
parent a9acbd6cab
commit 8c011af2e7

@ -32,6 +32,7 @@ rules = "1.6.1"
shadow = "8.1.1" shadow = "8.1.1"
sqliteJdbc = "3.45.1.0" sqliteJdbc = "3.45.1.0"
uiautomator = "2.3.0" uiautomator = "2.3.0"
work = "2.9.0"
[libraries] [libraries]
annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
@ -73,6 +74,7 @@ opencsv = { group = "com.opencsv", name = "opencsv", version.ref = "opencsv" }
rules = { group = "androidx.test", name = "rules", version.ref = "rules" } rules = { group = "androidx.test", name = "rules", version.ref = "rules" }
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" } sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" }
uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
[bundles] [bundles]
androidTest = [ androidTest = [

@ -108,6 +108,7 @@ dependencies {
implementation(libs.material) implementation(libs.material)
implementation(libs.opencsv) implementation(libs.opencsv)
implementation(libs.konfetti.xml) implementation(libs.konfetti.xml)
implementation(libs.work.runtime.ktx)
implementation(project(":uhabits-core")) implementation(project(":uhabits-core"))
ksp(libs.dagger.compiler) ksp(libs.dagger.compiler)

@ -27,10 +27,16 @@ import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.list.RESULT_BUG_REPORT import org.isoron.uhabits.activities.habits.list.RESULT_BUG_REPORT
@ -41,18 +47,32 @@ import org.isoron.uhabits.activities.habits.list.RESULT_REPAIR_DB
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
import org.isoron.uhabits.database.PublicBackupWorker
import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel
import org.isoron.uhabits.notifications.RingtoneManager import org.isoron.uhabits.notifications.RingtoneManager
import org.isoron.uhabits.utils.StyledResources import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.UriUtils
import org.isoron.uhabits.utils.startActivitySafely import org.isoron.uhabits.utils.startActivitySafely
import org.isoron.uhabits.widgets.WidgetUpdater import org.isoron.uhabits.widgets.WidgetUpdater
import java.util.Calendar import java.util.Calendar
import java.util.concurrent.TimeUnit
class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeListener { class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeListener {
private var sharedPrefs: SharedPreferences? = null private var sharedPrefs: SharedPreferences? = null
private var ringtoneManager: RingtoneManager? = null private var ringtoneManager: RingtoneManager? = null
private lateinit var prefs: Preferences private lateinit var prefs: Preferences
private var widgetUpdater: WidgetUpdater? = null private var widgetUpdater: WidgetUpdater? = null
private val selectFolderLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
if (uri != null) {
val flags =
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
requireContext().contentResolver.takePersistableUriPermission(uri, flags)
prefs.publicBackupUri = uri.toString()
val pref = findPreference("publicBackupFolder")
pref?.summary = UriUtils.getPathFromTreeUri(requireContext(), uri)
}
}
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@ -114,6 +134,10 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
activity?.startActivitySafely(intent) activity?.startActivitySafely(intent)
return true return true
} }
"publicBackupFolder" -> {
selectFolderLauncher.launch(null)
return true
}
} }
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
} }
@ -130,6 +154,17 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
updateWeekdayPreference() updateWeekdayPreference()
findPreference("reminderSound").isVisible = false findPreference("reminderSound").isVisible = false
val folderPref = findPreference("publicBackupFolder")
val uriStr = prefs.publicBackupUri
folderPref?.summary =
if (uriStr == null) {
getString(R.string.pref_public_backup_folder_summary)
} else {
UriUtils.getPathFromTreeUri(requireContext(), Uri.parse(uriStr))
}
scheduleAutoBackup(prefs.isPublicAutoBackupEnabled)
} }
private fun updateWeekdayPreference() { private fun updateWeekdayPreference() {
@ -151,6 +186,24 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
Log.d("SettingsFragment", "updating widgets") Log.d("SettingsFragment", "updating widgets")
widgetUpdater!!.updateWidgets() widgetUpdater!!.updateWidgets()
} }
if (key == "pref_public_auto_backup") {
val enabled = sharedPreferences.getBoolean("pref_public_auto_backup", false)
if (enabled && prefs.publicBackupUri == null) {
val pref = findPreference("pref_public_auto_backup") as SwitchPreferenceCompat?
pref?.isChecked = false
Toast.makeText(
context,
getString(R.string.pref_public_backup_folder_summary),
Toast.LENGTH_SHORT
).show()
return
}
scheduleAutoBackup(enabled)
}
if (key == "pref_public_backup_frequency") {
val enabled = sharedPreferences.getBoolean("pref_public_auto_backup", false)
scheduleAutoBackup(enabled)
}
BackupManager.dataChanged("org.isoron.uhabits") BackupManager.dataChanged("org.isoron.uhabits")
updateWeekdayPreference() updateWeekdayPreference()
} }
@ -192,6 +245,22 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
ringtonePreference.summary = ringtoneName ringtonePreference.summary = ringtoneName
} }
private fun scheduleAutoBackup(enabled: Boolean) {
val wm = WorkManager.getInstance(requireContext())
if (enabled) {
val interval = prefs.publicAutoBackupFrequency
val request =
PeriodicWorkRequestBuilder<PublicBackupWorker>(interval, TimeUnit.MINUTES).build()
wm.enqueueUniquePeriodicWork(
"public_auto_backup",
ExistingPeriodicWorkPolicy.UPDATE,
request
)
} else {
wm.cancelUniqueWork("public_auto_backup")
}
}
companion object { companion object {
private const val RINGTONE_REQUEST_CODE = 1 private const val RINGTONE_REQUEST_CODE = 1
} }

@ -0,0 +1,32 @@
package org.isoron.uhabits.database
import android.content.Context
import android.net.Uri
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.utils.DatabaseUtils
import org.isoron.uhabits.utils.UriUtils
import java.io.File
import java.io.IOException
class PublicBackupWorker(
appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
val app = applicationContext as HabitsApplication
val prefs = app.component.preferences
val uriString = prefs.publicBackupUri ?: return Result.failure()
val path = UriUtils.getPathFromTreeUri(applicationContext, Uri.parse(uriString))
?: return Result.failure()
return try {
val addDate = prefs.isPublicBackupAddDateEnabled
DatabaseUtils.saveDatabaseCopy(applicationContext, File(path), addDate)
Result.success()
} catch (e: IOException) {
Result.retry()
}
}
}

@ -58,10 +58,14 @@ object DatabaseUtils {
@JvmStatic @JvmStatic
@Throws(IOException::class) @Throws(IOException::class)
fun saveDatabaseCopy(context: Context, dir: File): String { fun saveDatabaseCopy(context: Context, dir: File, add_date: Boolean = true): String {
val dateFormat: SimpleDateFormat = getBackupDateFormat() val dateFormat: SimpleDateFormat = getBackupDateFormat()
val date = dateFormat.format(getLocalTime()) val filename = if (add_date) {
val filename = "${dir.absolutePath}/Loop Habits Backup $date.db" val date = dateFormat.format(getLocalTime())
"${dir.absolutePath}/Loop Habits Backup $date.db"
} else {
"${dir.absolutePath}/Loop Habits Backup.db"
}
Log.i("DatabaseUtils", "Writing: $filename") Log.i("DatabaseUtils", "Writing: $filename")
val db = getDatabaseFile(context) val db = getDatabaseFile(context)
val dbCopy = File(filename) val dbCopy = File(filename)

@ -0,0 +1,21 @@
package org.isoron.uhabits.utils
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
object UriUtils {
fun getPathFromTreeUri(context: Context, uri: Uri): String? {
val docId = DocumentsContract.getTreeDocumentId(uri)
val parts = docId.split(":")
val type = parts[0]
val relPath = if (parts.size > 1) parts[1] else ""
val base = if ("primary" == type) {
Environment.getExternalStorageDirectory().path
} else {
"/storage/" + type
}
return if (relPath.isEmpty()) base else "$base/$relPath"
}
}

@ -94,4 +94,24 @@
<item>51</item> <item>51</item>
<item>0</item> <item>0</item>
</string-array> </string-array>
<string-array name="public_backup_frequency_entries">
<item>@string/public_backup_frequency_15m</item>
<item>@string/public_backup_frequency_1h</item>
<item>@string/public_backup_frequency_1d</item>
<item>@string/public_backup_frequency_3d</item>
<item>@string/public_backup_frequency_7d</item>
<item>@string/public_backup_frequency_14d</item>
<item>@string/public_backup_frequency_1m</item>
</string-array>
<string-array name="public_backup_frequency_values" translatable="false">
<item>15</item>
<item>60</item>
<item>1440</item>
<item>4320</item>
<item>10080</item>
<item>20160</item>
<item>43200</item>
</string-array>
</resources> </resources>

@ -136,6 +136,42 @@
android:title="@string/import_data" android:title="@string/import_data"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<PreferenceCategory
android:key="automaticBackupCategory"
android:title="@string/automatic_backup">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_public_auto_backup"
android:title="@string/pref_public_auto_backup_title"
android:summary="@string/pref_public_auto_backup_summary"
app:iconSpaceReserved="false" />
<Preference
android:key="publicBackupFolder"
android:title="@string/pref_public_backup_folder_title"
android:summary="@string/pref_public_backup_folder_summary"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="pref_public_backup_add_date"
android:title="@string/pref_public_backup_add_date_title"
android:summary="@string/pref_public_backup_add_date_summary"
app:iconSpaceReserved="false" />
<ListPreference
android:key="pref_public_backup_frequency"
android:title="@string/pref_public_backup_frequency_title"
android:entries="@array/public_backup_frequency_entries"
android:entryValues="@array/public_backup_frequency_values"
android:defaultValue="15"
android:summary="%s"
android:dependency="pref_public_auto_backup"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory

@ -211,6 +211,37 @@ open class Preferences(private val storage: Storage) {
for (l in listeners) l.onQuestionMarksChanged() for (l in listeners) l.onQuestionMarksChanged()
} }
var publicBackupUri: String?
get() {
val uri = storage.getString("pref_public_backup_uri", "")
return if (uri.isEmpty()) null else uri
}
set(value) {
if (value == null) {
storage.remove("pref_public_backup_uri")
} else {
storage.putString("pref_public_backup_uri", value)
}
}
var isPublicAutoBackupEnabled: Boolean
get() = storage.getBoolean("pref_public_auto_backup", false)
set(enabled) {
storage.putBoolean("pref_public_auto_backup", enabled)
}
var isPublicBackupAddDateEnabled: Boolean
get() = storage.getBoolean("pref_public_backup_add_date", true)
set(enabled) {
storage.putBoolean("pref_public_backup_add_date", enabled)
}
var publicAutoBackupFrequency: Long
get() = storage.getString("pref_public_backup_frequency", "15").toLong()
set(value) {
storage.putString("pref_public_backup_frequency", value.toString())
}
/** /**
* @return An integer representing the first day of the week. Sunday * @return An integer representing the first day of the week. Sunday
* corresponds to 1, Monday to 2, and so on, until Saturday, which is * corresponds to 1, Monday to 2, and so on, until Saturday, which is

@ -169,4 +169,26 @@ class PreferencesTest : BaseUnitTest() {
prefs.isMidnightDelayEnabled = true prefs.isMidnightDelayEnabled = true
assertTrue(prefs.isMidnightDelayEnabled) assertTrue(prefs.isMidnightDelayEnabled)
} }
@Test
@Throws(Exception::class)
fun testPublicBackupPreferences() {
assertNull(prefs.publicBackupUri)
prefs.publicBackupUri = "content://test"
assertThat(prefs.publicBackupUri, equalTo("content://test"))
prefs.publicBackupUri = null
assertNull(prefs.publicBackupUri)
assertFalse(prefs.isPublicAutoBackupEnabled)
prefs.isPublicAutoBackupEnabled = true
assertTrue(prefs.isPublicAutoBackupEnabled)
assertTrue(prefs.isPublicBackupAddDateEnabled)
prefs.isPublicBackupAddDateEnabled = false
assertFalse(prefs.isPublicBackupAddDateEnabled)
assertThat(prefs.publicAutoBackupFrequency, equalTo(15L))
prefs.publicAutoBackupFrequency = 30
assertThat(prefs.publicAutoBackupFrequency, equalTo(30L))
}
} }

Loading…
Cancel
Save