-
-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add backup manager & rework on backup related stuff
Signed-off-by: starry-shivam <[email protected]>
- Loading branch information
1 parent
72ab93b
commit 4e06695
Showing
14 changed files
with
355 additions
and
23 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 114 additions & 5 deletions
119
app/src/main/java/com/starry/greenstash/backup/BackupManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() } | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
app/src/main/java/com/starry/greenstash/backup/BitmapTypeAdapter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.