Compare commits

...

16 Commits

Author SHA1 Message Date
efdb8f584a Avoid reloading logbooks list when the app is resumed, for a faster and flicker-less screen update 2025-01-20 07:57:46 +01:00
1511764497 Restored detail button string 2025-01-19 09:53:08 +01:00
744bfef62b Better error management, check Webdav instance unavailable, fixed crash on refresh when webdav instance was unavailable 2025-01-19 09:47:14 +01:00
34ca6c1cd6 Missing translation 2025-01-19 09:35:30 +01:00
b4f47ea6bb Implemented event deletion 2025-01-19 09:16:49 +01:00
99743288c5 Implemented transition from local to webdav logbooks and error management 2025-01-18 11:01:43 +01:00
03ec28f8ef Onboarding, fix in logbook list from file repo 2025-01-18 09:50:50 +01:00
ac9f74dbd7 Fix dropdown not updated on logbook creation, refinements 2025-01-18 09:09:44 +01:00
36f52234a3 Fixed bug loading always default logbook, indipendently from which one is selected 2025-01-18 08:56:12 +01:00
e23ab77274 Dropdown style, restored add logbook 2025-01-18 08:48:43 +01:00
e3aceaf133 Reimplemented logbook selector as spinner 2025-01-18 08:14:00 +01:00
0494a11538 Optimized space 2025-01-18 07:44:11 +01:00
ecbde64ca1 Working ugly selector 2025-01-18 07:36:31 +01:00
32fbeac079 WIP implementing multiple children (working, with some issues) 2025-01-17 08:55:37 +01:00
0b0fd8f5af Refactoring FileLogbookRepository 2025-01-15 08:08:26 +01:00
fe5da015cb Support for multiple logbook files 2025-01-13 08:16:06 +01:00
16 changed files with 584 additions and 121 deletions

1
.gitignore vendored
View File

