Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite Backup & Restore functionality #55

Merged
merged 6 commits into from
Oct 19, 2023
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
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