Skip to content

Commit

Permalink
Add backup manager & rework on backup related stuff
Browse files Browse the repository at this point in the history
Signed-off-by: starry-shivam <[email protected]>
  • Loading branch information
starry-shivam committed Oct 19, 2023
1 parent 72ab93b commit 4e06695
Show file tree
Hide file tree
Showing 14 changed files with 355 additions and 23 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.

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>
2 changes: 1 addition & 1 deletion app/src/main/java/com/starry/greenstash/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ import java.util.concurrent.Executor
@ExperimentalCoroutinesApi
@ExperimentalMaterialApi
@ExperimentalFoundationApi
@AndroidEntryPoint
@ExperimentalComposeUiApi
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

lateinit var settingsViewModel: SettingsViewModel
Expand Down
119 changes: 114 additions & 5 deletions app/src/main/java/com/starry/greenstash/backup/BackupManager.kt
Original file line number Diff line number Diff line change
@@ -1,19 +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

class BackupManager(private val goalDao: GoalDao) {
/**
* 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) {

private val gsonInstance = Gson()
suspend fun createDatabaseBackup() = withContext(Dispatchers.IO) {
/**
* 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) }

}

suspend fun restoreDatabaseBackup(): Nothing = withContext(Dispatchers.IO) {
TODO()
/**
* 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 tp parse backup json file! Err: ${exc.message}")
exc.printStackTrace()
null
}

if (backupData == 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() }
}
}
Original file line number Diff line number Diff line change
@@ -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<Bitmap?>, JsonDeserializer<Bitmap?> {

/**
* 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())
}
}
22 changes: 22 additions & 0 deletions app/src/main/java/com/starry/greenstash/database/goal/GoalDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ interface GoalDao {
@Insert
suspend fun insertGoal(goal: Goal): Long

@Transaction
suspend fun insertGoalWithTransaction(goalsWithTransactions: List<GoalWithTransactions>) {
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)

Expand Down Expand Up @@ -82,4 +96,12 @@ interface GoalDao {
)
fun getAllGoalsByPriority(sortOrder: Int): Flow<List<GoalWithTransactions>>

/**
* For internal use with insertGoalWithTransaction() method only,
* Please use Transaction Dao for transaction related operations.
*/
@Insert
suspend fun insertTransactions(
transactions: List<com.starry.greenstash.database.transaction.Transaction>
)
}
7 changes: 7 additions & 0 deletions app/src/main/java/com/starry/greenstash/di/MianModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Loading

0 comments on commit 4e06695

Please sign in to comment.