Features: - Sleep tracking with timer and manual duration input - Statistics module with 5 tabs (daily summary, feeding, diapers, sleep, growth) - Export/Import backup functionality in settings - Complete German, French and Italian translations
354 lines
15 KiB
Kotlin
354 lines
15 KiB
Kotlin
package it.danieleverducci.lunatracker
|
|
|
|
import android.net.Uri
|
|
import android.os.Bundle
|
|
import android.view.View
|
|
import android.widget.EditText
|
|
import android.widget.RadioButton
|
|
import android.widget.TextView
|
|
import android.widget.Toast
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
|
import com.thegrizzlylabs.sardineandroid.impl.SardineException
|
|
import it.danieleverducci.lunatracker.entities.Logbook
|
|
import it.danieleverducci.lunatracker.entities.LunaEvent
|
|
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
|
|
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
|
|
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
|
|
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
|
|
import okio.IOException
|
|
import org.json.JSONArray
|
|
import org.json.JSONObject
|
|
|
|
open class SettingsActivity : AppCompatActivity() {
|
|
protected lateinit var settingsRepository: LocalSettingsRepository
|
|
protected lateinit var radioDataLocal: RadioButton
|
|
protected lateinit var radioDataWebDAV: RadioButton
|
|
protected lateinit var textViewWebDAVUrl: TextView
|
|
protected lateinit var textViewWebDAVUser: TextView
|
|
protected lateinit var textViewWebDAVPass: TextView
|
|
protected lateinit var progressIndicator: LinearProgressIndicator
|
|
protected lateinit var switchNoBreastfeeding: SwitchMaterial
|
|
protected lateinit var textViewSignature: EditText
|
|
|
|
// Activity Result Launchers for Export/Import
|
|
private val exportLauncher = registerForActivityResult(
|
|
ActivityResultContracts.CreateDocument("application/json")
|
|
) { uri -> uri?.let { exportLogbookToUri(it) } }
|
|
|
|
private val importLauncher = registerForActivityResult(
|
|
ActivityResultContracts.OpenDocument()
|
|
) { uri -> uri?.let { importLogbookFromUri(it) } }
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
setContentView(R.layout.activity_settings)
|
|
radioDataLocal = findViewById(R.id.settings_data_local)
|
|
radioDataWebDAV = findViewById(R.id.settings_data_webdav)
|
|
textViewWebDAVUrl = findViewById(R.id.settings_data_webdav_url)
|
|
textViewWebDAVUser = findViewById(R.id.settings_data_webdav_user)
|
|
textViewWebDAVPass = findViewById(R.id.settings_data_webdav_pass)
|
|
progressIndicator = findViewById(R.id.progress_indicator)
|
|
switchNoBreastfeeding = findViewById(R.id.switch_no_breastfeeding)
|
|
textViewSignature = findViewById(R.id.settings_signature)
|
|
|
|
findViewById<View>(R.id.settings_save).setOnClickListener({
|
|
validateAndSave()
|
|
})
|
|
findViewById<View>(R.id.settings_cancel).setOnClickListener({
|
|
finish()
|
|
})
|
|
findViewById<View>(R.id.settings_export).setOnClickListener({
|
|
startExport()
|
|
})
|
|
findViewById<View>(R.id.settings_import).setOnClickListener({
|
|
startImport()
|
|
})
|
|
|
|
settingsRepository = LocalSettingsRepository(this)
|
|
loadSettings()
|
|
}
|
|
|
|
fun loadSettings() {
|
|
val dataRepo = settingsRepository.loadDataRepository()
|
|
val webDavCredentials = settingsRepository.loadWebdavCredentials()
|
|
val noBreastfeeding = settingsRepository.loadNoBreastfeeding()
|
|
val signature = settingsRepository.loadSignature()
|
|
|
|
when (dataRepo) {
|
|
LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> radioDataLocal.isChecked = true
|
|
LocalSettingsRepository.DATA_REPO.WEBDAV -> radioDataWebDAV.isChecked = true
|
|
}
|
|
|
|
textViewSignature.setText(signature)
|
|
switchNoBreastfeeding.isChecked = noBreastfeeding
|
|
|
|
if (webDavCredentials != null) {
|
|
textViewWebDAVUrl.text = webDavCredentials[0]
|
|
textViewWebDAVUser.text = webDavCredentials[1]
|
|
textViewWebDAVPass.text = webDavCredentials[2]
|
|
}
|
|
}
|
|
|
|
fun validateAndSave() {
|
|
if (radioDataLocal.isChecked) {
|
|
// No validation required, just save
|
|
saveSettings()
|
|
return
|
|
}
|
|
|
|
// Try to connect to WebDAV and check if the save file already exists
|
|
val webDAVLogbookRepo = WebDAVLogbookRepository(
|
|
textViewWebDAVUrl.text.toString(),
|
|
textViewWebDAVUser.text.toString(),
|
|
textViewWebDAVPass.text.toString()
|
|
)
|
|
progressIndicator.visibility = View.VISIBLE
|
|
|
|
webDAVLogbookRepo.listLogbooks(this, object: LogbookListObtainedListener{
|
|
|
|
override fun onLogbookListObtained(logbooksNames: ArrayList<String>) {
|
|
if (logbooksNames.isEmpty()) {
|
|
// TODO: Ask the user if he wants to upload the local ones or to create a new one
|
|
copyLocalLogbooksToWebdav(webDAVLogbookRepo, object: OnCopyLocalLogbooksToWebdavFinishedListener {
|
|
|
|
override fun onCopyLocalLogbooksToWebdavFinished(errors: String?) {
|
|
runOnUiThread({
|
|
progressIndicator.visibility = View.INVISIBLE
|
|
if (errors == null) {
|
|
saveSettings()
|
|
Toast.makeText(this@SettingsActivity, R.string.settings_webdav_creation_ok, Toast.LENGTH_SHORT).show()
|
|
} else {
|
|
Toast.makeText(this@SettingsActivity, errors, Toast.LENGTH_SHORT).show()
|
|
}
|
|
})
|
|
}
|
|
|
|
})
|
|
} else {
|
|
runOnUiThread({
|
|
progressIndicator.visibility = View.INVISIBLE
|
|
saveSettings()
|
|
Toast.makeText(this@SettingsActivity, R.string.settings_webdav_creation_ok, Toast.LENGTH_SHORT).show()
|
|
})
|
|
}
|
|
}
|
|
|
|
override fun onIOError(error: IOException) {
|
|
runOnUiThread({
|
|
progressIndicator.visibility = View.INVISIBLE
|
|
Toast.makeText(this@SettingsActivity, getString(R.string.settings_network_error) + error.toString(), Toast.LENGTH_SHORT).show()
|
|
})
|
|
}
|
|
|
|
override fun onWebDAVError(error: SardineException) {
|
|
runOnUiThread({
|
|
progressIndicator.visibility = View.INVISIBLE
|
|
if(error.toString().contains("401")) {
|
|
Toast.makeText(this@SettingsActivity, getString(R.string.settings_webdav_error_denied), Toast.LENGTH_SHORT).show()
|
|
} else {
|
|
Toast.makeText(this@SettingsActivity, getString(R.string.settings_webdav_error_generic) + error.toString(), Toast.LENGTH_SHORT).show()
|
|
}
|
|
})
|
|
}
|
|
|
|
override fun onError(error: Exception) {
|
|
runOnUiThread({
|
|
progressIndicator.visibility = View.INVISIBLE
|
|
Toast.makeText(this@SettingsActivity, getString(R.string.settings_generic_error) + error.toString(), Toast.LENGTH_SHORT).show()
|
|
})
|
|
}
|
|
})
|
|
|
|
/*webDAVLogbookRepo.createLogbook(this, LogbookRepository.DEFAULT_LOGBOOK_NAME, object: WebDAVLogbookRepository.LogbookCreatedListener{
|
|
|
|
override fun onJSONError(error: JSONException) {
|
|
runOnUiThread({
|
|
progressIndicator.visibility = View.INVISIBLE
|
|
Toast.makeText(this@SettingsActivity, getString(R.string.settings_json_error) + error.toString(), Toast.LENGTH_SHORT).show()
|
|
})
|
|
}
|
|
|
|
|
|
})*/
|
|
}
|
|
|
|
fun saveSettings() {
|
|
settingsRepository.saveDataRepository(
|
|
if (radioDataWebDAV.isChecked) LocalSettingsRepository.DATA_REPO.WEBDAV
|
|
else LocalSettingsRepository.DATA_REPO.LOCAL_FILE
|
|
)
|
|
settingsRepository.saveNoBreastfeeding(switchNoBreastfeeding.isChecked)
|
|
settingsRepository.saveSignature(textViewSignature.text.toString())
|
|
settingsRepository.saveWebdavCredentials(
|
|
textViewWebDAVUrl.text.toString(),
|
|
textViewWebDAVUser.text.toString(),
|
|
textViewWebDAVPass.text.toString()
|
|
)
|
|
finish()
|
|
}
|
|
|
|
/**
|
|
* Copies the local logbooks to webdav.
|
|
* @return success
|
|
*/
|
|
private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) {
|
|
Thread(Runnable {
|
|
val errors = StringBuilder()
|
|
val fileLogbookRepo = FileLogbookRepository()
|
|
val logbooks = fileLogbookRepo.getAllLogbooks(this)
|
|
for (logbook in logbooks) {
|
|
// Copy only if does not already exist
|
|
val error = webDAVLogbookRepository.uploadLogbookIfNotExists(this, logbook.name)
|
|
if (error != null) {
|
|
if (errors.isNotEmpty())
|
|
errors.append("\n")
|
|
errors.append(String.format(getString(R.string.settings_webdav_upload_error), logbook.name, error))
|
|
}
|
|
}
|
|
listener.onCopyLocalLogbooksToWebdavFinished(
|
|
if (errors.isEmpty()) null else errors.toString()
|
|
)
|
|
}).start()
|
|
}
|
|
|
|
private interface OnCopyLocalLogbooksToWebdavFinishedListener {
|
|
fun onCopyLocalLogbooksToWebdavFinished(errors: String?)
|
|
}
|
|
|
|
// Export/Import functionality
|
|
private fun startExport() {
|
|
val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.US)
|
|
.format(java.util.Date())
|
|
exportLauncher.launch("lunatracker_backup_$timestamp.json")
|
|
}
|
|
|
|
private fun startImport() {
|
|
importLauncher.launch(arrayOf("application/json"))
|
|
}
|
|
|
|
private fun exportLogbookToUri(uri: Uri) {
|
|
progressIndicator.visibility = View.VISIBLE
|
|
Thread {
|
|
try {
|
|
val fileLogbookRepo = FileLogbookRepository()
|
|
val logbooks = fileLogbookRepo.getAllLogbooks(this)
|
|
|
|
val json = JSONObject().apply {
|
|
put("version", 1)
|
|
put("app", "LunaTracker")
|
|
put("exported_at", System.currentTimeMillis())
|
|
put("logbooks", JSONArray().apply {
|
|
logbooks.forEach { logbook ->
|
|
put(JSONObject().apply {
|
|
put("name", logbook.name)
|
|
put("events", JSONArray().apply {
|
|
logbook.logs.forEach { event ->
|
|
put(event.toJson())
|
|
}
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
contentResolver.openOutputStream(uri)?.use { outputStream ->
|
|
outputStream.write(json.toString(2).toByteArray(Charsets.UTF_8))
|
|
}
|
|
|
|
val eventCount = logbooks.sumOf { it.logs.size }
|
|
runOnUiThread {
|
|
progressIndicator.visibility = View.INVISIBLE
|
|
Toast.makeText(
|
|
this,
|
|
getString(R.string.export_success, eventCount),
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
}
|
|
} catch (e: Exception) {
|
|
runOnUiThread {
|
|
progressIndicator.visibility = View.INVISIBLE
|
|
Toast.makeText(
|
|
this,
|
|
getString(R.string.export_error) + e.message,
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
}
|
|
}
|
|
}.start()
|
|
}
|
|
|
|
private fun importLogbookFromUri(uri: Uri) {
|
|
progressIndicator.visibility = View.VISIBLE
|
|
Thread {
|
|
try {
|
|
val jsonString = contentResolver.openInputStream(uri)?.bufferedReader()?.readText()
|
|
?: throw Exception("Could not read file")
|
|
|
|
val json = JSONObject(jsonString)
|
|
val version = json.optInt("version", 1)
|
|
|
|
val fileLogbookRepo = FileLogbookRepository()
|
|
var totalEvents = 0
|
|
|
|
if (json.has("logbooks")) {
|
|
// New format with multiple logbooks
|
|
val logbooksArray = json.getJSONArray("logbooks")
|
|
for (i in 0 until logbooksArray.length()) {
|
|
val logbookJson = logbooksArray.getJSONObject(i)
|
|
val name = logbookJson.optString("name", "")
|
|
val eventsArray = logbookJson.getJSONArray("events")
|
|
|
|
val logbook = Logbook(name)
|
|
for (j in 0 until eventsArray.length()) {
|
|
try {
|
|
logbook.logs.add(LunaEvent(eventsArray.getJSONObject(j)))
|
|
totalEvents++
|
|
} catch (e: IllegalArgumentException) {
|
|
// Skip invalid events
|
|
}
|
|
}
|
|
fileLogbookRepo.saveLogbook(this, logbook)
|
|
}
|
|
} else if (json.has("events")) {
|
|
// Old format with single logbook
|
|
val name = json.optString("logbook_name", "")
|
|
val eventsArray = json.getJSONArray("events")
|
|
|
|
val logbook = Logbook(name)
|
|
for (i in 0 until eventsArray.length()) {
|
|
try {
|
|
logbook.logs.add(LunaEvent(eventsArray.getJSONObject(i)))
|
|
totalEvents++
|
|
} catch (e: IllegalArgumentException) {
|
|
// Skip invalid events
|
|
}
|
|
}
|
|
fileLogbookRepo.saveLogbook(this, logbook)
|
|
}
|
|
|
|
runOnUiThread {
|
|
progressIndicator.visibility = View.INVISIBLE
|
|
Toast.makeText(
|
|
this,
|
|
getString(R.string.import_success, totalEvents),
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
}
|
|
} catch (e: Exception) {
|
|
runOnUiThread {
|
|
progressIndicator.visibility = View.INVISIBLE
|
|
Toast.makeText(
|
|
this,
|
|
getString(R.string.import_error) + ": " + e.message,
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
}
|
|
}
|
|
}.start()
|
|
}
|
|
|
|
} |