Skip to content

Commit

Permalink
Rethink backup feature: all in settings - Import in settings too
Browse files Browse the repository at this point in the history
  • Loading branch information
Loïc Teyssier committed Jan 22, 2024
1 parent ef526db commit a010e53
Show file tree
Hide file tree
Showing 57 changed files with 1,124 additions and 758 deletions.
6 changes: 5 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ android {
multiDexEnabled true
applicationId "com.lolo.io.onelist"
compileSdk 34
minSdkVersion 19
minSdkVersion 23
targetSdkVersion 34
versionCode 14
versionName "1.4.1"
vectorDrawables.useSupportLibrary = true
}

androidResources {
generateLocaleConfig true
}

buildFeatures {
viewBinding true
}
Expand Down
1 change: 0 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />

<application
android:name="com.lolo.io.onelist.App"
Expand Down
13 changes: 11 additions & 2 deletions app/src/main/java/com/lolo/io/onelist/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import android.view.MotionEvent
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import com.anggrayudi.storage.SimpleStorageHelper
import com.lolo.io.onelist.core.data.persistence.PersistenceHelper
import com.lolo.io.onelist.core.data.shared_preferences.SharedPreferencesHelper
import com.lolo.io.onelist.core.ui.Config
import com.lolo.io.onelist.core.ui.REQUEST_CODE_OPEN_DOCUMENT
import com.lolo.io.onelist.core.ui.REQUEST_CODE_OPEN_DOCUMENT_TREE
Expand All @@ -21,7 +21,7 @@ class MainActivity : AppCompatActivity(), StorageHelperHolder {

override val storageHelper = SimpleStorageHelper(this)

val persistence by inject<PersistenceHelper>()
val persistence by inject<SharedPreferencesHelper>()

// On some devices, displaying storage chooser fragment before activity is resumed leads to a crash.
// This is a workaround.
Expand Down Expand Up @@ -77,6 +77,15 @@ class MainActivity : AppCompatActivity(), StorageHelperHolder {
*/
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
println()
}

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
supportFragmentManager.fragments.filterIsInstance<OnDispatchTouchEvent>().forEach {
it.onDispatchTouchEvent(ev)
Expand Down
36 changes: 27 additions & 9 deletions app/src/main/java/com/lolo/io/onelist/core/data/di/AppModule.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
package com.lolo.io.onelist.core.data.di

import androidx.room.Room
import com.lolo.io.onelist.core.data.persistence.PersistenceHelper
import com.lolo.io.onelist.core.data.file_access.FileAccess
import com.lolo.io.onelist.core.data.reporitory.OneListRepository
import com.lolo.io.onelist.core.data.shared_preferences.SharedPreferencesHelper
import com.lolo.io.onelist.core.database.OneListDatabase
import com.lolo.io.onelist.core.database.dao.ItemListDao
import com.lolo.io.onelist.core.domain.use_cases.CreateList
import com.lolo.io.onelist.core.domain.use_cases.EditList
import com.lolo.io.onelist.core.domain.use_cases.GetAllLists
import com.lolo.io.onelist.core.domain.use_cases.HandleFirstLaunch
import com.lolo.io.onelist.core.domain.use_cases.ImportList
import com.lolo.io.onelist.core.domain.use_cases.MoveList
import com.lolo.io.onelist.core.domain.use_cases.OneListUseCases
import com.lolo.io.onelist.core.domain.use_cases.RemoveList
import com.lolo.io.onelist.core.domain.use_cases.SelectedListIndex
import com.lolo.io.onelist.core.domain.use_cases.UpsertList
import com.lolo.io.onelist.core.domain.use_cases.Version
import com.lolo.io.onelist.core.domain.use_cases.SetBackupUri
import com.lolo.io.onelist.core.domain.use_cases.ShowWhatsNew
import com.lolo.io.onelist.core.domain.use_cases.SyncAllLists
import org.koin.dsl.module

val appModule = module {

single {
PersistenceHelper(get(), get())
SharedPreferencesHelper(get())
}

single<OneListDatabase> {
Expand All @@ -29,17 +35,29 @@ val appModule = module {

single {
OneListUseCases(
upsertList = UpsertList(get()),
createList = CreateList(get()),
getAllLists = GetAllLists(
(get())
),
removeList = RemoveList((get())),
selectedListIndex = SelectedListIndex((get())),
handleFirstLaunch = HandleFirstLaunch(get()),
version = Version(get())
handleFirstLaunch = HandleFirstLaunch(get(), get()),
editList = EditList(get()),
importList = ImportList(get()),
moveList = MoveList(get()),
setBackupUri = SetBackupUri(get()),
syncAllLists = SyncAllLists(get()),
showWhatsNew = ShowWhatsNew(get())
)
}

single {
OneListRepository(get(), get(), get())
}

single {
FileAccess(get())
}

single<ItemListDao> {
val database = get<OneListDatabase>()
database.itemListDao
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package com.lolo.io.onelist.core.data.file_access

import android.app.Application
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.makeFile
import com.google.gson.Gson
import com.google.gson.JsonIOException
import com.google.gson.JsonSyntaxException
import com.lolo.io.onelist.R
import com.lolo.io.onelist.core.model.ItemList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileNotFoundException
import java.io.IOException

class FileAccess(
val app: Application,
) {
private val coroutineIOScope = CoroutineScope(Dispatchers.IO)
private val gson = Gson()


private val Uri.canWrite
get() =
DocumentFileCompat.fromUri(app, this)?.canWrite() == true

private fun Uri.isIntoBackupFolder(backupUri: Uri): Boolean =
DocumentFileCompat.fromUri(app, this)?.getAbsolutePath(app)
?.startsWith(
DocumentFileCompat.fromUri(app, backupUri)?.getAbsolutePath(app)
?: throw IllegalArgumentException("Backup uri could not be parsed")
) == true

private val Uri.fileExists
get() =
DocumentFileCompat.fromUri(app, this)?.exists() == true

@Throws(
FileNotFoundException::class,
JsonSyntaxException::class,
JsonIOException::class,
SecurityException::class
)
suspend fun getListFromLocalFile(list: ItemList): ItemList {
Log.d("1LogD", list.title)
return coroutineIOScope.async(SupervisorJob()) {
val listFromFile = list.uri?.let { uri ->
app.contentResolver.openInputStream(uri).use {
gson.fromJson(it?.reader(), ItemList::class.java)
}
} ?: list
listFromFile.apply {
uri = list.uri
}
}.await()
}


suspend fun saveListFile(
backupUri: String?,
list: ItemList,
onNewFileCreated: suspend (ItemList, Uri?) -> Unit
): ItemList {
if (backupUri != null) {
list.uri.let {
if (it == null
|| !it.fileExists
|| !it.isIntoBackupFolder(Uri.parse(backupUri))
|| !it.canWrite
) {
val uri = createListFile(backupUri, list)?.uri
onNewFileCreated(list, uri)
}
}

list.uri?.let { uri ->
try {
app.contentResolver.openOutputStream(uri, "wt").use { out ->
out?.write(
gson.toJson(list).toByteArray(Charsets.UTF_8)
)
}
} catch (e: Exception) {
coroutineIOScope.launch {
Toast.makeText(
app,
app.getString(
R.string.error_saving_to_path,
list.title
), // todo change to path to just error while saving list
Toast.LENGTH_SHORT
).show()
}
}
}
}
return list
}

private fun createListFile(backupUri: String, list: ItemList): DocumentFile? {
val folderUri = Uri.parse(backupUri)
return Uri.parse(backupUri)?.let {
return if (DocumentFileCompat.fromUri(app, it)?.canWrite() == true) {
val folder = DocumentFileCompat.fromUri(app, folderUri)
folder?.makeFile(app, "${list.title}-${list.id}.1list")
} else null
}
}


// TODO Toasts should not be here !
fun deleteListBackupFile(list: ItemList) {
coroutineIOScope.launch {
list.uri?.let { uri ->
try {
DocumentFile.fromSingleUri(app, uri)?.delete()
withContext(Dispatchers.Main) {
Toast.makeText(
app,
app.getString(R.string.file_deleted),
Toast.LENGTH_LONG
).show()
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
Toast.makeText(
app,
app.getString(R.string.error_deleting_list_file),
Toast.LENGTH_SHORT
).show()
}
}
}
}
}


suspend fun createListFromUri(
uri: Uri, onListCreated: suspend (list: ItemList) -> Unit
): ItemList {
return withContext(Dispatchers.IO) {
try {
val gson = Gson()
val content =
app.contentResolver.openInputStream(uri)
.use { iss -> iss?.bufferedReader()?.use { it.readText() } }
// return :
gson.fromJson(content, ItemList::class.java).also {
onListCreated(it)
}
} catch (e: IllegalArgumentException) {
throw e
} catch (e: Exception) {
throw IOException(app.getString(R.string.error_opening_file))
}
}
}

suspend fun saveAllListToFiles(
backupUri: String,
lists: List<ItemList>,
onNewFileCreated: (ItemList, Uri?) -> Unit
) {
return withContext(Dispatchers.IO) {
lists.forEach {
saveListFile(backupUri, it, onNewFileCreated)
}
}
}

fun revokeAllAccessFolders() {
DocumentFileCompat.getAccessibleUris(app)
.flatMap { it.value }
.forEach {
app.contentResolver.releasePersistableUriPermission(
it,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
}


}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.lolo.io.onelist.core.data.migration

import com.lolo.io.onelist.MainActivity
import com.lolo.io.onelist.core.data.persistence.PersistenceHelper
import com.lolo.io.onelist.core.data.shared_preferences.SharedPreferencesHelper

class UpdateHelper(
val persistenceHelper: PersistenceHelper
val persistenceHelper: SharedPreferencesHelper
) {
fun applyUpdatePatches() {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.lolo.io.onelist.core.data.model

import com.lolo.io.onelist.core.model.ItemList

data class AllListsWithErrors(
val lists: List<ItemList> = listOf(),
val errors: List<ErrorLoadingList> = listOf(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.lolo.io.onelist.core.data.model

sealed class ErrorLoadingList {
data object FileMissingError: ErrorLoadingList()
data object FileCorruptedError: ErrorLoadingList()
data object PermissionDeniedError: ErrorLoadingList()
data object UnknownError: ErrorLoadingList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.lolo.io.onelist.core.data.model

sealed class Resource<out T> {
data class Success<out T>(val value: T): Resource<T>()
data object Loading: Resource<Nothing>()
data class Error<out T>(val code: Int): Resource<T>()
}
Loading

0 comments on commit a010e53

Please sign in to comment.