Skip to content

Commit 2e5592e

Browse files
Add auto backup option with configurable location (#169)
* Initial plan * Add auto backup feature with configurable location and interval Co-authored-by: yogeshpaliyal <[email protected]> * Fix auto backup to support user-selected location via document picker Co-authored-by: yogeshpaliyal <[email protected]> * feat: add auto backup functionality with CSV export support --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: yogeshpaliyal <[email protected]> Co-authored-by: Yogesh Choudhary Paliyal <[email protected]>
1 parent 42ea555 commit 2e5592e

File tree

11 files changed

+337
-25
lines changed

11 files changed

+337
-25
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ dependencies {
148148
implementation(libs.coil.compose)
149149
implementation(libs.coil.network.ktor3)
150150
implementation(libs.ktor.client.android)
151+
implementation(libs.androidx.work.runtime.ktx)
151152

152153
// Firebase dependencies - use platform BOM and then add implementations
153154
implementation(platform(libs.firebase.bom))

app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.util.Log
55
import app.cash.sqldelight.db.SqlDriver
66
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
77
import app.cash.sqldelight.logs.LogSqliteDriver
8+
import com.yogeshpaliyal.deepr.backup.AutoBackupWorker
89
import com.yogeshpaliyal.deepr.backup.ExportRepository
910
import com.yogeshpaliyal.deepr.backup.ExportRepositoryImpl
1011
import com.yogeshpaliyal.deepr.backup.ImportRepository
@@ -64,11 +65,13 @@ class DeeprApplication : Application() {
6465

6566
single<SyncRepository> { SyncRepositoryImpl(androidContext(), get(), get()) }
6667

68+
single<AutoBackupWorker> { AutoBackupWorker(androidContext(), get(), get()) }
69+
6770
single {
6871
HttpClient(CIO)
6972
}
7073

71-
viewModel { AccountViewModel(get(), get(), get(), get(), get()) }
74+
viewModel { AccountViewModel(get(), get(), get(), get(), get(), get()) }
7275

7376
single {
7477
HtmlParser()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.yogeshpaliyal.deepr.backup
2+
3+
import android.content.Context
4+
import androidx.core.net.toUri
5+
import androidx.documentfile.provider.DocumentFile
6+
import com.yogeshpaliyal.deepr.DeeprQueries
7+
import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.flow.first
10+
import kotlinx.coroutines.withContext
11+
12+
class AutoBackupWorker(
13+
val context: Context,
14+
val deeprQueries: DeeprQueries,
15+
val preferenceDataStore: AppPreferenceDataStore,
16+
) {
17+
private val csvWriter by lazy {
18+
CsvWriter()
19+
}
20+
21+
suspend fun doWork() {
22+
return withContext(Dispatchers.IO) {
23+
try {
24+
val enabled = preferenceDataStore.getAutoBackupEnabled.first()
25+
if (!enabled) {
26+
return@withContext
27+
}
28+
29+
val location = preferenceDataStore.getAutoBackupLocation.first()
30+
if (location.isEmpty()) {
31+
return@withContext
32+
}
33+
34+
val count = deeprQueries.countDeepr().executeAsOne()
35+
if (count == 0L) {
36+
return@withContext
37+
}
38+
39+
val dataToExport = deeprQueries.listDeeprAsc().executeAsList()
40+
if (dataToExport.isEmpty()) {
41+
return@withContext
42+
}
43+
44+
if (!location.startsWith("content://")) {
45+
return@withContext
46+
}
47+
48+
val fileName = "deepr_backup.csv"
49+
50+
val success = saveToSelectedLocation(location, fileName, dataToExport)
51+
52+
if (success) {
53+
// Record backup time on successful completion
54+
preferenceDataStore.setLastBackupTime(System.currentTimeMillis())
55+
}
56+
} catch (e: Exception) {
57+
}
58+
}
59+
}
60+
61+
private fun saveToSelectedLocation(
62+
location: String,
63+
fileName: String,
64+
data: List<com.yogeshpaliyal.deepr.Deepr>,
65+
): Boolean =
66+
try {
67+
// For content:// URIs from document picker, create a new document in that folder
68+
val locationUri = location.toUri()
69+
val documentFile =
70+
DocumentFile.fromTreeUri(
71+
context,
72+
locationUri,
73+
)
74+
75+
val directory = DocumentFile.fromTreeUri(context, locationUri)
76+
var docFile = directory?.findFile(fileName)
77+
if (docFile == null) {
78+
docFile =
79+
DocumentFile.fromTreeUri(context, locationUri)?.createFile(
80+
"text/csv",
81+
fileName,
82+
)
83+
}
84+
85+
if (docFile != null) {
86+
context.contentResolver
87+
.openOutputStream(docFile.uri, "wt")
88+
?.use { outputStream ->
89+
csvWriter.writeToCsv(outputStream, data)
90+
}
91+
true
92+
} else {
93+
false
94+
}
95+
} catch (e: Exception) {
96+
false
97+
}
98+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.yogeshpaliyal.deepr.backup
2+
3+
import com.opencsv.CSVWriter
4+
import com.yogeshpaliyal.deepr.Deepr
5+
import com.yogeshpaliyal.deepr.util.Constants
6+
import java.io.OutputStream
7+
8+
class CsvWriter {
9+
fun writeToCsv(
10+
outputStream: OutputStream,
11+
data: List<Deepr>,
12+
) {
13+
outputStream.bufferedWriter().use { writer ->
14+
// Write Header
15+
CSVWriter(writer).use { csvWriter ->
16+
// Write Header
17+
csvWriter.writeNext(
18+
arrayOf(
19+
Constants.Header.LINK,
20+
Constants.Header.CREATED_AT,
21+
Constants.Header.OPENED_COUNT,
22+
Constants.Header.NAME,
23+
),
24+
)
25+
// Write Data
26+
data.forEach { item ->
27+
csvWriter.writeNext(
28+
arrayOf(
29+
item.link,
30+
item.createdAt.toString(),
31+
item.openedCount.toString(),
32+
item.name,
33+
),
34+
)
35+
}
36+
}
37+
}
38+
}
39+
}

app/src/main/java/com/yogeshpaliyal/deepr/backup/ExportRepositoryImpl.kt

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,13 @@ import android.net.Uri
66
import android.os.Build
77
import android.os.Environment
88
import android.provider.MediaStore
9-
import com.yogeshpaliyal.deepr.Deepr
109
import com.yogeshpaliyal.deepr.DeeprQueries
1110
import com.yogeshpaliyal.deepr.R
12-
import com.yogeshpaliyal.deepr.util.Constants
1311
import com.yogeshpaliyal.deepr.util.RequestResult
1412
import kotlinx.coroutines.Dispatchers
1513
import kotlinx.coroutines.withContext
1614
import java.io.File
1715
import java.io.FileOutputStream
18-
import java.io.OutputStream
1916
import java.text.SimpleDateFormat
2017
import java.util.Date
2118
import java.util.Locale
@@ -24,6 +21,10 @@ class ExportRepositoryImpl(
2421
private val context: Context,
2522
private val deeprQueries: DeeprQueries,
2623
) : ExportRepository {
24+
private val csvWriter by lazy {
25+
CsvWriter()
26+
}
27+
2728
override suspend fun exportToCsv(uri: Uri?): RequestResult<String> {
2829
val count = deeprQueries.countDeepr().executeAsOne()
2930
if (count == 0L) {
@@ -42,7 +43,7 @@ class ExportRepositoryImpl(
4243
if (uri != null) {
4344
return@withContext try {
4445
context.contentResolver.openOutputStream(uri, "wt")?.use { outputStream ->
45-
writeCsvData(outputStream, dataToExportInCsvFormat)
46+
csvWriter.writeToCsv(outputStream, dataToExportInCsvFormat)
4647
}
4748
RequestResult.Success(context.getString(R.string.export_success, uri.toString()))
4849
} catch (e: Exception) {
@@ -68,7 +69,7 @@ class ExportRepositoryImpl(
6869

6970
if (defaultUri != null) {
7071
resolver.openOutputStream(defaultUri)?.use { outputStream ->
71-
writeCsvData(outputStream, dataToExportInCsvFormat)
72+
csvWriter.writeToCsv(outputStream, dataToExportInCsvFormat)
7273
}
7374
RequestResult.Success(context.getString(R.string.export_success, "${Environment.DIRECTORY_DOWNLOADS}/Deepr/$fileName"))
7475
} else {
@@ -85,27 +86,10 @@ class ExportRepositoryImpl(
8586
val file = File(downloadsDir, fileName)
8687

8788
FileOutputStream(file).use { outputStream ->
88-
writeCsvData(outputStream, dataToExportInCsvFormat)
89+
csvWriter.writeToCsv(outputStream, dataToExportInCsvFormat)
8990
}
9091
RequestResult.Success(context.getString(R.string.export_success, file.absolutePath))
9192
}
9293
}
9394
}
94-
95-
private fun writeCsvData(
96-
outputStream: OutputStream,
97-
data: List<Deepr>,
98-
) {
99-
outputStream.bufferedWriter().use { writer ->
100-
// Write Header
101-
writer.write(
102-
"${Constants.Header.LINK},${Constants.Header.CREATED_AT},${Constants.Header.OPENED_COUNT},${Constants.Header.NAME}\n",
103-
)
104-
// Write Data
105-
data.forEach { item ->
106-
val row = "${item.link},${item.createdAt},${item.openedCount},${item.name}\n"
107-
writer.write(row)
108-
}
109-
}
110-
}
11195
}

app/src/main/java/com/yogeshpaliyal/deepr/backup/ImportRepositoryImpl.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.yogeshpaliyal.deepr.backup
22

33
import android.content.Context
44
import android.net.Uri
5+
import com.opencsv.CSVParserBuilder
56
import com.opencsv.CSVReaderBuilder
67
import com.opencsv.exceptions.CsvException
78
import com.yogeshpaliyal.deepr.DeeprQueries
@@ -20,8 +21,12 @@ class ImportRepositoryImpl(
2021
try {
2122
context.contentResolver.openInputStream(uri)?.use { inputStream ->
2223
inputStream.reader().use { reader ->
24+
val customParser =
25+
CSVParserBuilder()
26+
.build()
2327
val csvReader =
2428
CSVReaderBuilder(reader)
29+
.withCSVParser(customParser)
2530
.build()
2631

2732
// verify header first

app/src/main/java/com/yogeshpaliyal/deepr/preference/AppPreferenceDataStore.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ class AppPreferenceDataStore(
2424
private val SYNC_FILE_PATH = stringPreferencesKey("sync_file_path")
2525
private val LAST_SYNC_TIME = longPreferencesKey("last_sync_time")
2626
private val LANGUAGE_CODE = stringPreferencesKey("language_code")
27+
private val AUTO_BACKUP_ENABLED = booleanPreferencesKey("auto_backup_enabled")
28+
private val AUTO_BACKUP_LOCATION = stringPreferencesKey("auto_backup_location")
29+
private val AUTO_BACKUP_INTERVAL = longPreferencesKey("auto_backup_interval")
30+
private val LAST_BACKUP_TIME = longPreferencesKey("last_backup_time")
2731
}
2832

2933
val getSortingOrder: Flow<@SortType String> =
@@ -56,6 +60,21 @@ class AppPreferenceDataStore(
5660
preferences[LANGUAGE_CODE] ?: "" // Default to system language
5761
}
5862

63+
val getAutoBackupEnabled: Flow<Boolean> =
64+
context.appDataStore.data.map { preferences ->
65+
preferences[AUTO_BACKUP_ENABLED] ?: false // Default to disabled
66+
}
67+
68+
val getAutoBackupLocation: Flow<String> =
69+
context.appDataStore.data.map { preferences ->
70+
preferences[AUTO_BACKUP_LOCATION] ?: "" // Default to empty path
71+
}
72+
73+
val getLastBackupTime: Flow<Long> =
74+
context.appDataStore.data.map { preferences ->
75+
preferences[LAST_BACKUP_TIME] ?: 0L // Default to 0 (never backed up)
76+
}
77+
5978
suspend fun setSortingOrder(order: @SortType String) {
6079
context.appDataStore.edit { prefs ->
6180
prefs[SORTING_ORDER] = order
@@ -91,4 +110,28 @@ class AppPreferenceDataStore(
91110
prefs[LANGUAGE_CODE] = code
92111
}
93112
}
113+
114+
suspend fun setAutoBackupEnabled(enabled: Boolean) {
115+
context.appDataStore.edit { prefs ->
116+
prefs[AUTO_BACKUP_ENABLED] = enabled
117+
}
118+
}
119+
120+
suspend fun setAutoBackupLocation(location: String) {
121+
context.appDataStore.edit { prefs ->
122+
prefs[AUTO_BACKUP_LOCATION] = location
123+
}
124+
}
125+
126+
suspend fun setAutoBackupInterval(interval: Long) {
127+
context.appDataStore.edit { prefs ->
128+
prefs[AUTO_BACKUP_INTERVAL] = interval
129+
}
130+
}
131+
132+
suspend fun setLastBackupTime(timestamp: Long) {
133+
context.appDataStore.edit { prefs ->
134+
prefs[LAST_BACKUP_TIME] = timestamp
135+
}
136+
}
94137
}

0 commit comments

Comments
 (0)