First working sync (with hardcoded credentials and no concurrent modification checks)

This commit is contained in:
Daniele Verducci 2024-11-07 19:43:56 +01:00
parent 106f721365
commit 6432045419
14 changed files with 229 additions and 120 deletions

3
.gitignore vendored
View File

@ -103,3 +103,6 @@ app/release/output-metadata.json
.idea/misc.xml
.idea/deploymentTargetSelector.xml
.idea/other.xml
# Other
app/src/main/java/it/danieleverducci/lunatracker/TemporaryHardcodedCredentials.kt

3
.idea/.gitignore vendored
View File

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@ -1,57 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -51,6 +51,8 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.recyclerview)
implementation("com.github.thegrizzlylabs:sardine-android:v0.9")
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"

View File

@ -1,22 +1,25 @@
package it.danieleverducci.lunatracker
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log
import android.view.View
import android.widget.NumberPicker
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.LinearProgressIndicator
import it.danieleverducci.lunatracker.adapters.LunaEventRecyclerAdapter
import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.entities.LunaEventType
import it.danieleverducci.lunatracker.repository.LogbookLoadedListener
import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.LogbookSavedListener
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import kotlinx.coroutines.Runnable
class MainActivity : AppCompatActivity() {
@ -28,28 +31,30 @@ class MainActivity : AppCompatActivity() {
lateinit var logbook: Logbook
lateinit var adapter: LunaEventRecyclerAdapter
lateinit var progressIndicator: LinearProgressIndicator
lateinit var recyclerView: RecyclerView
lateinit var handler: Handler
val updateListRunnable: Runnable = Runnable {
adapter.notifyDataSetChanged()
handler.postDelayed(updateListRunnable, 1000*30)
loadLogbook()
handler.postDelayed(updateListRunnable, 1000*60)
}
val logbookRepo: LogbookRepository = WebDAVLogbookRepository( // TODO: support also FileLogbookRepository
TemporaryHardcodedCredentials.URL,
TemporaryHardcodedCredentials.USERNAME,
TemporaryHardcodedCredentials.PASSWORD
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handler = Handler(mainLooper)
// Load data
logbook = Logbook.load(this)
adapter = LunaEventRecyclerAdapter(this)
// Show view
setContentView(R.layout.activity_main)
// Show logbook
progressIndicator = findViewById<LinearProgressIndicator>(R.id.progress_indicator)
recyclerView = findViewById<RecyclerView>(R.id.list_events)
recyclerView.setLayoutManager(LinearLayoutManager(this))
adapter = LunaEventRecyclerAdapter(this)
adapter.items.addAll(logbook.logs)
recyclerView.setLayoutManager(LinearLayoutManager(applicationContext))
recyclerView.adapter = adapter
// Set listeners
@ -82,11 +87,18 @@ class MainActivity : AppCompatActivity() {
) }
}
fun showLogbook() {
// Show logbook
adapter.items.clear()
adapter.items.addAll(logbook.logs)
adapter.notifyDataSetChanged()
}
override fun onStart() {
super.onStart()
// Update list dates
adapter.notifyDataSetChanged()
loadLogbook()
handler.postDelayed(updateListRunnable, 1000*30)
}
@ -138,13 +150,52 @@ class MainActivity : AppCompatActivity() {
alertDialog.show()
}
fun loadLogbook() {
// Load data
progressIndicator.visibility = View.VISIBLE
logbookRepo.loadLogbook(this, object: LogbookLoadedListener{
override fun onLogbookLoaded(lb: Logbook) {
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
logbook = lb
showLogbook()
})
}
override fun onError(error: String) {
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
Log.e(TAG, "Unable to load logbook. Create a new one.")
logbook = Logbook()
showLogbook()
})
}
})
}
fun logEvent(event: LunaEvent) {
adapter.items.add(0, event)
adapter.notifyItemInserted(0)
recyclerView.smoothScrollToPosition(0)
progressIndicator.visibility = View.VISIBLE
logbook.logs.add(0, event)
logbook.save(this)
logbookRepo.saveLogbook(this, logbook, object: LogbookSavedListener{
override fun onLogbookSaved() {
Log.d(TAG, "Logbook saved")
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
})
}
override fun onError(error: String) {
Log.e(TAG, "ERROR: Logbook was NOT saved!")
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
})
}
})
Toast.makeText(this, R.string.toast_event_added, Toast.LENGTH_SHORT).show()
}

View File

@ -1,42 +1,5 @@
package it.danieleverducci.lunatracker.entities
import android.content.Context
import android.util.Log
import org.json.JSONArray
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
class Logbook {
companion object {
val TAG = "Logbook"
fun load(context: Context): Logbook {
val logbook = Logbook()
val file = File(context.getFilesDir(), "data.json")
try {
val json = FileInputStream(file).bufferedReader().use { it.readText() }
val ja = JSONArray(json)
for (i in 0 until ja.length()) {
val jo = ja.getJSONObject(i)
val evt = LunaEvent.fromJson(jo)
logbook.logs.add(evt)
}
} catch (e: FileNotFoundException) {
Log.d(TAG, "No logbook file found")
}
return logbook
}
}
val logs = ArrayList<LunaEvent>()
fun save(context: Context) {
val file = File(context.getFilesDir(), "data.json")
val ja = JSONArray()
for (l in logs) {
ja.put(l.toJson())
}
file.writeText(ja.toString())
}
}

