Skip to content

Commit

Permalink
Rewrite Backup & Restore functionality (#55)
Browse files Browse the repository at this point in the history
* Add backup package
* Bump target SDK, upgrade AGP version, switch from KAPT to KSP & more
* Add backup manager & rework on backup related stuff
* Minor fix
---------

Signed-off-by: starry-shivam <[email protected]>
  • Loading branch information
starry-shivam authored Oct 19, 2023
1 parent 81ce673 commit 88c1464
Show file tree
Hide file tree
Showing 20 changed files with 418 additions and 114 deletions.
2 changes: 1 addition & 1 deletion .idea/deploymentTargetDropDown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 28 additions & 30 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ 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"
}

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"

Expand All @@ -24,11 +24,8 @@ android {
useSupportLibrary true
}

javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
ksp {
arg('room.schemaLocation', "$projectDir/schemas")
}
}

Expand All @@ -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 {
Expand All @@ -76,46 +75,45 @@ 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.
implementation 'androidx.core:core-splashscreen:1.0.1'
// 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.
Expand All @@ -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"
}
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@
</intent-filter>
</receiver>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>

</application>

</manifest>
50 changes: 3 additions & 47 deletions app/src/main/java/com/starry/greenstash/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
}
}
}
128 changes: 128 additions & 0 deletions app/src/main/java/com/starry/greenstash/backup/BackupManager.kt
Original file line number Diff line number Diff line change
@@ -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<GoalWithTransactions>
)

/**
* 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() }
}
}
Loading

0 comments on commit 88c1464

Please sign in to comment.