Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ dependencies {
implementation(libs.coil.compose)
implementation(libs.coil.network.ktor3)
implementation(libs.ktor.client.android)
implementation(libs.androidx.work.runtime.ktx)

// Firebase dependencies - use platform BOM and then add implementations
implementation(platform(libs.firebase.bom))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.util.Log
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import app.cash.sqldelight.logs.LogSqliteDriver
import com.yogeshpaliyal.deepr.backup.AutoBackupWorker
import com.yogeshpaliyal.deepr.backup.ExportRepository
import com.yogeshpaliyal.deepr.backup.ExportRepositoryImpl
import com.yogeshpaliyal.deepr.backup.ImportRepository
Expand Down Expand Up @@ -64,11 +65,13 @@ class DeeprApplication : Application() {

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

single<AutoBackupWorker> { AutoBackupWorker(androidContext(), get(), get()) }

single {
HttpClient(CIO)
}

viewModel { AccountViewModel(get(), get(), get(), get(), get()) }
viewModel { AccountViewModel(get(), get(), get(), get(), get(), get()) }

single {
HtmlParser()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.yogeshpaliyal.deepr.backup

import android.content.Context
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import com.yogeshpaliyal.deepr.DeeprQueries
import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext

class AutoBackupWorker(
val context: Context,
val deeprQueries: DeeprQueries,
val preferenceDataStore: AppPreferenceDataStore,
) {
private val csvWriter by lazy {
CsvWriter()
}

suspend fun doWork() {
return withContext(Dispatchers.IO) {
try {
val enabled = preferenceDataStore.getAutoBackupEnabled.first()
if (!enabled) {
return@withContext
}

val location = preferenceDataStore.getAutoBackupLocation.first()
if (location.isEmpty()) {
return@withContext
}

val count = deeprQueries.countDeepr().executeAsOne()
if (count == 0L) {
return@withContext
}

val dataToExport = deeprQueries.listDeeprAsc().executeAsList()
if (dataToExport.isEmpty()) {
return@withContext
}

if (!location.startsWith("content://")) {
return@withContext
}

val fileName = "deepr_backup.csv"

val success = saveToSelectedLocation(location, fileName, dataToExport)

if (success) {
// Record backup time on successful completion
preferenceDataStore.setLastBackupTime(System.currentTimeMillis())
}
} catch (e: Exception) {
}
}
}

private fun saveToSelectedLocation(
location: String,
fileName: String,
data: List<com.yogeshpaliyal.deepr.Deepr>,
): Boolean =
try {
// For content:// URIs from document picker, create a new document in that folder
val locationUri = location.toUri()
val documentFile =
DocumentFile.fromTreeUri(
context,
locationUri,
)

val directory = DocumentFile.fromTreeUri(context, locationUri)
var docFile = directory?.findFile(fileName)
if (docFile == null) {
docFile =
DocumentFile.fromTreeUri(context, locationUri)?.createFile(
"text/csv",
fileName,
)
}

if (docFile != null) {
context.contentResolver
.openOutputStream(docFile.uri, "wt")
?.use { outputStream ->
csvWriter.writeToCsv(outputStream, data)
}
true
} else {
false
}
} catch (e: Exception) {
false
}
}
39 changes: 39 additions & 0 deletions app/src/main/java/com/yogeshpaliyal/deepr/backup/CsvWriter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.yogeshpaliyal.deepr.backup

import com.opencsv.CSVWriter
import com.yogeshpaliyal.deepr.Deepr
import com.yogeshpaliyal.deepr.util.Constants
import java.io.OutputStream

class CsvWriter {
fun writeToCsv(
outputStream: OutputStream,
data: List<Deepr>,
) {
outputStream.bufferedWriter().use { writer ->
// Write Header
CSVWriter(writer).use { csvWriter ->
// Write Header
csvWriter.writeNext(
arrayOf(
Constants.Header.LINK,
Constants.Header.CREATED_AT,
Constants.Header.OPENED_COUNT,
Constants.Header.NAME,
),
)
// Write Data
data.forEach { item ->
csvWriter.writeNext(
arrayOf(
item.link,
item.createdAt.toString(),
item.openedCount.toString(),
item.name,
),
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import com.yogeshpaliyal.deepr.Deepr
import com.yogeshpaliyal.deepr.DeeprQueries
import com.yogeshpaliyal.deepr.R
import com.yogeshpaliyal.deepr.util.Constants
import com.yogeshpaliyal.deepr.util.RequestResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
Expand All @@ -24,6 +21,10 @@ class ExportRepositoryImpl(
private val context: Context,
private val deeprQueries: DeeprQueries,
) : ExportRepository {
private val csvWriter by lazy {
CsvWriter()
}

override suspend fun exportToCsv(uri: Uri?): RequestResult<String> {
val count = deeprQueries.countDeepr().executeAsOne()
if (count == 0L) {
Expand All @@ -42,7 +43,7 @@ class ExportRepositoryImpl(
if (uri != null) {
return@withContext try {
context.contentResolver.openOutputStream(uri, "wt")?.use { outputStream ->
writeCsvData(outputStream, dataToExportInCsvFormat)
csvWriter.writeToCsv(outputStream, dataToExportInCsvFormat)
}
RequestResult.Success(context.getString(R.string.export_success, uri.toString()))
} catch (e: Exception) {
Expand All @@ -68,7 +69,7 @@ class ExportRepositoryImpl(

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

FileOutputStream(file).use { outputStream ->
writeCsvData(outputStream, dataToExportInCsvFormat)
csvWriter.writeToCsv(outputStream, dataToExportInCsvFormat)
}
RequestResult.Success(context.getString(R.string.export_success, file.absolutePath))
}
}
}

private fun writeCsvData(
outputStream: OutputStream,
data: List<Deepr>,
) {
outputStream.bufferedWriter().use { writer ->
// Write Header
writer.write(
"${Constants.Header.LINK},${Constants.Header.CREATED_AT},${Constants.Header.OPENED_COUNT},${Constants.Header.NAME}\n",
)
// Write Data
data.forEach { item ->
val row = "${item.link},${item.createdAt},${item.openedCount},${item.name}\n"
writer.write(row)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.yogeshpaliyal.deepr.backup

import android.content.Context
import android.net.Uri
import com.opencsv.CSVParserBuilder
import com.opencsv.CSVReaderBuilder
import com.opencsv.exceptions.CsvException
import com.yogeshpaliyal.deepr.DeeprQueries
Expand All @@ -20,8 +21,12 @@ class ImportRepositoryImpl(
try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
inputStream.reader().use { reader ->
val customParser =
CSVParserBuilder()
.build()
val csvReader =
CSVReaderBuilder(reader)
.withCSVParser(customParser)
.build()

// verify header first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class AppPreferenceDataStore(
private val SYNC_FILE_PATH = stringPreferencesKey("sync_file_path")
private val LAST_SYNC_TIME = longPreferencesKey("last_sync_time")
private val LANGUAGE_CODE = stringPreferencesKey("language_code")
private val AUTO_BACKUP_ENABLED = booleanPreferencesKey("auto_backup_enabled")
private val AUTO_BACKUP_LOCATION = stringPreferencesKey("auto_backup_location")
private val AUTO_BACKUP_INTERVAL = longPreferencesKey("auto_backup_interval")
private val LAST_BACKUP_TIME = longPreferencesKey("last_backup_time")
}

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

val getAutoBackupEnabled: Flow<Boolean> =
context.appDataStore.data.map { preferences ->
preferences[AUTO_BACKUP_ENABLED] ?: false // Default to disabled
}

val getAutoBackupLocation: Flow<String> =
context.appDataStore.data.map { preferences ->
preferences[AUTO_BACKUP_LOCATION] ?: "" // Default to empty path
}

val getLastBackupTime: Flow<Long> =
context.appDataStore.data.map { preferences ->
preferences[LAST_BACKUP_TIME] ?: 0L // Default to 0 (never backed up)
}

suspend fun setSortingOrder(order: @SortType String) {
context.appDataStore.edit { prefs ->
prefs[SORTING_ORDER] = order
Expand Down Expand Up @@ -91,4 +110,28 @@ class AppPreferenceDataStore(
prefs[LANGUAGE_CODE] = code
}
}

suspend fun setAutoBackupEnabled(enabled: Boolean) {
context.appDataStore.edit { prefs ->
prefs[AUTO_BACKUP_ENABLED] = enabled
}
}

suspend fun setAutoBackupLocation(location: String) {
context.appDataStore.edit { prefs ->
prefs[AUTO_BACKUP_LOCATION] = location
}
}

suspend fun setAutoBackupInterval(interval: Long) {
context.appDataStore.edit { prefs ->
prefs[AUTO_BACKUP_INTERVAL] = interval
}
}

suspend fun setLastBackupTime(timestamp: Long) {
context.appDataStore.edit { prefs ->
prefs[LAST_BACKUP_TIME] = timestamp
}
}
}
Loading