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"
sqliteJdbc = "3.45.1.0"
uiautomator = "2.3.0"
work = "2.9.0"
[libraries]
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" }
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" }
uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
[bundles]
androidTest = [

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

@ -27,10 +27,16 @@ import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
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.R
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.ui.NotificationTray
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.RingtoneManager
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.UriUtils
import org.isoron.uhabits.utils.startActivitySafely
import org.isoron.uhabits.widgets.WidgetUpdater
import java.util.Calendar
import java.util.concurrent.TimeUnit
class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeListener {
private var sharedPrefs: SharedPreferences? = null
private var ringtoneManager: RingtoneManager? = null
private lateinit var prefs: Preferences
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")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@ -114,6 +134,10 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
activity?.startActivitySafely(intent)
return true
}
"publicBackupFolder" -> {
selectFolderLauncher.launch(null)
return true
}
}
return super.onPreferenceTreeClick(preference)
}
@ -130,6 +154,17 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
updateWeekdayPreference()
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() {
@ -151,6 +186,24 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
Log.d("SettingsFragment", "updating widgets")
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")
updateWeekdayPreference()
}
@ -192,6 +245,22 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
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 {
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
@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 filename = if (add_date) {
val date = dateFormat.format(getLocalTime())
val filename = "${dir.absolutePath}/Loop Habits Backup $date.db"
"${dir.absolutePath}/Loop Habits Backup $date.db"
} else {
"${dir.absolutePath}/Loop Habits Backup.db"
}
Log.i("DatabaseUtils", "Writing: $filename")
val db = getDatabaseFile(context)
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>0</item>
</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>

@ -136,6 +136,42 @@
android:title="@string/import_data"
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

@ -211,6 +211,37 @@ open class Preferences(private val storage: Storage) {
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
* corresponds to 1, Monday to 2, and so on, until Saturday, which is

@ -169,4 +169,26 @@ class PreferencesTest : BaseUnitTest() {
prefs.isMidnightDelayEnabled = true
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