@ -106,3 +106,4 @@ app/release/output-metadata.json
# Other
app/src/main/java/it/danieleverducci/lunatracker/TemporaryHardcodedCredentials.kt
.kotlin/sessions/*

View File

@ -1,5 +1,6 @@
package it.danieleverducci.lunatracker
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Handler
@ -7,13 +8,18 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.NumberPicker
import android.widget.PopupWindow
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.LinearProgressIndicator
@ -24,6 +30,7 @@ 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.LogbookLoadedListener
import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.LogbookSavedListener
@ -42,7 +49,7 @@ class MainActivity : AppCompatActivity() {
val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false
}
lateinit var logbook: Logbook
var logbook: Logbook? = null
lateinit var adapter: LunaEventRecyclerAdapter
lateinit var progressIndicator: LinearProgressIndicator
lateinit var buttonsContainer: ViewGroup
@ -50,7 +57,8 @@ class MainActivity : AppCompatActivity() {
lateinit var handler: Handler
var savingEvent = false
val updateListRunnable: Runnable = Runnable {
loadLogbook()
if (logbook != null)
loadLogbook(logbook!!.name)
handler.postDelayed(updateListRunnable, 1000*60)
}
var logbookRepo: LogbookRepository? = null
@ -77,6 +85,7 @@ class MainActivity : AppCompatActivity() {
recyclerView.adapter = adapter
// Set listeners
findViewById<View>(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog(true) }
findViewById<View>(R.id.button_bottle).setOnClickListener { askBabyBottleContent() }
findViewById<View>(R.id.button_scale).setOnClickListener { askWeightValue() }
findViewById<View>(R.id.button_nipple_left).setOnClickListener { logEvent(
@ -115,10 +124,11 @@ class MainActivity : AppCompatActivity() {
showSettings()
})
findViewById<View>(R.id.button_no_connection_retry).setOnClickListener({
loadLogbook()
// This may happen at start, when logbook is still null: better ask the logbook list
loadLogbookList()
})
findViewById<View>(R.id.button_sync).setOnClickListener({
loadLogbook()
loadLogbookList()
})
}
@ -129,8 +139,11 @@ class MainActivity : AppCompatActivity() {
fun showLogbook() {
// Show logbook
if (logbook == null)
Log.w(TAG, "showLogbook(): logbook is null!")
adapter.items.clear()
adapter.items.addAll(logbook.logs)
adapter.items.addAll(logbook?.logs ?: listOf())
adapter.notifyDataSetChanged()
}
@ -155,8 +168,13 @@ class MainActivity : AppCompatActivity() {
// Update list dates
adapter.notifyDataSetChanged()
// Reload data
loadLogbook()
if (logbook != null) {
// Already running: reload data for currently selected logbook
loadLogbook(logbook!!.name)
} else {
// First start: load logbook list
loadLogbookList()
}
}
override fun onStop() {
@ -276,7 +294,7 @@ class MainActivity : AppCompatActivity() {
}
)
d.setPositiveButton(R.string.trim_logbook_dialog_button_ok) { dialogInterface, i ->
logbook.trim()
logbook?.trim()
saveLogbook()
}
d.setNegativeButton(R.string.trim_logbook_dialog_button_cancel) { dialogInterface, i ->
@ -300,11 +318,154 @@ class MainActivity : AppCompatActivity() {
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_notes).setText(event.notes)
d.setView(dialogView)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> dialogInterface.dismiss() }
d.setNeutralButton(R.string.dialog_event_detail_delete_button) { dialogInterface, i -> deleteEvent(event) }
val alertDialog = d.create()
alertDialog.show()
alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL).setTextColor(ContextCompat.getColor(this, R.color.danger))
}
fun showAddLogbookDialog(requestedByUser: Boolean) {
val d = AlertDialog.Builder(this)
d.setTitle(R.string.dialog_add_logbook_title)
val dialogView = layoutInflater.inflate(R.layout.dialog_add_logbook, null)
dialogView.findViewById<TextView>(R.id.dialog_add_logbook_message).text = getString(
if (requestedByUser) R.string.dialog_add_logbook_message else R.string.dialog_add_logbook_message_intro
)
val logbookNameEditText = dialogView.findViewById<EditText>(R.id.dialog_add_logbook_logbookname)
d.setView(dialogView)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> addLogbook(logbookNameEditText.text.toString()) }
if (requestedByUser) {
d.setCancelable(true)
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
} else {
d.setCancelable(false)
}
val alertDialog = d.create()
alertDialog.show()
}
fun loadLogbook() {
fun loadLogbookList() {
setLoading(true)
logbookRepo?.listLogbooks(this, object: LogbookListObtainedListener {
override fun onLogbookListObtained(logbooksNames: ArrayList<String>) {
runOnUiThread({
if (logbooksNames.isEmpty()) {
// First run, no logbook: create one
showAddLogbookDialog(false)
return@runOnUiThread
}
// Show logbooks dropdown
val spinner = findViewById<Spinner>(R.id.logbooks_spinner)
val sAdapter = ArrayAdapter<String>(this@MainActivity, android.R.layout.simple_spinner_item)
sAdapter.setDropDownViewResource(R.layout.row_logbook_spinner)
for (ln in logbooksNames) {
sAdapter.add(
if (ln.isEmpty()) getString(R.string.default_logbook_name) else ln
)
}
spinner.adapter = sAdapter
spinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
// Changed logbook: empty list
adapter.items.clear()
adapter.notifyDataSetChanged()
// Load logbook
loadLogbook(logbooksNames.get(position))
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
})
}
override fun onIOError(error: IOException) {
Log.e(TAG, "Unable to load logbooks list (IOError): $error")
runOnUiThread({
setLoading(false)
onRepoError(getString(R.string.settings_network_error) + error.toString())
})
}
override fun onWebDAVError(error: SardineException) {
Log.e(TAG, "Unable to load logbooks list (SardineException): $error")
runOnUiThread({
setLoading(false)
onRepoError(
if(error.toString().contains("401")) {
getString(R.string.settings_webdav_error_denied)
} else if(error.toString().contains("503")) {
getString(R.string.settings_webdav_error_server_offline)
} else {
getString(R.string.settings_webdav_error_generic) + error.toString()
}
)
})
}
override fun onError(error: Exception) {
Log.e(TAG, "Unable to load logbooks list: $error")
runOnUiThread({
setLoading(false)
onRepoError(getString(R.string.settings_generic_error) + error.toString())
})
}
})
}
fun addLogbook(logbookName: String) {
val newLogbook = Logbook(logbookName)
setLoading(true)
logbookRepo?.saveLogbook(this, newLogbook, object: LogbookSavedListener{
override fun onLogbookSaved() {
Log.d(TAG, "Logbook $logbookName created")
runOnUiThread({
setLoading(false)
loadLogbookList()
Toast.makeText(this@MainActivity, getString(R.string.logbook_created) + logbookName, Toast.LENGTH_SHORT).show()
})
}
override fun onIOError(error: IOException) {
runOnUiThread({
onRepoError(getString(R.string.settings_network_error) + error.toString())
})
}
override fun onWebDAVError(error: SardineException) {
runOnUiThread({
onRepoError(
if(error.toString().contains("401")) {
getString(R.string.settings_webdav_error_denied)
} else if(error.toString().contains("503")) {
getString(R.string.settings_webdav_error_server_offline)
} else {
getString(R.string.settings_webdav_error_generic) + error.toString()
}
)
})
}
override fun onJSONError(error: JSONException) {
runOnUiThread({
onRepoError(getString(R.string.settings_json_error) + error.toString())
})
}
override fun onError(error: Exception) {
runOnUiThread({
onRepoError(getString(R.string.settings_generic_error) + error.toString())
})
}
})
}
fun loadLogbook(name: String) {
if (savingEvent)
return
@ -314,7 +475,7 @@ class MainActivity : AppCompatActivity() {
// Load data
setLoading(true)
logbookRepo?.loadLogbook(this, object: LogbookLoadedListener{
logbookRepo?.loadLogbook(this, name, object: LogbookLoadedListener{
override fun onLogbookLoaded(lb: Logbook) {
runOnUiThread({
setLoading(false)
@ -323,7 +484,7 @@ class MainActivity : AppCompatActivity() {
showLogbook()
if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) {
for (e in logbook.logs) {
for (e in logbook?.logs ?: listOf()) {
val em = e.getTypeEmoji(this@MainActivity)
if (em == getString(R.string.event_unknown_type)) {
Log.e(TAG, "UNKNOWN: ${e.type}")
@ -346,6 +507,8 @@ class MainActivity : AppCompatActivity() {
onRepoError(
if(error.toString().contains("401")) {
getString(R.string.settings_webdav_error_denied)
} else if(error.toString().contains("503")) {
getString(R.string.settings_webdav_error_server_offline)
} else {
getString(R.string.settings_webdav_error_generic) + error.toString()
}
@ -385,21 +548,37 @@ class MainActivity : AppCompatActivity() {
recyclerView.smoothScrollToPosition(0)
setLoading(true)
logbook.logs.add(0, event)
logbook?.logs?.add(0, event)
saveLogbook(event)
// Check logbook size to avoid OOM errors
if (logbook.isTooBig()) {
if (logbook?.isTooBig() == true) {
askToTrimLogbook()
}
}
fun deleteEvent(event: LunaEvent) {
// Update view
savingEvent(true)
adapter.items.remove(event)
adapter.notifyDataSetChanged()
// Update data
setLoading(true)
logbook?.logs?.remove(event)
saveLogbook()
}
/**
* Saves the logbook. If saving while adding an event, please specify the event so in case
* of error can be removed from the list.
*/
fun saveLogbook(lastEventAdded: LunaEvent? = null) {
logbookRepo?.saveLogbook(this, logbook, object: LogbookSavedListener{
if (logbook == null) {
Log.e(TAG, "Trying to save logbook, but logbook is null!")
return
}
logbookRepo?.saveLogbook(this, logbook!!, object: LogbookSavedListener{
override fun onLogbookSaved() {
Log.d(TAG, "Logbook saved")
runOnUiThread({
@ -432,6 +611,8 @@ class MainActivity : AppCompatActivity() {
onRepoError(
if(error.toString().contains("401")) {
getString(R.string.settings_webdav_error_denied)
} else if(error.toString().contains("503")) {
getString(R.string.settings_webdav_error_server_offline)
} else {
getString(R.string.settings_webdav_error_generic) + error.toString()
}

View File

@ -8,7 +8,10 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException
import org.json.JSONException
@ -72,13 +75,34 @@ open class SettingsActivity : AppCompatActivity() {
textViewWebDAVPass.text.toString()
)
progressIndicator.visibility = View.VISIBLE
webDAVLogbookRepo.createLogbook(this, object: WebDAVLogbookRepository.LogbookCreatedListener{
override fun onLogbookCreated() {
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
saveSettings()
Toast.makeText(this@SettingsActivity, R.string.settings_webdav_creation_ok, Toast.LENGTH_SHORT).show()
})
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) {
@ -99,6 +123,16 @@ open class SettingsActivity : AppCompatActivity() {
})
}
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
@ -106,14 +140,8 @@ open class SettingsActivity : AppCompatActivity() {
})
}
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()
})
}
})
})*/
}
fun saveSettings() {
@ -129,4 +157,32 @@ open class SettingsActivity : AppCompatActivity() {
finish()
}
/**
* Copies the local logbooks to webdav.
* @return success
*/
private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) {
Thread(Runnable {
var 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?)
}
}