View File

@ -0,0 +1,48 @@
package it.danieleverducci.lunatracker.repository
import android.content.Context
import it.danieleverducci.lunatracker.entities.Logbook
import android.util.Log
import it.danieleverducci.lunatracker.entities.LunaEvent
import org.json.JSONArray
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
class FileLogbookRepository: LogbookRepository {
companion object {
val TAG = "FileLogbookRepository"
}
override fun loadLogbook(context: Context, listener: LogbookLoadedListener) {
val logbook = Logbook()
val file = File(context.getFilesDir(), "data.json")
try {
val json = FileInputStream(file).bufferedReader().use { it.readText() }
val ja = JSONArray(json)
for (i in 0 until ja.length()) {
val jo = ja.getJSONObject(i)
val evt = LunaEvent.fromJson(jo)
logbook.logs.add(evt)
}
} catch (e: FileNotFoundException) {
Log.d(TAG, "No logbook file found")
listener.onError(e.toString())
}
listener.onLogbookLoaded(logbook)
}
override fun saveLogbook(
context: Context,
logbook: Logbook,
listener: LogbookSavedListener
) {
val file = File(context.getFilesDir(), "data.json")
val ja = JSONArray()
for (l in logbook.logs) {
ja.put(l.toJson())
}
file.writeText(ja.toString())
listener.onLogbookSaved()
}
}

View File

@ -0,0 +1,19 @@
package it.danieleverducci.lunatracker.repository
import android.content.Context
import it.danieleverducci.lunatracker.entities.Logbook
interface LogbookRepository {
fun loadLogbook(context: Context, listener: LogbookLoadedListener)
fun saveLogbook(context: Context, logbook: Logbook, listener: LogbookSavedListener)
}
interface LogbookLoadedListener {
fun onLogbookLoaded(logbook: Logbook)
fun onError(error: String)
}
interface LogbookSavedListener {
fun onLogbookSaved()
fun onError(error: String)
}

View File

@ -0,0 +1,74 @@
package it.danieleverducci.lunatracker.repository
import android.content.Context
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.TemporaryHardcodedCredentials
import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent
import kotlinx.coroutines.Runnable
import org.json.JSONArray
import java.io.BufferedReader
import kotlin.io.bufferedReader
class WebDAVLogbookRepository(val webDavURL: String, val username: String, val password: String): LogbookRepository {
companion object {
val TAG = "LogbookRepository"
val FILE_NAME = "lunatracker_logbook.json"
}
val sardine: OkHttpSardine = OkHttpSardine()
init {
sardine.setCredentials(
username,
password
)
}
override fun loadLogbook(context: Context, listener: LogbookLoadedListener) {
Thread(Runnable {
try {
val inputStream = sardine.get("$webDavURL/$FILE_NAME")
val json = inputStream.bufferedReader().use(BufferedReader::readText)
val ja = JSONArray(json)
val logbook = Logbook()
for (i in 0 until ja.length()) {
val jo = ja.getJSONObject(i)
val evt = LunaEvent.fromJson(jo)
logbook.logs.add(evt)
}
listener.onLogbookLoaded(logbook)
} catch (e: SardineException) {
listener.onError(e.toString())
}
}).start()
}
override fun saveLogbook(context: Context, logbook: Logbook, listener: LogbookSavedListener) {
Thread(Runnable {
// Lock logbook on WebDAV to avoid concurrent changes
//sardine.lock(getUrl())
// Reload logbook from WebDAV
// Merge logbooks (based on time)
// Write logbook
// Unlock logbook on WebDAV
//sardine.unlock(getUrl())
val ja = JSONArray()
for (l in logbook.logs) {
ja.put(l.toJson())
}
try {
sardine.put(getUrl(), ja.toString().toByteArray())
listener.onLogbookSaved()
} catch (e: SardineException) {
listener.onError(e.toString())
}
}).start()
}
private fun getUrl(): String {
return "${TemporaryHardcodedCredentials.URL}/$FILE_NAME"
}
}

View File

@ -1,7 +1,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:paddingTop="30dp"
android:paddingLeft="15dp"
@ -116,11 +116,21 @@
</LinearLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:indeterminate="true"
app:indicatorColor="@color/accent"
android:visibility="invisible"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Diario di bordo"
android:layout_marginTop="30dp"/>
android:textColor="@color/accent"
android:textStyle="bold"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_events"

View File

@ -10,6 +10,7 @@ activityCompose = "1.8.0"
composeBom = "2024.04.01"
appcompat = "1.7.0"
recyclerview = "1.3.2"
material = "1.12.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -28,6 +29,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

View File

@ -16,6 +16,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io")
}
}