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

[Feature] Save back up file on external storage #19 #20

Merged
merged 1 commit into from
Dec 20, 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
3 changes: 3 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

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

7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />

<application
android:name=".MasrofyApp"
android:allowBackup="true"
Expand Down
10 changes: 8 additions & 2 deletions app/src/main/java/com/masrofy/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.masrofy.screens.currency.currencyScreen
import com.masrofy.screens.mainScreen.mainScreenNavigation
import com.masrofy.screens.onboarding.onBoardingDest
import com.masrofy.screens.settings.backups.backupScreens
import com.masrofy.screens.settings.backups.device_backup.deviceBackupDest
import com.masrofy.screens.settings.backups.drive_backup.driveBackupDest
import com.masrofy.screens.settings.settingsDest
import com.masrofy.screens.statisticsScreen.statisticsScreen
Expand All @@ -59,12 +60,16 @@ class MainActivity : ComponentActivity() {
viewModel.checkCategories()
viewModel.checkOnboarding()
val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
ActivityResultContracts.RequestMultiplePermissions(),
){

}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
permissionLauncher.launch(
arrayOf(Manifest.permission.POST_NOTIFICATIONS, Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_MEDIA_LOCATION)
)
}

setContent {
Expand Down Expand Up @@ -215,6 +220,7 @@ fun SetNavigationScreen(mainViewModel: MainViewModel) {
currencyScreen(navController)
backupScreens(navController)
driveBackupDest(navController)
deviceBackupDest(navController)
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/masrofy/Screens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const val ONBOARDING_SCREENS_ARGS = "onboarding_screens"
sealed class Screens(val route: String) {
abstract val args: List<NamedNavArgument>

data object DeviceBackup :Screens("device-backup-screen"){
override val args: List<NamedNavArgument>
get() = emptyList()
}
data object DriveBackupScreen:Screens("drive-backup-screen"){
override val args: List<NamedNavArgument>
get() = emptyList()
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/masrofy/component/ImportFilesDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import com.masrofy.ui.theme.MasrofyTheme
@Composable
fun ImportFilesDialog(
files:List<BackUpDataFileInfo>,
progressDownloadState: ProgressDownloadState,
progressDownloadState: ProgressDownloadState?,
onClickFile:(String)->Unit,
onDismiss:()->Unit
) {
Expand All @@ -53,7 +53,7 @@ fun ImportFilesDialog(
.padding(8.dp), horizontalArrangement = Arrangement.SpaceBetween) {
Icon(painter = painterResource(id = R.drawable.backup_icon), contentDescription = "")
Text(text = "${it.fileName} ", fontSize = 17.sp, maxLines = 1)
if (progressDownloadState.fileId == it.idFile){
if (progressDownloadState?.fileId == it.idFile){
if (progressDownloadState.state == ProgressState.INITIATION_STARTED){
CircularProgressIndicator()
}else if (progressDownloadState.state == ProgressState.STARTED){
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/com/masrofy/component/MScaffoldBar.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.masrofy.component

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import com.masrofy.R
import com.masrofy.screens.settings.backups.drive_backup.DriveBackupEvent

@Composable
fun MScaffoldBar(
title:TranslatableString,
onBack:() -> Unit,
menuItem: List<MenuItem> = listOf(),
content:@Composable (padding:PaddingValues)->Unit
) {
Scaffold(topBar = {
AppBar(title, {
IconButton(onClick = onBack) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "")
}
},menuItem)
}) {
content(it)
}
}
17 changes: 16 additions & 1 deletion app/src/main/java/com/masrofy/core/backup/AbstractBackupData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import android.content.ContentResolver.MimeTypeInfo
import android.webkit.MimeTypeMap
import com.google.gson.Gson
import com.masrofy.core.drive.DRIVE_BACKUP_FILENAME
import com.masrofy.data.database.MasrofyDatabase
import com.masrofy.screens.settings.backups.drive_backup.PeriodSchedule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import java.io.File
import java.io.OutputStream
Expand Down Expand Up @@ -37,13 +39,26 @@ abstract class AbstractBackupData(backupEventListener: BackupEventListener) {


fun getFileName():String = "$DRIVE_BACKUP_FILENAME ${System.currentTimeMillis()}.json"

suspend fun saveDataToDatabase(backupDataModel:BackupDataModel,database: MasrofyDatabase){
val getTransactionDao = database.transactionDao
getTransactionDao.upsertAccount(backupDataModel.account.toAccountEntity())
backupDataModel.transactions.forEach {
getTransactionDao.insertTransaction(it.toTransactionEntity())
}
}
suspend fun getBackupModel(database: MasrofyDatabase): BackupDataModel {
val getAccount = database.transactionDao.getAccounts().first()
val transactions =
database.transactionDao.getTransactions().map { it.toTransactionBackupData() }
return BackupDataModel(getAccount.toAccountBackupData(), transactions)
}
/**
* save data to json file
*/
suspend fun writeDateToFile(backupDataModel: BackupDataModel):File{
return withContext(Dispatchers.IO){
val toGson = Gson().toJson(backupDataModel)
// TODO: improve it
val tempFile = File.createTempFile(getFileName(),null,null).apply {
writeText(toGson.toString())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ data class AccountBackupData(
val type: CategoryAccount,
val totalAmount: Long,
val createdAt: Long,

val currency: Currency
)

Expand Down
83 changes: 75 additions & 8 deletions app/src/main/java/com/masrofy/core/backup/BackupExternalStorage.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,88 @@
package com.masrofy.core.backup

import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import java.io.File
import android.os.Build
import android.os.Environment
import android.provider.DocumentsProvider
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.util.Log
import androidx.core.database.getStringOrNull
import androidx.core.net.toUri
import com.google.gson.Gson
import com.masrofy.core.drive.DRIVE_BACKUP_FILENAME
import com.masrofy.data.database.MasrofyDatabase
import com.masrofy.utils.getFileSize
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext

class BackupExternalStorage(
private val backupEventListener: BackupEventListener,
private val context: Context
) :AbstractBackupData(backupEventListener){
private val context: Context,
private val database: MasrofyDatabase
) : AbstractBackupData(backupEventListener) {

private val contentResolver = context.contentResolver
override suspend fun backup() {
TODO("Not yet implemented")
withContext(Dispatchers.IO + CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("BackupExternalStorage", "backup: unknown error", throwable)
}) {
val backupModel = async { getBackupModel(database) }
val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) MediaStore.Downloads.getContentUri(
MediaStore.VOLUME_EXTERNAL
) else return@withContext

val contentValues = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, getFileName())
put(MediaStore.Downloads.MIME_TYPE, "application/json")
put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
try {
val getFile = writeDateToFile(backupModel.await())
contentResolver.insert(collection, contentValues)?.also {
contentResolver.openOutputStream(it).use { output ->
output?.write(getFile.readBytes())
}
}
backupEventListener.onFinish()
} catch (e: Exception) {
Log.d("BackupExternalStorage", "backup: unknown error", e)
}
}
}

override suspend fun import(file: String) {
TODO("Not yet implemented")
override suspend fun import(fileId: String) {
val getUri = fileId.toUri()
try {
contentResolver.query(getUri,null,null,null,null).use{cor->
if (cor != null){
if (cor.moveToNext()){
val displayName = cor.getStringOrNull(cor.getColumnIndex(OpenableColumns.DISPLAY_NAME))?:return@use
if (displayName.contains(DRIVE_BACKUP_FILENAME)){
contentResolver.openInputStream(getUri).use{
val parseByte = String(it?.readBytes()?:return@use)
val gson = Gson().fromJson(parseByte,BackupDataModel::class.java)
saveDataToDatabase(gson,database)
}
}else{
Log.d(javaClass.simpleName, "import: it not this file")
}
}
}
}
}catch (e:Exception){
Log.e("BackupExternalStorage", "import: error",e )
}
}

override suspend fun getImportFiles(): List<BackUpDataFileInfo> {
TODO("Not yet implemented")
return emptyList()
}
}

}

21 changes: 6 additions & 15 deletions app/src/main/java/com/masrofy/core/backup/DriveBackupDataImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,9 @@ class DriveBackupDataImpl(
override suspend fun backup() {
withContext(Dispatchers.IO) {
eventListener.onBackup()
val getAccount = async { database.transactionDao.getAccounts().first() }
val transactions = async {
database.transactionDao.getTransactions().map { it.toTransactionBackupData() }
}
val backupModel = async { getBackupModel(database) }
eventListener.progressBackup(currentProgressState.copy(ProgressState.INITIATION_STARTED))
val backupModel = BackupDataModel(getAccount.await().toAccountBackupData(), transactions.await())
val file = async { writeDateToFile(backupModel) }
val file = async { writeDateToFile(backupModel.await()) }
fileSize = file.await().length()

backupDrive(drive!!, file.await(), getFileName()).apply {
Expand All @@ -70,6 +66,7 @@ class DriveBackupDataImpl(

override suspend fun import(fileId: String) {
withContext(Dispatchers.IO) {
// TODO: move to work manager it cancel when back screen
eventListener.onImport()
val getFromDrive = getFileById(drive!!, fileId)
val downloader = getFromDrive.mediaHttpDownloader
Expand All @@ -79,9 +76,10 @@ class DriveBackupDataImpl(
currentDownloadProgressState = currentDownloadProgressState.copy(fileId = fileId)
eventListener.progressDownloadFile(currentDownloadProgressState.copy(progressState = ProgressState.INITIATION_STARTED, fileId = fileId))
try {

getFromDrive.executeMediaAndDownloadTo(byteArrayOutputStream)
val parseToBackupModel = async { parseByteToJsonString(byteArrayOutputStream) }
saveDataToDatabase(parseToBackupModel.await())
saveDataToDatabase(parseToBackupModel.await(),database)
}catch (e:Exception){
// improve later
Log.e(javaClass.simpleName, "import: called", e)
Expand All @@ -90,13 +88,7 @@ class DriveBackupDataImpl(
}
}

private suspend fun saveDataToDatabase(backupDataModel:BackupDataModel){
val getTransactionDao = database.transactionDao
getTransactionDao.upsertAccount(backupDataModel.account.toAccountEntity())
backupDataModel.transactions.forEach {
getTransactionDao.insertTransaction(it.toTransactionEntity())
}
}

private fun parseByteToJsonString(byteArrayOutputStream: ByteArrayOutputStream): BackupDataModel {
val string = String(byteArrayOutputStream.toByteArray())
val toJson = Gson().fromJson(string, BackupDataModel::class.java)
Expand All @@ -114,7 +106,6 @@ class DriveBackupDataImpl(
MediaHttpDownloader.DownloadState.MEDIA_COMPLETE -> {
eventListener.progressDownloadFile(currentDownloadProgressState.copy(ProgressState.COMPLETE, progress = downloader.progress ))
eventListener.onFinish()

}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
Expand All @@ -32,18 +33,26 @@ fun NavGraphBuilder.backupScreens(navController: NavController) {
composable(Screens.BackupScreens.route) {
val backupViewModel = hiltViewModel<BackupViewModel>()
val state by backupViewModel.state.collectAsStateWithLifecycle()
BackupScreens(backupStates = state){
navController.navigate(Screens.DriveBackupScreen.route)
val effect by backupViewModel.effect.collectAsStateWithLifecycle()

LaunchedEffect(key1 = effect){
when(effect){
is BackupSettingEffect.OnNavigate -> {
navController.navigate((effect as BackupSettingEffect.OnNavigate).route)
backupViewModel.resetEffect()
}
null -> Unit
}
}
BackupScreens(backupStates = state,backupViewModel::onEvent)
}
}


@Composable
fun BackupScreens(
backupStates: BackupStates,
// for test now
onNavigate:() -> Unit
onEvent:(BackupSettingEvent)->Unit
) {
Scaffold(topBar = {
AppBar(
Expand All @@ -70,7 +79,8 @@ fun BackupScreens(
),
endLabelColor = backupStates.getLabelColor()
) {
onNavigate()
onEvent(BackupSettingEvent.Navigate(Screens.DriveBackupScreen.route))

}
Spacer(modifier = Modifier.height(6.dp))
SettingsComponent(
Expand All @@ -79,17 +89,17 @@ fun BackupScreens(
id = R.drawable.device_icon
),
) {

onEvent(BackupSettingEvent.Navigate(Screens.DeviceBackup.route))
}
Spacer(modifier = Modifier.height(6.dp))
SettingsComponent(
settingHeaderText = stringResource(id = R.string.export_email),
painterResourceID = painterResource(
id = R.drawable.email_icon
),
) {

}
// SettingsComponent(
// settingHeaderText = stringResource(id = R.string.export_email),
// painterResourceID = painterResource(
// id = R.drawable.email_icon
// ),
// ) {
//
// }
}
}
}
Expand Down
Loading