diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
index 48edde44..5dd2abd2 100644
--- a/.idea/deploymentTargetDropDown.xml
+++ b/.idea/deploymentTargetDropDown.xml
@@ -12,6 +12,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 217e5c51..fdf8d994 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 887687fb..653f6280 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,7 +2,7 @@ plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'dagger.hilt.android.plugin'
- id 'kotlin-kapt'
+ id 'com.google.devtools.ksp'
id "com.mikepenz.aboutlibraries.plugin" version "10.5.2"
}
@@ -10,12 +10,12 @@ apply plugin: 'com.mikepenz.aboutlibraries.plugin'
android {
namespace 'com.starry.greenstash'
- compileSdk 33
+ compileSdk 34
defaultConfig {
applicationId "com.starry.greenstash"
minSdk 24
- targetSdk 33
+ targetSdk 34
versionCode 27
versionName "2.7.0"
@@ -24,11 +24,8 @@ android {
useSupportLibrary true
}
- javaCompileOptions {
- annotationProcessorOptions {
- arguments += ["room.schemaLocation":
- "$projectDir/schemas".toString()]
- }
+ ksp {
+ arg('room.schemaLocation', "$projectDir/schemas")
}
}
@@ -53,13 +50,15 @@ android {
}
kotlinOptions {
jvmTarget = '17'
+ freeCompilerArgs = []
}
buildFeatures {
- buildConfig = true
compose true
+ buildConfig = true
+
}
composeOptions {
- kotlinCompilerExtensionVersion '1.4.7'
+ kotlinCompilerExtensionVersion '1.5.2'
}
packagingOptions {
resources {
@@ -76,28 +75,26 @@ aboutLibraries {
dependencies {
- def composeBom = platform('androidx.compose:compose-bom:2023.06.01')
+ def composeBom = platform('androidx.compose:compose-bom:2023.08.00')
implementation composeBom
androidTestImplementation composeBom
// Android core components.
- implementation 'androidx.core:core-ktx:1.10.1'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
- implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
+ implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
+ implementation 'androidx.activity:activity-compose:1.7.2'
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2"
// Jetpack compose.
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.ui:ui-tooling-preview"
- implementation "androidx.compose.material3:material3"
implementation "androidx.compose.material:material"
implementation "androidx.compose.animation:animation"
- implementation "androidx.lifecycle:lifecycle-viewmodel-compose"
implementation "androidx.compose.runtime:runtime-livedata"
- implementation 'androidx.activity:activity-compose'
-
- //accompanist
- implementation "com.google.accompanist:accompanist-navigation-animation:0.29.0-alpha"
+ implementation "androidx.compose.material3:material3"
+ // Accompanist compose.
implementation "com.google.accompanist:accompanist-systemuicontroller:0.28.0"
-
+ implementation "com.google.accompanist:accompanist-navigation-animation:0.33.1-alpha"
// Material theme for main activity.
implementation 'com.google.android.material:material:1.9.0'
// Android 12+ splash API.
@@ -105,17 +102,18 @@ dependencies {
// Room database
implementation "androidx.room:room-ktx:2.5.2"
implementation 'androidx.appcompat:appcompat:1.6.1'
- kapt "androidx.room:room-compiler:2.5.2"
+ implementation 'androidx.core:core-ktx:1.12.0'
+ ksp "androidx.room:room-compiler:2.5.2"
androidTestImplementation "androidx.room:room-testing:2.5.2"
- // Room database backup library.
- implementation 'de.raphaelebner:roomdatabasebackup:1.0.0-beta12'
// Dagger - Hilt.
implementation "com.google.dagger:hilt-android:$hilt_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
- kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
- kapt "androidx.hilt:hilt-compiler:1.0.0"
- // DataStore Preferences
+ ksp "com.google.dagger:hilt-android-compiler:$hilt_version"
+ ksp "androidx.hilt:hilt-compiler:1.0.0"
+ // DataStore Preferences.
implementation("androidx.datastore:datastore-preferences:1.0.0")
+ // Gson JSON parser.
+ implementation 'com.google.code.gson:gson:2.10.1'
// Coil Image loading library.
implementation "io.coil-kt:coil-compose:2.4.0"
// Material 3 calender / Date picker.
@@ -136,7 +134,7 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
- androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
- debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
- debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4"
+ debugImplementation "androidx.compose.ui:ui-tooling"
+ debugImplementation "androidx.compose.ui:ui-test-manifest"
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fd4a4273..964e3aeb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -82,6 +82,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/starry/greenstash/MainActivity.kt b/app/src/main/java/com/starry/greenstash/MainActivity.kt
index 116cb2ef..f9ec6be8 100644
--- a/app/src/main/java/com/starry/greenstash/MainActivity.kt
+++ b/app/src/main/java/com/starry/greenstash/MainActivity.kt
@@ -25,9 +25,7 @@
package com.starry.greenstash
-import android.content.Intent
import android.os.Bundle
-import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
@@ -49,7 +47,6 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModelProvider
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
-import com.starry.greenstash.database.core.AppDatabase
import com.starry.greenstash.ui.navigation.NavGraph
import com.starry.greenstash.ui.screens.settings.viewmodels.SettingsViewModel
import com.starry.greenstash.ui.screens.settings.viewmodels.ThemeMode
@@ -58,16 +55,16 @@ import com.starry.greenstash.utils.PreferenceUtils
import com.starry.greenstash.utils.Utils
import com.starry.greenstash.utils.toToast
import dagger.hilt.android.AndroidEntryPoint
-import de.raphaelebner.roomdatabasebackup.core.RoomBackup
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import java.util.concurrent.Executor
-import javax.inject.Inject
+@ExperimentalCoroutinesApi
@ExperimentalMaterialApi
@ExperimentalFoundationApi
-@AndroidEntryPoint
@ExperimentalComposeUiApi
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
+@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
lateinit var settingsViewModel: SettingsViewModel
@@ -77,11 +74,6 @@ class MainActivity : AppCompatActivity() {
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo
- private lateinit var roomBackup: RoomBackup
-
- @Inject
- lateinit var appDatabase: AppDatabase
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -147,26 +139,6 @@ class MainActivity : AppCompatActivity() {
} else {
setAppContents()
}
-
- // initialize & setup room backup instance
- roomBackup = RoomBackup(this)
- .database(appDatabase)
- .enableLogDebug(true)
- .backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
- .customBackupFileName("GreenStash-${System.currentTimeMillis()}.backup")
- .apply {
- onCompleteListener { success, message, _ ->
- if (success) restartApp(
- Intent(
- this@MainActivity,
- MainActivity::class.java
- )
- ) else Toast.makeText(
- this@MainActivity,
- message, Toast.LENGTH_SHORT,
- ).show()
- }
- }
}
fun setAppContents() {
@@ -194,20 +166,4 @@ class MainActivity : AppCompatActivity() {
}
}
}
-
- fun backupDatabase() {
- try {
- roomBackup.backup()
- } catch (exc: NullPointerException) {
- exc.printStackTrace()
- }
- }
-
- fun restoreDatabase() {
- try {
- roomBackup.restore()
- } catch (exc: NullPointerException) {
- exc.printStackTrace()
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/starry/greenstash/backup/BackupManager.kt b/app/src/main/java/com/starry/greenstash/backup/BackupManager.kt
new file mode 100644
index 00000000..d5d9009b
--- /dev/null
+++ b/app/src/main/java/com/starry/greenstash/backup/BackupManager.kt
@@ -0,0 +1,128 @@
+package com.starry.greenstash.backup
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.util.Log
+import androidx.core.content.FileProvider
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.starry.greenstash.BuildConfig
+import com.starry.greenstash.database.core.GoalWithTransactions
+import com.starry.greenstash.database.goal.GoalDao
+import com.starry.greenstash.utils.updateText
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.time.LocalDateTime
+
+/**
+ * Handles all backup & restore related functionalities.
+ * Note: Access this class using DI instead of manually initialising.
+ */
+class BackupManager(private val context: Context, private val goalDao: GoalDao) {
+
+ /**
+ * Instance of [Gson] with custom type adaptor applied for serializing
+ * and deserializing [Bitmap] fields.
+ */
+ private val gsonInstance = GsonBuilder()
+ .registerTypeAdapter(Bitmap::class.java, BitmapTypeAdapter())
+ .setDateFormat(ISO8601_DATE_FORMAT)
+ .create()
+
+ companion object {
+ /** Backup schema version. */
+ const val BACKUP_SCHEMA_VERSION = 1
+ /** Authority for using file provider API. */
+ private const val FILE_PROVIDER_AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider"
+ /** An ISO-8601 date format for Gson */
+ private const val ISO8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
+ }
+
+ /**
+ * Model for backup json data, containing current schema version
+ * and timestamp when backup was created.
+ */
+ data class BackupJsonModel(
+ val version: Int = BACKUP_SCHEMA_VERSION,
+ val timestamp: Long,
+ val data: List
+ )
+
+ /**
+ * Logger function with pre-applied tag.
+ */
+ private fun log(message: String) {
+ Log.d("BackupManager", message)
+ }
+
+ /**
+ * Creates a database backup by converting goals and transaction data into json
+ * then saving that json file into cache directory and retuning a chooser intent
+ * for the backup file.
+ *
+ * @return a chooser [Intent] for newly created backup file.
+ */
+ suspend fun createDatabaseBackup(): Intent = withContext(Dispatchers.IO) {
+ log("Fetching goals from database and serialising into json...")
+ val goalsWithTransactions = goalDao.getAllGoals()
+ val jsonString = gsonInstance.toJson(
+ BackupJsonModel(
+ timestamp = System.currentTimeMillis(),
+ data = goalsWithTransactions
+ )
+ )
+
+ log("Creating backup json file inside cache directory...")
+ val fileName = "Greenstash-Backup (${LocalDateTime.now()}).json"
+ val file = File(context.cacheDir, fileName)
+ file.updateText(jsonString)
+ val uri = FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file)
+
+ log("Building and returning chooser intent for backup file.")
+ return@withContext Intent(Intent.ACTION_SEND).apply {
+ type = "application/json"
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ putExtra(Intent.EXTRA_STREAM, uri)
+ putExtra(Intent.EXTRA_SUBJECT, "Greenstash Backup")
+ putExtra(Intent.EXTRA_TEXT, "Created at ${LocalDateTime.now()}")
+ }.let { intent -> Intent.createChooser(intent, fileName) }
+
+ }
+
+ /**
+ * Restores a database backup by deserializing the backup json string
+ * and saving goals and transactions back into the database.
+ *
+ * @param jsonString a valid backup json as sting.
+ * @param onFailure callback to be called if [BackupManager] failed parse the json string.
+ * @param onSuccess callback to be called after backup was successfully restored.
+ */
+ suspend fun restoreDatabaseBackup(
+ jsonString: String,
+ onFailure: () -> Unit,
+ onSuccess: () -> Unit
+ ) = withContext(Dispatchers.IO) {
+
+ // Parse json string.
+ log("Parsing backup json file...")
+ val backupData: BackupJsonModel? = try {
+ gsonInstance.fromJson(jsonString, BackupJsonModel::class.java)
+ } catch (exc: Exception) {
+ log("Failed to parse backup json file! Err: ${exc.message}")
+ exc.printStackTrace()
+ null
+ }
+
+ if (backupData?.data == null) {
+ withContext(Dispatchers.Main) { onFailure() }
+ return@withContext
+ }
+
+ // Insert goal & transaction data into database.
+ log("Inserting goals & transactions into the database...")
+ goalDao.insertGoalWithTransaction(backupData.data)
+ withContext(Dispatchers.Main) { onSuccess() }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/starry/greenstash/backup/BitmapTypeAdapter.kt b/app/src/main/java/com/starry/greenstash/backup/BitmapTypeAdapter.kt
new file mode 100644
index 00000000..73fd2606
--- /dev/null
+++ b/app/src/main/java/com/starry/greenstash/backup/BitmapTypeAdapter.kt
@@ -0,0 +1,73 @@
+package com.starry.greenstash.backup
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.util.Base64
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonParseException
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import java.io.ByteArrayOutputStream
+import java.lang.reflect.Type
+
+
+/**
+ * Gson type adaptor used for serializing and deserializing goal image which is
+ * stored as [Bitmap] in the database.
+ * Currently used for backup & restore functionality.
+ */
+class BitmapTypeAdapter : JsonSerializer, JsonDeserializer {
+
+ /**
+ * Gson invokes this call-back method during serialization when it encounters a field of the
+ * specified type.
+ *
+ *
+ * In the implementation of this call-back method, you should consider invoking
+ * [JsonSerializationContext.serialize] method to create JsonElements for any
+ * non-trivial field of the `src` object. However, you should never invoke it on the
+ * `src` object itself since that will cause an infinite loop (Gson will call your
+ * call-back method again).
+ *
+ * @param src the object that needs to be converted to Json.
+ * @param typeOfSrc the actual type (fully genericized version) of the source object.
+ * @return a JsonElement corresponding to the specified object.
+ */
+ override fun serialize(
+ src: Bitmap?, typeOfSrc: Type?, context: JsonSerializationContext?
+ ): JsonElement {
+ val byteArrayOutputStream = ByteArrayOutputStream()
+ src?.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
+ return JsonPrimitive(
+ Base64.encodeToString(
+ byteArrayOutputStream.toByteArray(), Base64.NO_WRAP
+ )
+ )
+ }
+
+ /**
+ * Gson invokes this call-back method during deserialization when it encounters a field of the
+ * specified type.
+ *
+ * In the implementation of this call-back method, you should consider invoking
+ * [JsonDeserializationContext.deserialize] method to create objects
+ * for any non-trivial field of the returned object. However, you should never invoke it on the
+ * the same type passing `json` since that will cause an infinite loop (Gson will call your
+ * call-back method again).
+ *
+ * @param json The Json data being deserialized
+ * @param typeOfT The type of the Object to deserialize to
+ * @return a deserialized object of the specified type typeOfT which is a subclass of `T`
+ * @throws JsonParseException if json is not in the expected format of `typeofT`
+ */
+ override fun deserialize(
+ json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?
+ ): Bitmap? {
+ if (json?.asString == null) return null
+ val byteArray: ByteArray = Base64.decode(json.asString, Base64.NO_WRAP)
+ return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.count())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/starry/greenstash/database/goal/GoalDao.kt b/app/src/main/java/com/starry/greenstash/database/goal/GoalDao.kt
index c58ba6bf..41a0beab 100644
--- a/app/src/main/java/com/starry/greenstash/database/goal/GoalDao.kt
+++ b/app/src/main/java/com/starry/greenstash/database/goal/GoalDao.kt
@@ -37,6 +37,20 @@ interface GoalDao {
@Insert
suspend fun insertGoal(goal: Goal): Long
+ @Transaction
+ suspend fun insertGoalWithTransaction(goalsWithTransactions: List) {
+ goalsWithTransactions.forEach { goalWithTransactions ->
+ // Set placeholder id.
+ goalWithTransactions.goal.goalId = 0L
+ // insert goal and get actual id from database.
+ val goalId = insertGoal(goalWithTransactions.goal)
+ // map transactions with inserted goal, and insert them into database.
+ val transactionsWithGoalId =
+ goalWithTransactions.transactions.map { it.copy(ownerGoalId = goalId) }
+ insertTransactions(transactionsWithGoalId)
+ }
+ }
+
@Update
suspend fun updateGoal(goal: Goal)
@@ -82,4 +96,12 @@ interface GoalDao {
)
fun getAllGoalsByPriority(sortOrder: Int): Flow>
+ /**
+ * For internal use with insertGoalWithTransaction() method only,
+ * Please use Transaction Dao for transaction related operations.
+ */
+ @Insert
+ suspend fun insertTransactions(
+ transactions: List
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/com/starry/greenstash/di/MianModule.kt b/app/src/main/java/com/starry/greenstash/di/MianModule.kt
index 2a94ae27..396e9439 100644
--- a/app/src/main/java/com/starry/greenstash/di/MianModule.kt
+++ b/app/src/main/java/com/starry/greenstash/di/MianModule.kt
@@ -31,7 +31,9 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.ui.ExperimentalComposeUiApi
+import com.starry.greenstash.backup.BackupManager
import com.starry.greenstash.database.core.AppDatabase
+import com.starry.greenstash.database.goal.GoalDao
import com.starry.greenstash.other.WelcomeDataStore
import com.starry.greenstash.reminder.ReminderManager
import com.starry.greenstash.reminder.ReminderNotificationSender
@@ -78,4 +80,9 @@ class MianModule {
@Singleton
fun provideReminderNotificationSender(@ApplicationContext context: Context) =
ReminderNotificationSender(context)
+
+ @Provides
+ @Singleton
+ fun providebackupmanager(@ApplicationContext context: Context, goalDao: GoalDao) =
+ BackupManager(context = context, goalDao = goalDao)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt b/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt
index c6051d27..932bc05e 100644
--- a/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt
+++ b/app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt
@@ -53,7 +53,9 @@ import com.starry.greenstash.ui.screens.settings.composables.AboutScreen
import com.starry.greenstash.ui.screens.settings.composables.OSLScreen
import com.starry.greenstash.ui.screens.settings.composables.SettingsScreen
import com.starry.greenstash.ui.screens.welcome.composables.WelcomeScreen
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+@ExperimentalCoroutinesApi
@ExperimentalMaterialApi
@ExperimentalFoundationApi
@ExperimentalComposeUiApi
diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupScreen.kt
index 5dc665bf..3b1a09db 100644
--- a/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupScreen.kt
+++ b/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupScreen.kt
@@ -25,8 +25,8 @@
package com.starry.greenstash.ui.screens.backups
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -39,7 +39,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Button
@@ -48,14 +47,17 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -63,6 +65,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionResult
@@ -70,22 +73,24 @@ import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
-import com.starry.greenstash.MainActivity
import com.starry.greenstash.R
-import com.starry.greenstash.utils.getActivity
+import kotlinx.coroutines.launch
+import java.io.InputStreamReader
+import java.io.Reader
+import java.nio.charset.StandardCharsets
+
@ExperimentalMaterial3Api
-@ExperimentalAnimationApi
-@ExperimentalComposeUiApi
-@ExperimentalFoundationApi
-@ExperimentalMaterialApi
@Composable
fun BackupScreen(navController: NavController) {
val context = LocalContext.current
- val activity = (context.getActivity() as MainActivity)
+ val viewModel = hiltViewModel()
+
+ val snackBarHostState = remember { SnackbarHostState() }
+ val coroutineScope = rememberCoroutineScope()
- Scaffold(
- modifier = Modifier.fillMaxSize(),
+ Scaffold(modifier = Modifier.fillMaxSize(),
+ snackbarHost = { SnackbarHost(snackBarHostState) },
topBar = {
TopAppBar(modifier = Modifier.fillMaxWidth(), title = {
Text(
@@ -103,20 +108,50 @@ fun BackupScreen(navController: NavController) {
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(4.dp)
)
)
- },
- content = {
- BackupScreenContent(
- paddingValues = it,
- onBackupClicked = { activity.backupDatabase() },
- onRestoreClicked = { activity.restoreDatabase() })
+ }, content = {
+ val backupLauncher =
+ rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
+ uri?.let { fileUri ->
+ context.contentResolver.openInputStream(fileUri)?.let { ips ->
+ // read json content from input stream
+ val bufferSize = 1024
+ val buffer = CharArray(bufferSize)
+ val out = StringBuilder()
+ val reader: Reader = InputStreamReader(ips, StandardCharsets.UTF_8)
+ var numRead: Int
+ while (reader.read(buffer, 0, buffer.size)
+ .also { nRead -> numRead = nRead } > 0
+ ) {
+ out.appendRange(buffer, 0, numRead)
+ }
+
+ viewModel.restoreBackup(jsonString = out.toString(),
+ onSuccess = {
+ coroutineScope.launch {
+ snackBarHostState.showSnackbar(context.getString(R.string.backup_restore_success))
+ }
+ },
+ onFailure = {
+ coroutineScope.launch {
+ snackBarHostState.showSnackbar(context.getString(R.string.unknown_error))
+ }
+ }
+ )
+ }
+ }
+
+ }
+
+ BackupScreenContent(paddingValues = it,
+ onBackupClicked = { viewModel.takeBackup { intent -> context.startActivity(intent) } },
+ onRestoreClicked = { backupLauncher.launch(arrayOf("application/json")) }
+ )
})
}
@Composable
fun BackupScreenContent(
- paddingValues: PaddingValues,
- onBackupClicked: () -> Unit,
- onRestoreClicked: () -> Unit
+ paddingValues: PaddingValues, onBackupClicked: () -> Unit, onRestoreClicked: () -> Unit
) {
Column(
modifier = Modifier
@@ -160,8 +195,7 @@ fun BackupScreenContent(
}
Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
+ modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = onBackupClicked,
diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupViewModel.kt b/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupViewModel.kt
new file mode 100644
index 00000000..39dd899b
--- /dev/null
+++ b/app/src/main/java/com/starry/greenstash/ui/screens/backups/BackupViewModel.kt
@@ -0,0 +1,34 @@
+package com.starry.greenstash.ui.screens.backups
+
+import android.content.Intent
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.starry.greenstash.backup.BackupManager
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+@HiltViewModel
+class BackupViewModel @Inject constructor(
+ private val backupManager: BackupManager
+) : ViewModel() {
+
+ fun takeBackup(onComplete: (Intent) -> Unit) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val backupIntent = backupManager.createDatabaseBackup()
+ withContext(Dispatchers.Main) { onComplete(backupIntent) }
+ }
+ }
+
+ fun restoreBackup(jsonString: String, onSuccess: () -> Unit, onFailure: () -> Unit) {
+ viewModelScope.launch(Dispatchers.IO) {
+ backupManager.restoreDatabaseBackup(
+ jsonString = jsonString,
+ onSuccess = onSuccess,
+ onFailure = onFailure
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt
index 2f9329ab..6e64dfde 100644
--- a/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt
+++ b/app/src/main/java/com/starry/greenstash/ui/screens/input/composables/InputScreen.kt
@@ -140,11 +140,13 @@ import com.starry.greenstash.utils.getActivity
import com.starry.greenstash.utils.toToast
import com.starry.greenstash.utils.validateAmount
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.time.format.DateTimeFormatter
+@ExperimentalCoroutinesApi
@ExperimentalAnimationApi
@ExperimentalMaterialApi
@ExperimentalFoundationApi
@@ -597,6 +599,7 @@ fun GoalPriorityMenu(viewModel: InputViewModel) {
}
}
+@ExperimentalCoroutinesApi
@ExperimentalMaterial3Api
@ExperimentalAnimationApi
@ExperimentalComposeUiApi
@@ -675,6 +678,7 @@ fun GoalReminderMenu(
}
}
+@ExperimentalCoroutinesApi
@ExperimentalMaterialApi
@ExperimentalAnimationApi
@ExperimentalFoundationApi
diff --git a/app/src/main/java/com/starry/greenstash/utils/Extensions.kt b/app/src/main/java/com/starry/greenstash/utils/Extensions.kt
index cd3ea386..3bd30554 100644
--- a/app/src/main/java/com/starry/greenstash/utils/Extensions.kt
+++ b/app/src/main/java/com/starry/greenstash/utils/Extensions.kt
@@ -33,9 +33,12 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import java.io.File
+import java.io.PrintWriter
fun Context.getActivity(): AppCompatActivity? = when (this) {
is AppCompatActivity -> this
@@ -45,8 +48,8 @@ fun Context.getActivity(): AppCompatActivity? = when (this) {
@Composable
fun LazyListState.isScrollingUp(): Boolean {
- var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
- var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
+ var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) }
+ var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
@@ -69,4 +72,16 @@ fun String.toToast(context: Context, length: Int = Toast.LENGTH_SHORT) {
fun String.validateAmount() =
this.isNotEmpty() && this.isNotBlank()
&& !this.matches("[0.]+".toRegex())
- && !this.endsWith(".")
\ No newline at end of file
+ && !this.endsWith(".")
+
+fun File.clearText() {
+ PrintWriter(this).also {
+ it.print("")
+ it.close()
+ }
+}
+
+fun File.updateText(content: String) {
+ clearText()
+ appendText(content)
+}
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 9e1bd73b..05ae81fe 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -1,7 +1,10 @@
GreenStash
+
+
Confirmar
Cancelar
+ Oops! something went wrong.
Inicio
@@ -101,6 +104,7 @@
Nota: La copia de seguridad no incluye la configuración de la app.
Respaldar Datos
Recuperar Datos
+ Backup restored successfully!
Configuración
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index da5d48a2..a76c67e4 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -1,7 +1,10 @@
GreenStash
+
+
确认
取消
+ Oops! something went wrong.
主页
@@ -102,6 +105,7 @@
注意:备份不包括应用设置信息。
备份应用数据
恢复应用数据
+ Backup restored successfully!
设置
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b21e9935..81a75510 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,7 +1,10 @@
GreenStash
+
+
Confirm
Cancel
+ Oops! something went wrong.
Home
@@ -102,6 +105,7 @@
Note: Backups does not include app settings.
Backup App Data
Restore App Data
+ Backup restored successfully!
Settings
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
new file mode 100644
index 00000000..c322aec6
--- /dev/null
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index dabbc5bb..fa2aed75 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,15 +1,15 @@
buildscript {
ext {
- compose_version = '1.4.3'
- hilt_version = '2.47'
+ kotlin_version = '1.9.0'
+ hilt_version = '2.48'
}
repositories {
google()
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.4.2'
- classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21'
+ classpath 'com.android.tools.build:gradle:8.1.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -18,7 +18,8 @@ buildscript {
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id 'com.android.application' version '8.1.0' apply false
- id 'com.android.library' version '8.1.0' apply false
- id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
+ id 'com.android.application' version '8.1.1' apply false
+ id 'com.android.library' version '8.1.1' apply false
+ id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
+ id 'com.google.devtools.ksp' version '1.9.0-1.0.13' apply false
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 3c5031eb..a2e90d87 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,4 +20,6 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
\ No newline at end of file
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=true
+android.nonFinalResIds=false
\ No newline at end of file