View File

@ -1,6 +1,6 @@
package it.danieleverducci.lunatracker.entities
class Logbook {
class Logbook(val name: String) {
companion object {
val MAX_SAFE_LOGBOOK_SIZE = 30000
}

View File

@ -9,26 +9,30 @@ import org.json.JSONException
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FilenameFilter
class FileLogbookRepository: LogbookRepository {
companion object {
val TAG = "FileLogbookRepository"
val FILE_NAME_START = "data"
val FILE_NAME_END = ".json"
}
override fun loadLogbook(context: Context, listener: LogbookLoadedListener) {
override fun loadLogbook(context: Context, name: String, listener: LogbookLoadedListener) {
try {
listener.onLogbookLoaded(loadLogbook(context))
listener.onLogbookLoaded(loadLogbook(context, name))
} catch (e: FileNotFoundException) {
Log.d(TAG, "No logbook file found, create one")
val newLogbook = Logbook()
val newLogbook = Logbook(name)
saveLogbook(context, newLogbook)
listener.onLogbookLoaded(newLogbook)
}
}
fun loadLogbook(context: Context): Logbook {
val logbook = Logbook()
val file = File(context.getFilesDir(), "data.json")
fun loadLogbook(context: Context, name: String): Logbook {
val logbook = Logbook(name)
val fileName = getFileName(name)
val file = File(context.getFilesDir(), fileName)
val json = FileInputStream(file).bufferedReader().use { it.readText() }
val ja = JSONArray(json)
for (i in 0 until ja.length()) {
@ -53,11 +57,55 @@ class FileLogbookRepository: LogbookRepository {
}
fun saveLogbook(context: Context, logbook: Logbook) {
val file = File(context.getFilesDir(), "data.json")
val fileName = getFileName(logbook.name)
val file = File(context.getFilesDir(), fileName)
val ja = JSONArray()
for (l in logbook.logs) {
ja.put(l.toJson())
}
file.writeText(ja.toString())
}
override fun listLogbooks(
context: Context,
listener: LogbookListObtainedListener
) {
listener.onLogbookListObtained(listLogbooks(context))
}
fun getAllLogbooks(context: Context): List<Logbook> {
val logbooks = arrayListOf<Logbook>()
for (logbookName in listLogbooks(context)) {
logbooks.add(loadLogbook(context, logbookName))
}
return logbooks
}
private fun listLogbooks(context: Context): ArrayList<String> {
val logbooksFileNames = context.getFilesDir().list(object: FilenameFilter {
override fun accept(dir: File?, name: String?): Boolean {
if (name == null)
return false
if (name.startsWith(FILE_NAME_START) && name.endsWith(FILE_NAME_END))
return true
return false
}
})
if (logbooksFileNames == null || logbooksFileNames.isEmpty()) {
return arrayListOf()
}
val logbooksNames = arrayListOf<String>()
logbooksFileNames.forEach { it ->
logbooksNames.add(
it.replace("${FILE_NAME_START}_", "").replace(FILE_NAME_START, "").replace(FILE_NAME_END, "")
)
}
return logbooksNames
}
private fun getFileName(name: String): String {
return "$FILE_NAME_START${if (name.isNotEmpty()) "_" else ""}${name}$FILE_NAME_END"
}
}

View File

@ -7,8 +7,12 @@ import okio.IOException
import org.json.JSONException
interface LogbookRepository {
fun loadLogbook(context: Context, listener: LogbookLoadedListener)
fun saveLogbook(context: Context, logbook: Logbook, listener: LogbookSavedListener)
companion object {
val DEFAULT_LOGBOOK_NAME = "" // For compatibility with older app versions
}
fun loadLogbook(context: Context, name: String = "", listener: LogbookLoadedListener)
fun saveLogbook(context: Context,logbook: Logbook, listener: LogbookSavedListener)
fun listLogbooks(context: Context, listener: LogbookListObtainedListener)
}
interface LogbookLoadedListener {
@ -25,4 +29,11 @@ interface LogbookSavedListener {
fun onWebDAVError(error: SardineException)
fun onJSONError(error: JSONException)
fun onError(error: Exception)
}
interface LogbookListObtainedListener {
fun onLogbookListObtained(logbooksNames: ArrayList<String>)
fun onIOError(error: IOException)
fun onWebDAVError(error: SardineException)
fun onError(error: Exception)
}

View File

@ -2,6 +2,7 @@ package it.danieleverducci.lunatracker.repository
import android.content.Context
import android.util.Log
import com.thegrizzlylabs.sardineandroid.DavResource
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.entities.Logbook
@ -14,11 +15,13 @@ import java.io.FileNotFoundException
import java.io.IOException
import java.net.SocketTimeoutException
import kotlin.io.bufferedReader
import kotlin.text.replace
class WebDAVLogbookRepository(val webDavURL: String, val username: String, val password: String): LogbookRepository {
companion object {
val TAG = "LogbookRepository"
val FILE_NAME = "lunatracker_logbook.json"
val FILE_NAME_START = "lunatracker_logbook"
val FILE_NAME_END = ".json"
}
val sardine: OkHttpSardine = OkHttpSardine()
@ -29,10 +32,10 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
)
}
override fun loadLogbook(context: Context, listener: LogbookLoadedListener) {
override fun loadLogbook(context: Context, name: String, listener: LogbookLoadedListener) {
Thread(Runnable {
try {
val logbook = loadLogbook(context)
val logbook = loadLogbook(name)
listener.onLogbookLoaded(logbook)
} catch (e: SardineException) {
Log.e(TAG, e.toString())
@ -52,12 +55,12 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start()
}
private fun loadLogbook(context: Context): Logbook {
val inputStream = sardine.get("$webDavURL/$FILE_NAME")
private fun loadLogbook(name: String,): Logbook {
val inputStream = sardine.get(getUrl(name))
val json = inputStream.bufferedReader().use(BufferedReader::readText)
inputStream.close()
val ja = JSONArray(json)
val logbook = Logbook()
val logbook = Logbook(name)
for (i in 0 until ja.length()) {
try {
val evt: LunaEvent = LunaEvent(ja.getJSONObject(i))
@ -95,6 +98,40 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start()
}
override fun listLogbooks(
context: Context,
listener: LogbookListObtainedListener
) {
Thread(Runnable {
try {
val logbooksNames = arrayListOf<String>()
for (dr: DavResource in sardine.list(webDavURL)){
if(!dr.name.startsWith(FILE_NAME_START))
continue
if(!dr.name.endsWith(FILE_NAME_END))
continue
logbooksNames.add(
dr.name.replace("${FILE_NAME_START}_", "")
.replace(FILE_NAME_START, "")
.replace(FILE_NAME_END, "")
)
}
listener.onLogbookListObtained(logbooksNames)
} catch (e: SardineException) {
Log.e(TAG, e.toString())
listener.onWebDAVError(e)
} catch (e: IOException) {
Log.e(TAG, e.toString())
listener.onIOError(e)
} catch (e: SocketTimeoutException) {
Log.e(TAG, e.toString())
listener.onIOError(e)
} catch (e: Exception) {
listener.onError(e)
}
}).start()
}
private fun saveLogbook(context: Context, logbook: Logbook) {
// Lock logbook on WebDAV to avoid concurrent changes
//sardine.lock(getUrl())
@ -108,64 +145,56 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
for (l in logbook.logs) {
ja.put(l.toJson())
}
sardine.put(getUrl(), ja.toString().toByteArray())
sardine.put(getUrl(logbook.name), ja.toString().toByteArray())
}
/**
* Connect to server and check if a logbook already exists.
* If it does not exist, try to upload the local one (or create a new one).
* If it does not exist, try to upload the local one.
* @return error, or null if no error
*/
fun createLogbook(context: Context, listener: LogbookCreatedListener) {
Thread(Runnable {
try {
loadLogbook(context)
listener.onLogbookCreated()
} catch (e: SardineException) {
if (e.toString().contains("404")) {
// Connection successful, but no existing save. Upload the local one.
try {
val flr = FileLogbookRepository()
val logbook = flr.loadLogbook(context)
saveLogbook(context, logbook)
Log.d(TAG, "Local logbook file found, uploaded")
listener.onLogbookCreated()
} catch (e: FileNotFoundException) {
Log.d(TAG, "No local logbook file found, uploading empty file")
saveLogbook(context, Logbook())
listener.onLogbookCreated()
} catch (e: SardineException) {
Log.e(TAG, "Unable to upload logbook: $e")
listener.onWebDAVError(e)
}
} else {
Log.e(TAG, e.toString())
listener.onWebDAVError(e)
fun uploadLogbookIfNotExists(context: Context, name: String): String? {
val flr = FileLogbookRepository()
try {
loadLogbook(name)
Log.d(TAG, "Logbook file $name already exist on the webDav share: will not overwrite it")
return null
} catch (e: SardineException) {
if (e.toString().contains("404")) {
// Connection successful, but logbook does not exist. Upload the local one.
try {
val logbook = flr.loadLogbook(context, name)
saveLogbook(context, logbook)
Log.d(TAG, "Local logbook file $name found, uploaded")
return null
} catch (e: FileNotFoundException) {
Log.e(TAG, "No local logbook file found, this should not happen!")
return "No local logbook file found, app is in inconsistent state, please delete and reinstall it"
} catch (e: SardineException) {
Log.e(TAG, "Unable to upload logbook: $e")
return e.toString()
}
} catch (e: IOException) {
} else {
Log.e(TAG, e.toString())
listener.onIOError(e)
} catch (e: SocketTimeoutException) {
Log.e(TAG, e.toString())
listener.onIOError(e)
} catch (e: JSONException) {
Log.e(TAG, e.toString())
listener.onJSONError(e)
} catch (e: Exception) {
listener.onError(e)
return e.toString()
}
}).start()
} catch (e: IOException) {
Log.e(TAG, e.toString())
return e.toString()
} catch (e: SocketTimeoutException) {
Log.e(TAG, e.toString())
return e.toString()
} catch (e: JSONException) {
Log.e(TAG, e.toString())
return e.toString()
} catch (e: Exception) {
return e.toString()
}
}
private fun getUrl(): String {
return "$webDavURL/$FILE_NAME"
}
interface LogbookCreatedListener {
fun onLogbookCreated()
fun onIOError(error: okio.IOException)
fun onWebDAVError(error: SardineException)
fun onJSONError(error: JSONException)
fun onError(error: Exception)
private fun getUrl(name: String): String {
val fileName = "${FILE_NAME_START}${if (name.isNotEmpty()) "_" else ""}${name}${FILE_NAME_END}"
Log.d(TAG, fileName)
return "$webDavURL/$fileName"
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/dropdown_list_item_background_pressed"/>
<item android:drawable="@drawable/dropdown_list_item_background_released"/>
</selector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="2dp"
android:color="@color/grey" />
<solid
android:color="@color/grey" />
<corners android:radius="15dp" />
<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />
</shape>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="2dp"
android:color="@color/grey" />
<solid
android:color="@color/cardview_dark_background"/>
<corners android:radius="15dp" />
<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />
</shape>

View File

@ -7,16 +7,77 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="30dp"
android:paddingTop="10dp"
android:paddingLeft="15dp"
android:paddingRight="15dp">
<TextView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title"
android:textSize="30dp"
android:gravity="center_horizontal"/>
android:orientation="horizontal">
<ImageView
android:id="@+id/button_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_gravity="end"
android:src="@drawable/ic_settings"
app:tint="@color/grey"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/title"
android:textSize="26dp"
android:gravity="center"/>
<ImageView
android:id="@+id/button_sync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_gravity="start"
android:src="@drawable/ic_sync"
app:tint="@color/grey"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="38dp"
android:layout_margin="10dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/button_background">
<Spinner
android:id="@+id/logbooks_spinner"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
<TextView
android:id="@+id/logbooks_add_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textStyle="bold"
android:textColor="@color/accent"
android:textSize="20dp"
android:text="+"
android:background="@drawable/button_background"/>
</LinearLayout>
<TextView
android:layout_width="match_parent"
@ -164,24 +225,6 @@
</LinearLayout>
<ImageView
android:id="@+id/button_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_gravity="end"
android:src="@drawable/ic_settings"
app:tint="@color/grey"/>
<ImageView
android:id="@+id/button_sync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_gravity="start"
android:src="@drawable/ic_sync"
app:tint="@color/grey"/>
<LinearLayout
android:id="@+id/no_connection_screen"
android:layout_width="match_parent"

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">
<TextView
android:id="@+id/dialog_add_logbook_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_add_logbook_message"/>
<EditText
android:id="@+id/dialog_add_logbook_logbookname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:lines="1"
android:inputType="text"
android:hint="@string/dialog_add_logbook_logbookname"
android:background="@drawable/textview_background"/>
</LinearLayout>

View File

@ -15,7 +15,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
android:background="@drawable/button_background"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_medicine"/>
@ -25,7 +25,7 @@
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/button_background"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_enema"/>
@ -35,7 +35,7 @@
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/button_background"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_note"/>
@ -45,7 +45,7 @@
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/button_background"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_temperature"/>
@ -55,7 +55,7 @@
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/button_background"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_colic"/>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="20dp"
android:paddingBottom="20dp"
android:singleLine="true"
android:ellipsize="end"/>

View File

@ -61,11 +61,13 @@
<string name="settings_storage_dav_pass">Password</string>
<string name="settings_network_error">Impossibile raggiungere il server: </string>
<string name="settings_webdav_error_denied">Nome utente o password WebDAV sbagliati</string>
<string name="settings_webdav_error_server_offline">Il server WebDAV non è al momento disponibile</string>
<string name="settings_webdav_error_generic">Si è verificato un errore tentando di accedere al server WebDAV:</string>
<string name="settings_webdav_creation_error_generic">Impossibile creare un file di salvataggio sul server WebDAV:</string>
<string name="settings_webdav_creation_ok">Connessione al server WebDAV avvenuta con successo</string>
<string name="settings_json_error">Sul server esiste un salvataggio, ma è corrotto o illeggibile. Cancellare il file </string>
<string name="settings_generic_error">Si è verificato un errore: </string>
<string name="settings_webdav_upload_error">Errore durante l\'upload del logbook locale %1$s su webdav: %2$s</string>
<string name="trim_logbook_dialog_title">Il tuo diario è bello grande!</string>
<string name="trim_logbook_dialog_message_local">Il file del tuo diario sta crescendo molto. Ti suggeriamo di cancellare gli eventi più vecchi per evitare problemi di memoria.</string>
@ -79,5 +81,15 @@
<string name="log_notes_dialog_note_hint">Inserisci le note</string>
<string name="dialog_event_detail_title">Dettaglio evento</string>
<string name="dialog_event_detail_save_button">Salva</string>
<string name="dialog_event_detail_delete_button">Elimina</string>
<string name="dialog_add_logbook_title">Aggiungi diario</string>
<string name="dialog_add_logbook_logbookname">👶 Nome del diario</string>
<string name="dialog_add_logbook_message">Scrivi un nome per identificare questo diario. Comparirà in cima allo schermo, e se usi WebDAV sarà incluso anche nel nome del file di salvataggio.\nSe vuoi un\'icona, inserisci una emoji!</string>
<string name="dialog_add_logbook_message_intro">Benvenuto! Per usare quest\'app devi creare almeno un diario. Probabilmente vuoi chiamarlo col nome di tuo figlio.</string>
<string name="default_logbook_name">👶 Il mio primo diario</string>
<string name="logbook_created">Creato nuovo diario: </string>
</resources>

View File

@ -75,11 +75,13 @@
<string name="settings_storage_dav_pass">Password</string>
<string name="settings_network_error">Unable to reach server: </string>
<string name="settings_webdav_error_denied">Wrong WebDAV user or password</string>
<string name="settings_webdav_error_server_offline">WebDAV server is currently unavailable</string>
<string name="settings_webdav_error_generic">Error while trying to access WebDAV:</string>
<string name="settings_webdav_creation_error_generic">Unable to save a file on the WebDAV server:</string>
<string name="settings_webdav_creation_ok">Successfully connected with WebDAV server</string>
<string name="settings_json_error">There\'s a save file on the server, but is corrupted or unreadable. Please delete it </string>
<string name="settings_generic_error">Error: </string>
<string name="settings_webdav_upload_error">Error while uploading local logbook %1$s to webdav: %2$s</string>
<string name="trim_logbook_dialog_title">Your logbook is pretty big!</string>
<string name="trim_logbook_dialog_message_local">Your logbook file is growing a lot. We suggest trimming the oldest events to avoid crashes.</string>
@ -102,5 +104,15 @@
<string name="measurement_unit_temperature_base_metric" translatable="false">°C</string>
<string name="dialog_event_detail_title">Event detail</string>
<string name="dialog_event_detail_save_button">Save</string>
<string name="dialog_event_detail_delete_button">Delete</string>
<string name="dialog_add_logbook_title">Add logbook</string>
<string name="dialog_add_logbook_logbookname">👶 Logbook name</string>
<string name="dialog_add_logbook_message">Write a name to identify this logbook. This name will appear on top of the screen and, if you use WebDAV, will be in the save file name as well.</string>
<string name="dialog_add_logbook_message_intro">Welcome! To use this app you need to create at least one logbook. You would probably want to call it with your child\'s name.</string>
<string name="default_logbook_name">👶 My first logbook</string>
<string name="logbook_created">New logbook created: </string>
</resources>