3 Commits

11 changed files with 238 additions and 27 deletions

View File

@ -24,6 +24,7 @@ import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.repository.FileLogbookRepository import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.LogbookLoadedListener import it.danieleverducci.lunatracker.repository.LogbookLoadedListener
import it.danieleverducci.lunatracker.repository.LogbookRepository import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.LogbookSavedListener import it.danieleverducci.lunatracker.repository.LogbookSavedListener
@ -156,7 +157,7 @@ class MainActivity : AppCompatActivity() {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
// Reload data // Reload data
loadLogbook() loadLogbookList()
} }
override fun onStop() { override fun onStop() {
@ -304,17 +305,89 @@ class MainActivity : AppCompatActivity() {
alertDialog.show() 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 buttons
val logbooksButtonsContainer =
findViewById<ViewGroup>(R.id.logbooks_buttons_container)
logbooksButtonsContainer.removeAllViews()
for (lbn in logbooksNames) {
val lbnButton = layoutInflater.inflate(
R.layout.logbook_button,
logbooksButtonsContainer,
false
) as TextView
lbnButton.setText(
if (lbn.isEmpty()) getString(R.string.default_logbook_name) else lbn
)
lbnButton.setOnClickListener({
loadLogbook(lbn)
})
logbooksButtonsContainer.addView(lbnButton)
}
// Show "Add logbook" button
val addLogbookButton = layoutInflater.inflate(
R.layout.logbook_button,
logbooksButtonsContainer,
false
) as TextView
addLogbookButton.setText("+")
addLogbookButton.setOnClickListener({showAddLogbookDialog()})
logbooksButtonsContainer.addView(addLogbookButton)
// Load logbook
loadLogbook()
})
}
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()
}
fun loadLogbook(name: String = LogbookRepository.DEFAULT_LOGBOOK_NAME) {
if (savingEvent) if (savingEvent)
return return
// TODO: Highlight logbook button
// Reset time counter // Reset time counter
handler.removeCallbacks(updateListRunnable) handler.removeCallbacks(updateListRunnable)
handler.postDelayed(updateListRunnable, UPDATE_EVERY_SECS*1000) handler.postDelayed(updateListRunnable, UPDATE_EVERY_SECS*1000)
// Load data // Load data
setLoading(true) setLoading(true)
logbookRepo?.loadLogbook(this, object: LogbookLoadedListener{ logbookRepo?.loadLogbook(this, name, object: LogbookLoadedListener{
override fun onLogbookLoaded(lb: Logbook) { override fun onLogbookLoaded(lb: Logbook) {
runOnUiThread({ runOnUiThread({
setLoading(false) setLoading(false)

View File

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

View File

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

View File

@ -9,26 +9,30 @@ import org.json.JSONException
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.FilenameFilter
class FileLogbookRepository: LogbookRepository { class FileLogbookRepository: LogbookRepository {
companion object { companion object {
val TAG = "FileLogbookRepository" 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 { try {
listener.onLogbookLoaded(loadLogbook(context)) listener.onLogbookLoaded(loadLogbook(context, name))
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
Log.d(TAG, "No logbook file found, create one") Log.d(TAG, "No logbook file found, create one")
val newLogbook = Logbook() val newLogbook = Logbook(name)
saveLogbook(context, newLogbook) saveLogbook(context, newLogbook)
listener.onLogbookLoaded(newLogbook) listener.onLogbookLoaded(newLogbook)
} }
} }
fun loadLogbook(context: Context): Logbook { fun loadLogbook(context: Context, name: String): Logbook {
val logbook = Logbook() val logbook = Logbook(name)
val file = File(context.getFilesDir(), "data.json") val fileName = getFileName(name)
val file = File(context.getFilesDir(), fileName)
val json = FileInputStream(file).bufferedReader().use { it.readText() } val json = FileInputStream(file).bufferedReader().use { it.readText() }
val ja = JSONArray(json) val ja = JSONArray(json)
for (i in 0 until ja.length()) { for (i in 0 until ja.length()) {
@ -53,11 +57,42 @@ class FileLogbookRepository: LogbookRepository {
} }
fun saveLogbook(context: Context, logbook: Logbook) { 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() val ja = JSONArray()
for (l in logbook.logs) { for (l in logbook.logs) {
ja.put(l.toJson()) ja.put(l.toJson())
} }
file.writeText(ja.toString()) 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 import org.json.JSONException
interface LogbookRepository { interface LogbookRepository {
fun loadLogbook(context: Context, listener: LogbookLoadedListener) companion object {
fun saveLogbook(context: Context, logbook: Logbook, listener: LogbookSavedListener) 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 { interface LogbookLoadedListener {
@ -25,4 +29,11 @@ interface LogbookSavedListener {
fun onWebDAVError(error: SardineException) fun onWebDAVError(error: SardineException)
fun onJSONError(error: JSONException) fun onJSONError(error: JSONException)
fun onError(error: Exception) 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.content.Context
import android.util.Log import android.util.Log
import com.thegrizzlylabs.sardineandroid.DavResource
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import com.thegrizzlylabs.sardineandroid.impl.SardineException import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.entities.Logbook import it.danieleverducci.lunatracker.entities.Logbook
@ -14,11 +15,13 @@ import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import kotlin.io.bufferedReader import kotlin.io.bufferedReader
import kotlin.text.replace
class WebDAVLogbookRepository(val webDavURL: String, val username: String, val password: String): LogbookRepository { class WebDAVLogbookRepository(val webDavURL: String, val username: String, val password: String): LogbookRepository {
companion object { companion object {
val TAG = "LogbookRepository" val TAG = "LogbookRepository"
val FILE_NAME = "lunatracker_logbook.json" val FILE_NAME_START = "lunatracker_logbook"
val FILE_NAME_END = ".json"
} }
val sardine: OkHttpSardine = OkHttpSardine() 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 { Thread(Runnable {
try { try {
val logbook = loadLogbook(context) val logbook = loadLogbook(name)
listener.onLogbookLoaded(logbook) listener.onLogbookLoaded(logbook)
} catch (e: SardineException) { } catch (e: SardineException) {
Log.e(TAG, e.toString()) Log.e(TAG, e.toString())
@ -52,12 +55,12 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start() }).start()
} }
private fun loadLogbook(context: Context): Logbook { private fun loadLogbook(name: String,): Logbook {
val inputStream = sardine.get("$webDavURL/$FILE_NAME") val inputStream = sardine.get(getUrl(name))
val json = inputStream.bufferedReader().use(BufferedReader::readText) val json = inputStream.bufferedReader().use(BufferedReader::readText)
inputStream.close() inputStream.close()
val ja = JSONArray(json) val ja = JSONArray(json)
val logbook = Logbook() val logbook = Logbook(name)
for (i in 0 until ja.length()) { for (i in 0 until ja.length()) {
try { try {
val evt: LunaEvent = LunaEvent(ja.getJSONObject(i)) val evt: LunaEvent = LunaEvent(ja.getJSONObject(i))
@ -95,6 +98,27 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start() }).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) { private fun saveLogbook(context: Context, logbook: Logbook) {
// Lock logbook on WebDAV to avoid concurrent changes // Lock logbook on WebDAV to avoid concurrent changes
//sardine.lock(getUrl()) //sardine.lock(getUrl())
@ -108,30 +132,30 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
for (l in logbook.logs) { for (l in logbook.logs) {
ja.put(l.toJson()) 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. * 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 (or create a new one).
*/ */
fun createLogbook(context: Context, listener: LogbookCreatedListener) { fun createLogbook(context: Context, name: String, listener: LogbookCreatedListener) {
Thread(Runnable { Thread(Runnable {
try { try {
loadLogbook(context) loadLogbook(name)
listener.onLogbookCreated() listener.onLogbookCreated()
} catch (e: SardineException) { } catch (e: SardineException) {
if (e.toString().contains("404")) { if (e.toString().contains("404")) {
// Connection successful, but no existing save. Upload the local one. // Connection successful, but no existing save. Upload the local one.
try { try {
val flr = FileLogbookRepository() val flr = FileLogbookRepository()
val logbook = flr.loadLogbook(context) val logbook = flr.loadLogbook(context, name)
saveLogbook(context, logbook) saveLogbook(context, logbook)
Log.d(TAG, "Local logbook file found, uploaded") Log.d(TAG, "Local logbook file found, uploaded")
listener.onLogbookCreated() listener.onLogbookCreated()
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
Log.d(TAG, "No local logbook file found, uploading empty file") Log.d(TAG, "No local logbook file found, uploading empty file")
saveLogbook(context, Logbook()) saveLogbook(context, Logbook(name))
listener.onLogbookCreated() listener.onLogbookCreated()
} catch (e: SardineException) { } catch (e: SardineException) {
Log.e(TAG, "Unable to upload logbook: $e") Log.e(TAG, "Unable to upload logbook: $e")
@ -156,8 +180,10 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start() }).start()
} }
private fun getUrl(): String { private fun getUrl(name: String): String {
return "$webDavURL/$FILE_NAME" val fileName = "${FILE_NAME_START}${if (name.isNotEmpty()) "_" else ""}${name}${FILE_NAME_END}"
Log.d(TAG, fileName)
return "$webDavURL/$fileName"
} }

View File

@ -17,6 +17,22 @@
android:text="@string/title" android:text="@string/title"
android:textSize="30dp" android:textSize="30dp"
android:gravity="center_horizontal"/> android:gravity="center_horizontal"/>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp">
<LinearLayout
android:id="@+id/logbooks_buttons_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
</LinearLayout>
</HorizontalScrollView>
<TextView <TextView
android:layout_width="match_parent" 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

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:background="@drawable/button_background"
android:textStyle="bold"
android:textColor="@color/accent"/>

View File

@ -80,4 +80,10 @@
<string name="dialog_event_detail_title">Dettaglio evento</string> <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">Senza nome</string>
</resources> </resources>

View File

@ -103,4 +103,10 @@
<string name="dialog_event_detail_title">Event detail</string> <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">Unnamed</string>
</resources> </resources>