1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-23 03:12:14 +00:00

feat: add import/export functionality (#152)

This commit is contained in:
Florian Bouillon 2023-02-26 18:17:19 +01:00 committed by GitHub
parent 85ec96e540
commit 468f30bcca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 259 additions and 24 deletions

View File

@ -16,7 +16,6 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:name=".Application" android:name=".Application"
android:allowBackup="true" android:allowBackup="true"

View File

@ -13,6 +13,9 @@ import com.dzeio.openhealth.data.water.Water
import com.dzeio.openhealth.data.water.WaterDao import com.dzeio.openhealth.data.water.WaterDao
import com.dzeio.openhealth.data.weight.Weight import com.dzeio.openhealth.data.weight.Weight
import com.dzeio.openhealth.data.weight.WeightDao import com.dzeio.openhealth.data.weight.WeightDao
import com.dzeio.openhealth.utils.ZipFile
import java.io.File
import java.io.IOException
/** /**
* ROOM SQLite database for the application * ROOM SQLite database for the application
@ -44,7 +47,7 @@ abstract class AppDatabase : RoomDatabase() {
/** /**
* database name duh * database name duh
*/ */
private const val DATABASE_NAME = "open_health" const val DATABASE_NAME = "open_health"
// For Singleton instantiation // For Singleton instantiation
@Volatile @Volatile
@ -66,4 +69,38 @@ abstract class AppDatabase : RoomDatabase() {
.build() .build()
} }
} }
/**
* export the database to a ZIP file and returns the ByteArray of this ZipFile
*
* @param context the application context
* @return the database as a .zip file
*
* based on https://stackoverflow.com/a/73201175/7335674
*/
fun exportDatabase(context: Context): ByteArray? {
if (instance == null) return null
val dbFile = context.getDatabasePath(DATABASE_NAME)
val dbWalFile = File(dbFile.path + "-whal")
val dbShmFile = File(dbFile.path + "-shm")
checkpoint()
try {
val output = ZipFile()
output.addFile(dbFile.name, dbFile)
if (dbWalFile.exists()) output.addFile(dbWalFile.name, dbWalFile)
if (dbShmFile.exists()) output.addFile(dbShmFile.name, dbShmFile)
return output.toByteArray()
} catch (e: IOException) {
e.printStackTrace()
return null
}
}
fun checkpoint() {
val db = this.openHelper.writableDatabase
db.query("PRAGMA wal_checkpoint(FULL);", emptyArray())
db.query("PRAGMA wal_checkpoint(TRUNCATE);", emptyArray())
}
} }

View File

@ -0,0 +1,98 @@
package com.dzeio.openhealth.ui.importexport
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.dzeio.openhealth.R
import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.data.AppDatabase
import com.dzeio.openhealth.databinding.FragmentImportExportBinding
import dagger.hilt.android.AndroidEntryPoint
import java.io.File
import java.util.zip.ZipInputStream
import kotlin.system.exitProcess
@AndroidEntryPoint
class ImportExportFragment : BaseFragment<ImportExportViewModel, FragmentImportExportBinding>(
ImportExportViewModel::class.java
) {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentImportExportBinding =
FragmentImportExportBinding::inflate
/**
* Response to the import button event
*/
private val readResult = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) {
// stop if no document was chosen
if (it == null) {
return@registerForActivityResult
}
// read the zipfile
val stream = requireActivity().contentResolver.openInputStream(it)
val zip = ZipInputStream(stream)
var entry = zip.nextEntry
// copy each file to the database directory
while (entry != null) {
val direction = File(requireContext().getDatabasePath(AppDatabase.DATABASE_NAME).parent, entry.name)
Log.d("Pouet", entry.name)
direction.writeBytes(zip.readBytes())
entry = zip.nextEntry
}
// Restart the app
val i = requireContext().packageManager.getLaunchIntentForPackage(requireContext().packageName)
i!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
requireContext().startActivity(i)
Toast.makeText(requireContext(), R.string.import_complete, Toast.LENGTH_LONG).show()
exitProcess(0)
}
/**
* Response to the export button event
*/
private val writeResult = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/zip")
) {
// stop if no location is given
if (it == null) {
return@registerForActivityResult
}
// export the db to a zip
val bkp = viewModel.appDatabase.exportDatabase(requireContext())
viewModel.appDatabase.checkpoint()
// write file to location
requireActivity().contentResolver.openOutputStream(it)?.apply {
write(bkp)
close()
}
Toast.makeText(requireContext(), R.string.export_complete, Toast.LENGTH_LONG).show()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.imports.setOnClickListener {
// ask user the location of the backup file
readResult.launch(arrayOf("application/zip"))
}
binding.export.setOnClickListener {
// ask user the location to save the backup file
writeResult.launch("export.openhealth")
}
}
}

