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:
parent
85ec96e540
commit
468f30bcca
@ -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"
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
42
app/src/main/java/com/dzeio/openhealth/utils/ZipFile.kt
Normal file
42
app/src/main/java/com/dzeio/openhealth/utils/ZipFile.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
47
app/src/main/res/layout/fragment_import_export.xml
Normal file
47
app/src/main/res/layout/fragment_import_export.xml
Normal 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>
|
@ -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" />
|
||||||
|
@ -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>
|
||||||
|
@ -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 & 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>
|
||||||
|
@ -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 & 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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user