7 Commits

16 changed files with 327 additions and 55 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

@ -7,13 +7,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 +29,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
@ -77,6 +83,7 @@ class MainActivity : AppCompatActivity() {
recyclerView.adapter = adapter
// Set listeners
findViewById<View>(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog() }
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(
@ -156,7 +163,7 @@ class MainActivity : AppCompatActivity() {
adapter.notifyDataSetChanged()
// Reload data
loadLogbook()
loadLogbookList()
}
override fun onStop() {
@ -304,7 +311,74 @@ class MainActivity : AppCompatActivity() {
alertDialog.show()
}
fun loadLogbook() {
fun showAddLogbookDialog() {
val d = AlertDialog.Builder(this)
d.setTitle(R.string.dialog_add_logbook_title)
val dialogView = layoutInflater.inflate(R.layout.dialog_add_logbook, null)
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()) }
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
val alertDialog = d.create()
alertDialog.show()
}
fun loadLogbookList() {
setLoading(true)
logbookRepo?.listLogbooks(this, object: LogbookListObtainedListener {
override fun onLogbookListObtained(logbooksNames: ArrayList<String>) {
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) {
TODO("Not yet implemented")
}
override fun onWebDAVError(error: SardineException) {
TODO("Not yet implemented")
}
override fun onError(error: Exception) {
TODO("Not yet implemented")
}
})
}
fun addLogbook(logbookName: String) {
this.logbook = Logbook(logbookName)
saveLogbook()
loadLogbookList() // TODO: Does not reload logbooks buttons on top, why?
}
fun loadLogbook(name: String = LogbookRepository.DEFAULT_LOGBOOK_NAME) {
if (savingEvent)
return
@ -314,7 +388,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)

View File

@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException
import org.json.JSONException
@ -72,7 +73,7 @@ open class SettingsActivity : AppCompatActivity() {
textViewWebDAVPass.text.toString()
)
progressIndicator.visibility = View.VISIBLE
webDAVLogbookRepo.createLogbook(this, object: WebDAVLogbookRepository.LogbookCreatedListener{
webDAVLogbookRepo.createLogbook(this, LogbookRepository.DEFAULT_LOGBOOK_NAME, object: WebDAVLogbookRepository.LogbookCreatedListener{
override fun onLogbookCreated() {
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE

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,42 @@ 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
) {
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())
listener.onLogbookListObtained(arrayListOf())
val logbooksNames = arrayListOf<String>()
logbooksFileNames.forEach { it ->
logbooksNames.add(
it.replace("${FILE_NAME_START}_", "").replace(FILE_NAME_START, "").replace(FILE_NAME_END, "")
)
}
listener.onLogbookListObtained(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)
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 {
@ -26,3 +30,10 @@ interface LogbookSavedListener {
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,27 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start()
}
override fun listLogbooks(
context: Context,
listener: LogbookListObtainedListener
) {
Thread(Runnable {
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)
}).start()
}
private fun saveLogbook(context: Context, logbook: Logbook) {
// Lock logbook on WebDAV to avoid concurrent changes
//sardine.lock(getUrl())
@ -108,30 +132,30 @@ 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).
*/
fun createLogbook(context: Context, listener: LogbookCreatedListener) {
fun createLogbook(context: Context, name: String, listener: LogbookCreatedListener) {
Thread(Runnable {
try {
loadLogbook(context)
loadLogbook(name)
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)
val logbook = flr.loadLogbook(context, name)
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())
saveLogbook(context, Logbook(name))
listener.onLogbookCreated()
} catch (e: SardineException) {
Log.e(TAG, "Unable to upload logbook: $e")
@ -156,8 +180,10 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start()
}
private fun getUrl(): String {
return "$webDavURL/$FILE_NAME"
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: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="30dp"
android:gravity="center_horizontal"/>
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,24 @@
<?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: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

@ -80,4 +80,10 @@
<string name="dialog_event_detail_title">Dettaglio evento</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.</string>
<string name="default_logbook_name">👶 Il mio primo diario</string>
</resources>

View File

@ -103,4 +103,10 @@
<string name="dialog_event_detail_title">Event detail</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="default_logbook_name">👶 My first logbook</string>
</resources>