View File

@ -0,0 +1,11 @@
package com.dzeio.openhealth.ui.importexport
import com.dzeio.openhealth.core.BaseViewModel
import com.dzeio.openhealth.data.AppDatabase
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ImportExportViewModel @Inject internal constructor(
val appDatabase: AppDatabase
) : BaseViewModel()

View File

@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.databinding.FragmentTestsBinding import com.dzeio.openhealth.databinding.FragmentTestsBinding
import com.dzeio.openhealth.utils.Bluetooth
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
@ -19,28 +18,8 @@ class TestsFragment :
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
// Bindings // Test code below
binding.button.setOnClickListener { binding.button.setOnClickListener {
Bluetooth(requireContext()).apply {
// scanDevices(cb)
// val device = DeviceMiSmartScale2(requireContext())
//
// device.status.addObserver {
// Log.d("Device", "New device status $it")
//
// if (it == Device.ConnectionStatus.CONNECTED) {
// device.fetchWeights().addObserver {
// Log.i(
// "FetchStatus",
// "${(it.progress.toFloat() / it.progressMax.toFloat() * 100f).roundToInt()}% ${it.progress}/${it.progressMax}"
// )
// }
// }
// }
//
// device.connect()
}
} }
} }
} }

View File

@ -0,0 +1,42 @@
package com.dzeio.openhealth.utils
import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class ZipFile {
private val stream = ByteArrayOutputStream()
private val output = ZipOutputStream(BufferedOutputStream(stream))
fun addFile(path: String, item: String) = addFile(path, item.toByteArray())
fun addFile(path: String, item: File) {
// Read file
val data = item.inputStream()
val bytes = data.readBytes()
data.close()
return addFile(path, bytes)
}
fun addFile(path: String, item: ByteArray) {
val entry = ZipEntry(path)
try {
output.putNextEntry(entry)
output.write(item)
} catch (e: IOException) {
e.printStackTrace()
}
output.closeEntry()
}
fun toByteArray(): ByteArray {
output.close()
return stream.toByteArray()
}
}

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="32dp"
app:srcCompat="@drawable/ic_logo_full" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/import_export_data"
android:paddingBottom="16dp"
android:textAlignment="center" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/imports"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="@string/importTxt" />
<Button
android:id="@+id/export"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="@string/export" />
</LinearLayout>
</LinearLayout>

View File

@ -14,6 +14,11 @@
android:title="@string/page_settings" android:title="@string/page_settings"
android:icon="@drawable/ic_baseline_settings_24"/> android:icon="@drawable/ic_baseline_settings_24"/>
<item
android:id="@+id/import_export_fragment"
android:title="@string/import_export" />
<item <item
android:id="@+id/nav_about" android:id="@+id/nav_about"
android:title="@string/about" /> android:title="@string/about" />

View File

@ -212,4 +212,9 @@
tools:layout="@layout/dialog_search" tools:layout="@layout/dialog_search"
android:name="com.dzeio.openhealth.ui.weight.ScanScalesDialog" android:name="com.dzeio.openhealth.ui.weight.ScanScalesDialog"
android:label="ScanScalesDialog" /> android:label="ScanScalesDialog" />
<fragment
android:id="@+id/import_export_fragment"
android:name="com.dzeio.openhealth.ui.importexport.ImportExportFragment"
tools:layout="@layout/fragment_import_export"
android:label="@string/import_export" />
</navigation> </navigation>

View File

@ -58,4 +58,10 @@
<string name="bluetooth_scale">Balance bluetooth</string> <string name="bluetooth_scale">Balance bluetooth</string>
<string name="sync">Synchroniser</string> <string name="sync">Synchroniser</string>
<string name="searching_scales">Recherche de balances connecté...</string> <string name="searching_scales">Recherche de balances connecté...</string>
<string name="import_export_data">Importer &amp; Exporter les données de l\'application.</string>
<string name="export">Exporter</string>
<string name="importTxt">Importer</string>
<string name="import_complete">Import réussi, redémarrage de l\'application</string>
<string name="export_complete">Export Réussi!</string>
<string name="import_export">Importer/Exporter</string>
</resources> </resources>

View File

@ -71,4 +71,10 @@
<string name="searching_scales">Searching Scales</string> <string name="searching_scales">Searching Scales</string>
<string name="sync">Sync</string> <string name="sync">Sync</string>
<string name="bluetooth_scale">Bluetooth Scale</string> <string name="bluetooth_scale">Bluetooth Scale</string>
<string name="import_export_data">Import &amp; Export the application datas</string>
<string name="export">Export</string>
<string name="importTxt">Import</string>
<string name="import_complete">Import successful, Restarting the application</string>
<string name="export_complete">Export successful!</string>
<string name="import_export">Import/Export</string>
</resources> </resources>