diff --git a/README.md b/README.md
index 0450631d..71ea4b99 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
## Target platforms :
-API 16 or later
+API 23 or later
## Features :
@@ -31,18 +31,6 @@ API 16 or later
## Donations :
-[ Buy me a beer](https://www.paypal.me/loicteyssier/5)
-
-[ Buy me a pizza](https://www.paypal.me/loicteyssier/10)
-
-[ Buy me a meal](https://www.paypal.me/loicteyssier/20)
-
-[ Buy me whatever you want](https://www.paypal.me/loicteyssier)
+ height="20"> Buy me a beer](https://www.paypal.com/donate/?business=Z32JPDRAJV2ZQ&no_recurring=0&item_name=1List+App¤cy_code=EUR)
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 9754d154..28c115e1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -14,8 +14,8 @@ android {
compileSdk 34
minSdkVersion 23
targetSdkVersion 34
- versionCode 14
- versionName "1.4.1"
+ versionCode 17
+ versionName "1.4.0"
vectorDrawables.useSupportLibrary = true
}
@@ -25,16 +25,16 @@ android {
buildFeatures {
viewBinding true
+ buildConfig = true
}
signingConfigs {
release {
- storeFile file('C:\\Users\\Loic\\Dropbox (Personal)\\.keys\\keystores\\android.jks')
- storePassword '7c3c1779c148a6ff3d5ee87e7ef70f81'
- keyAlias 'OneListKey'
- keyPassword '97220be66f2b279723c09f770ab45089'
+ storeFile = file(System.getenv("ONELIST_KEYSTORE_PATH"))
+ storePassword = System.getenv("ONELIST_KEYSTORE_PASSWORD")
+ keyAlias = System.getenv("ONELIST_KEYSTORE_ALIAS")
+ keyPassword = System.getenv("ONELIST_KEYSTORE_ALIAS_PASSWORD")
}
- release
}
buildTypes {
@@ -68,6 +68,8 @@ repositories {
}
dependencies {
+ def room_version = "2.6.1"
+
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
implementation 'androidx.appcompat:appcompat:1.6.1'
@@ -92,21 +94,7 @@ dependencies {
implementation "io.insert-koin:koin-android:3.5.0"
implementation "io.insert-koin:koin-androidx-navigation:3.5.0"
- def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
ksp "androidx.room:room-compiler:$room_version"
-
-}
-
-def props = new Properties()
-if (rootProject.file("release.properties").exists()) {
- props.load(new FileInputStream(rootProject.file("release.properties")))
- android.signingConfigs.release.storeFile file(props.keyStore)
- android.signingConfigs.release.storePassword props.keyStorePassword
- android.signingConfigs.release.keyAlias props.keyAlias
- android.signingConfigs.release.keyPassword props.keyAliasPassword
-} else {
- project.logger.info('INFO: Set the values storeFile, storePassword, keyAlias, and keyPassword in release.properties to sign the release.')
- android.buildTypes.release.signingConfig = null
}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 00000000..50746776
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,23 @@
+-dontwarn com.fasterxml.jackson.core.**
+-dontwarn com.google.common.annotations.**
+-dontwarn javax.ws.rs.**
+-dontwarn org.immutables.value.**
+
+# R8 GSON special rules :
+# https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md => Troubleshooting
+# For data classes used for serialization all fields that are used in the serialization must be kept by the configuration. R8 can decide to replace instances of types that are never instantiated with null. So if instances of a given class are only created through deserialization from JSON, R8 will not see that class as instantiated leaving it as always null.
+# If the @SerializedName annotation is not used the following conservative rule can be used for each data class :
+
+-keepclassmembers class com.lolo.io.onelist.core.model.ItemList {
+ !transient ;
+}
+-keepclassmembers class com.lolo.io.onelist.core.model.Item {
+ !transient ;
+}
+
+# GSON uses type tokens to serialize and deserialize generic types.
+# The anonymous class will have a generic signature argument of List to the super type TypeToken that is reflective read for serialization. It is therefore necessary to keep both the Signature attribute, the com.google.gson.reflect.TypeToken class and all sub-types.
+
+-keep class com.google.gson.reflect.TypeToken
+-keep class * extends com.google.gson.reflect.TypeToken
+-keep public class * implements java.lang.reflect.Type
diff --git a/app/src/main/java/com/lolo/io/onelist/App.kt b/app/src/main/java/com/lolo/io/onelist/App.kt
index 7f152120..99aae152 100644
--- a/app/src/main/java/com/lolo/io/onelist/App.kt
+++ b/app/src/main/java/com/lolo/io/onelist/App.kt
@@ -9,6 +9,7 @@ import org.koin.androidx.fragment.koin.fragmentFactory
import org.koin.core.context.startKoin
class App : Application() {
+
override fun onCreate() {
super.onCreate()
startKoin {
diff --git a/app/src/main/java/com/lolo/io/onelist/MainActivity.kt b/app/src/main/java/com/lolo/io/onelist/MainActivity.kt
index 16e2c379..cb064f98 100644
--- a/app/src/main/java/com/lolo/io/onelist/MainActivity.kt
+++ b/app/src/main/java/com/lolo/io/onelist/MainActivity.kt
@@ -1,9 +1,7 @@
package com.lolo.io.onelist
import android.content.Context
-import android.content.Intent
import android.content.res.Configuration
-import android.os.Build
import android.os.Bundle
import android.view.MotionEvent
import androidx.appcompat.app.AppCompatActivity
@@ -11,8 +9,6 @@ import androidx.appcompat.app.AppCompatDelegate
import com.anggrayudi.storage.SimpleStorageHelper
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
import com.lolo.io.onelist.feature.lists.OneListFragment
import com.lolo.io.onelist.feature.lists.utils.StorageHelperHolder
import org.koin.android.ext.android.inject
@@ -21,18 +17,7 @@ class MainActivity : AppCompatActivity(), StorageHelperHolder {
override val storageHelper = SimpleStorageHelper(this)
- val persistence by inject()
-
- // On some devices, displaying storage chooser fragment before activity is resumed leads to a crash.
- // This is a workaround.
- var whenResumed = {}
- set(value) {
- if (this.isResumed) value()
- else field = value
- }
- private var isResumed = false
-
- var onPathChosenActivityResult: (String) -> Any? = {}
+ private val preferences by inject()
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme)
@@ -63,18 +48,6 @@ class MainActivity : AppCompatActivity(), StorageHelperHolder {
.replace(R.id.fragmentContainer, fragment, "OneListFragment")
.commit()
-
- //supportFragmentManager.beginTransaction().add(R.id.fragmentContainer).commit()
-
-
- /* todo migrate whatsnew to a normal fragment
- // WORKAROUND FOR WHATSNEW LIB NOT HANDLING WELL CONFIG CHANGES
- if (savedInstanceState != null) {
- supportFragmentManager.findFragmentByTag(WhatsNew.TAG)
- ?.let { supportFragmentManager.beginTransaction().remove(it).commit() }
- ?.let { WhatsNew.releasesNotes.entries.last().value().show(this) }
- }
- */
}
override fun onRequestPermissionsResult(
@@ -93,19 +66,6 @@ class MainActivity : AppCompatActivity(), StorageHelperHolder {
return super.dispatchTouchEvent(ev)
}
- override fun onResume() {
- super.onResume()
-
- whenResumed()
- whenResumed = {}
- isResumed = true
- }
-
- override fun onPause() {
- super.onPause()
- isResumed = false
- }
-
interface OnDispatchTouchEvent {
fun onDispatchTouchEvent(ev: MotionEvent)
}
@@ -115,22 +75,6 @@ class MainActivity : AppCompatActivity(), StorageHelperHolder {
return true
}
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
-
- if (requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE || requestCode == REQUEST_CODE_OPEN_DOCUMENT)
- data?.data?.let { uri ->
- contentResolver.takePersistableUriPermission(
- uri,
- Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- )
- onPathChosenActivityResult(uri.toString())
- onPathChosenActivityResult = { }
- }
- }
- }
-
override fun attachBaseContext(newBase: Context) {
val context: Context = updateThemeConfiguration(newBase)
super.attachBaseContext(context)
@@ -138,7 +82,7 @@ class MainActivity : AppCompatActivity(), StorageHelperHolder {
private fun updateThemeConfiguration(context: Context): Context {
var mode = context.resources.configuration.uiMode
- when (persistence.theme) {
+ when (preferences.theme) {
"light" -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
mode = Configuration.UI_MODE_NIGHT_NO;
@@ -154,12 +98,6 @@ class MainActivity : AppCompatActivity(), StorageHelperHolder {
val config = Configuration(context.resources.configuration)
config.uiMode = mode
- var ctx = context
- if (Build.VERSION.SDK_INT >= 17) {
- ctx = context.createConfigurationContext(config)
- } else {
- context.resources.updateConfiguration(config, context.resources.displayMetrics)
- }
- return ctx
+ return context.createConfigurationContext(config)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lolo/io/onelist/core/data/file_access/FileAccess.kt b/app/src/main/java/com/lolo/io/onelist/core/data/file_access/FileAccess.kt
index 4043ed96..ec34d2e3 100644
--- a/app/src/main/java/com/lolo/io/onelist/core/data/file_access/FileAccess.kt
+++ b/app/src/main/java/com/lolo/io/onelist/core/data/file_access/FileAccess.kt
@@ -8,6 +8,7 @@ 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.isTreeDocumentFile
import com.anggrayudi.storage.file.makeFile
import com.google.gson.Gson
import com.google.gson.JsonIOException
@@ -19,6 +20,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
+import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import java.io.FileNotFoundException
import java.io.IOException
@@ -34,12 +36,8 @@ class FileAccess(
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 fun Uri.isIntoBackupFolder(): Boolean =
+ DocumentFileCompat.fromUri(app, this)?.isTreeDocumentFile == true
private val Uri.fileExists
get() =
@@ -52,7 +50,6 @@ class FileAccess(
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 {
@@ -60,6 +57,7 @@ class FileAccess(
}
} ?: list
listFromFile.apply {
+ id = list.id
uri = list.uri
}
}.await()
@@ -73,9 +71,10 @@ class FileAccess(
): ItemList {
if (backupUri != null) {
list.uri.let {
- if (it == null
+ if (
+ it == null
|| !it.fileExists
- || !it.isIntoBackupFolder(Uri.parse(backupUri))
+ || !it.isIntoBackupFolder()
|| !it.canWrite
) {
val uri = createListFile(backupUri, list)?.uri
@@ -91,16 +90,8 @@ class FileAccess(
)
}
} 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()
- }
+ // Just don't save list in file. error has been toasted before normally.
+ // Should be handled better
}
}
}
@@ -118,28 +109,20 @@ class FileAccess(
}
- // TODO Toasts should not be here !
- fun deleteListBackupFile(list: ItemList) {
- coroutineIOScope.launch {
+ @kotlin.jvm.Throws
+ fun deleteListBackupFile(
+ list: ItemList,
+ onFileDeleted: () -> Unit,
+ ) {
+ coroutineIOScope.run {
list.uri?.let { uri ->
- try {
+ if (
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()
- }
+ != true
+ ) {
+ throw IOException("Could not delete file")
}
+ onFileDeleted()
}
}
}
@@ -156,8 +139,10 @@ class FileAccess(
.use { iss -> iss?.bufferedReader()?.use { it.readText() } }
// return :
gson.fromJson(content, ItemList::class.java).also {
+ it.uri = uri
onListCreated(it)
}
+
} catch (e: IllegalArgumentException) {
throw e
} catch (e: Exception) {
diff --git a/app/src/main/java/com/lolo/io/onelist/core/data/migration/UpdateHelper.kt b/app/src/main/java/com/lolo/io/onelist/core/data/migration/UpdateHelper.kt
index 56152744..c9c63e11 100644
--- a/app/src/main/java/com/lolo/io/onelist/core/data/migration/UpdateHelper.kt
+++ b/app/src/main/java/com/lolo/io/onelist/core/data/migration/UpdateHelper.kt
@@ -1,11 +1,103 @@
package com.lolo.io.onelist.core.data.migration
+import android.content.Context
+import android.content.SharedPreferences
+import android.health.connect.datatypes.units.Length
+import android.util.Log
+import android.widget.Toast
+import androidx.fragment.app.FragmentActivity
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.lolo.io.onelist.App
+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.dao.ItemListDao
+import com.lolo.io.onelist.core.model.ItemList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import java.lang.ref.WeakReference
class UpdateHelper(
- val persistenceHelper: SharedPreferencesHelper
+ private val preferences: SharedPreferencesHelper,
+ private val repository: OneListRepository
) {
- fun applyUpdatePatches() {
+ private var oldListsIds: Map = linkedMapOf()
+
+ private var activityWR: WeakReference? = null
+
+ private val oldVersionPref: String = "version"
+ private val oldSelectedListPref = "selectedList"
+ private val oldListIdsPref = "listsIds"
+ private val oldDefaultPathPref = "defaultPath"
+ private val oldThemePref: String = "theme"
+
+ fun hasToMigratePrefs(activity: FragmentActivity): Boolean {
+ val activityPreferences = activity.getPreferences(Context.MODE_PRIVATE)
+ return activityPreferences.getString(oldVersionPref, null) != null
}
+
+ fun applyUpdatePatches(activity: FragmentActivity, then: () -> Unit) {
+ val activityPreferences = activity.getPreferences(Context.MODE_PRIVATE)
+ val editor = activityPreferences.edit()
+
+ activityWR = WeakReference(activity)
+
+ preferences.version = activityPreferences.getString(oldVersionPref, "0.0.0") ?: "0.0.0"
+ editor.putString(oldVersionPref, null)
+
+ preferences.selectedListIndex = activityPreferences.getInt(oldSelectedListPref, 0)
+ editor.putString(oldSelectedListPref, null)
+
+ preferences.theme = activityPreferences.getString(oldThemePref, "auto") ?: "auto"
+ editor.putString(oldThemePref, null)
+
+ val oldLists = getAllLists(activityPreferences)
+ CoroutineScope(Dispatchers.IO).launch {
+ oldLists.forEach {
+ repository.createList(it.copy(id = 0))
+ }
+ then()
+ }
+
+ editor.putString(oldListIdsPref, null)
+ editor.putString(oldDefaultPathPref, null)
+
+ preferences.firstLaunch = false
+
+ editor.apply()
+ }
+
+
+ private fun getAllLists(sp: SharedPreferences): List {
+ val gson = Gson()
+ return runBlocking {
+ oldListsIds = getListIdsTable()
+ try {
+ val ret = oldListsIds.map {
+ gson.fromJson(sp.getString(it.key.toString(), ""), ItemList::class.java)
+ }
+ ret
+ } catch (e: Exception) {
+ listOf()
+ }
+ }
+ }
+
+ private fun getListIdsTable(): Map {
+ return activityWR?.get()?.let { act ->
+ val sp = act.getPreferences(Context.MODE_PRIVATE)
+ val gson = Gson()
+ val json =
+ sp.getString(oldListIdsPref, null)?.replace("\\", "")?.removeSurrounding("\"")
+ return if (json != null) {
+ gson.fromJson(json, object : TypeToken