diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3267bd3..3d08cfb 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -19,6 +19,10 @@ android {
}
buildTypes {
+ debug {
+ applicationIdSuffix = ".theo"
+ resValue("string", "app_name", "Theotracker")
+ }
release {
isMinifyEnabled = false
proguardFiles(
@@ -52,6 +56,7 @@ dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.recyclerview)
implementation("com.github.thegrizzlylabs:sardine-android:v0.9")
+ implementation("com.google.android.flexbox:flexbox:3.0.0")
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b335ff9..a99d564 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,6 +34,18 @@
android:name=".StatisticsActivity"
android:label="@string/statistics_title"
android:theme="@style/Theme.LunaTracker"/>
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/BackupActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/BackupActivity.kt
new file mode 100644
index 0000000..c30b80f
--- /dev/null
+++ b/app/src/main/java/it/danieleverducci/lunatracker/BackupActivity.kt
@@ -0,0 +1,177 @@
+package it.danieleverducci.lunatracker
+
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.material.progressindicator.LinearProgressIndicator
+import it.danieleverducci.lunatracker.entities.Logbook
+import it.danieleverducci.lunatracker.entities.LunaEvent
+import it.danieleverducci.lunatracker.repository.FileLogbookRepository
+import org.json.JSONArray
+import org.json.JSONObject
+
+/**
+ * Activity for backup functionality (export/import logbooks).
+ */
+class BackupActivity : AppCompatActivity() {
+
+ private lateinit var progressIndicator: LinearProgressIndicator
+
+ private val exportLauncher = registerForActivityResult(
+ ActivityResultContracts.CreateDocument("application/json")
+ ) { uri -> uri?.let { exportLogbookToUri(it) } }
+
+ private val importLauncher = registerForActivityResult(
+ ActivityResultContracts.OpenDocument()
+ ) { uri -> uri?.let { importLogbookFromUri(it) } }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_backup)
+
+ progressIndicator = findViewById(R.id.progress_indicator)
+
+ findViewById(R.id.backup_export).setOnClickListener {
+ startExport()
+ }
+ findViewById(R.id.backup_import).setOnClickListener {
+ startImport()
+ }
+ findViewById(R.id.backup_close).setOnClickListener {
+ finish()
+ }
+ }
+
+ private fun startExport() {
+ val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.US)
+ .format(java.util.Date())
+ exportLauncher.launch("lunatracker_backup_$timestamp.json")
+ }
+
+ private fun startImport() {
+ importLauncher.launch(arrayOf("application/json"))
+ }
+
+ private fun exportLogbookToUri(uri: Uri) {
+ progressIndicator.visibility = View.VISIBLE
+ Thread {
+ try {
+ val fileLogbookRepo = FileLogbookRepository()
+ val logbooks = fileLogbookRepo.getAllLogbooks(this)
+
+ val json = JSONObject().apply {
+ put("version", 1)
+ put("app", "LunaTracker")
+ put("exported_at", System.currentTimeMillis())
+ put("logbooks", JSONArray().apply {
+ logbooks.forEach { logbook ->
+ put(JSONObject().apply {
+ put("name", logbook.name)
+ put("events", JSONArray().apply {
+ logbook.logs.forEach { event ->
+ put(event.toJson())
+ }
+ })
+ })
+ }
+ })
+ }
+
+ contentResolver.openOutputStream(uri)?.use { outputStream ->
+ outputStream.write(json.toString(2).toByteArray(Charsets.UTF_8))
+ }
+
+ val eventCount = logbooks.sumOf { it.logs.size }
+ runOnUiThread {
+ progressIndicator.visibility = View.INVISIBLE
+ Toast.makeText(
+ this,
+ getString(R.string.export_success, eventCount),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ } catch (e: Exception) {
+ runOnUiThread {
+ progressIndicator.visibility = View.INVISIBLE
+ Toast.makeText(
+ this,
+ getString(R.string.export_error) + e.message,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }.start()
+ }
+
+ private fun importLogbookFromUri(uri: Uri) {
+ progressIndicator.visibility = View.VISIBLE
+ Thread {
+ try {
+ val jsonString = contentResolver.openInputStream(uri)?.bufferedReader()?.readText()
+ ?: throw Exception("Could not read file")
+
+ val json = JSONObject(jsonString)
+
+ val fileLogbookRepo = FileLogbookRepository()
+ var totalEvents = 0
+
+ if (json.has("logbooks")) {
+ // New format with multiple logbooks
+ val logbooksArray = json.getJSONArray("logbooks")
+ for (i in 0 until logbooksArray.length()) {
+ val logbookJson = logbooksArray.getJSONObject(i)
+ val name = logbookJson.optString("name", "")
+ val eventsArray = logbookJson.getJSONArray("events")
+
+ val logbook = Logbook(name)
+ for (j in 0 until eventsArray.length()) {
+ try {
+ logbook.logs.add(LunaEvent(eventsArray.getJSONObject(j)))
+ totalEvents++
+ } catch (e: IllegalArgumentException) {
+ // Skip invalid events
+ }
+ }
+ fileLogbookRepo.saveLogbook(this, logbook)
+ }
+ } else if (json.has("events")) {
+ // Old format with single logbook
+ val name = json.optString("logbook_name", "")
+ val eventsArray = json.getJSONArray("events")
+
+ val logbook = Logbook(name)
+ for (i in 0 until eventsArray.length()) {
+ try {
+ logbook.logs.add(LunaEvent(eventsArray.getJSONObject(i)))
+ totalEvents++
+ } catch (e: IllegalArgumentException) {
+ // Skip invalid events
+ }
+ }
+ fileLogbookRepo.saveLogbook(this, logbook)
+ }
+
+ runOnUiThread {
+ progressIndicator.visibility = View.INVISIBLE
+ Toast.makeText(
+ this,
+ getString(R.string.import_success, totalEvents),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ } catch (e: Exception) {
+ runOnUiThread {
+ progressIndicator.visibility = View.INVISIBLE
+ Toast.makeText(
+ this,
+ getString(R.string.import_error) + ": " + e.message,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }.start()
+ }
+}
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/ButtonConfigActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/ButtonConfigActivity.kt
new file mode 100644
index 0000000..eac7990
--- /dev/null
+++ b/app/src/main/java/it/danieleverducci/lunatracker/ButtonConfigActivity.kt
@@ -0,0 +1,71 @@
+package it.danieleverducci.lunatracker
+
+import android.os.Bundle
+import android.widget.Button
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import it.danieleverducci.lunatracker.adapters.ButtonConfigAdapter
+import it.danieleverducci.lunatracker.adapters.ButtonConfigItemTouchHelperCallback
+import it.danieleverducci.lunatracker.repository.ButtonConfigRepository
+
+/**
+ * Activity for configuring which buttons appear on the main screen
+ * and in what order.
+ */
+class ButtonConfigActivity : AppCompatActivity() {
+
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var adapter: ButtonConfigAdapter
+ private lateinit var itemTouchHelper: ItemTouchHelper
+ private lateinit var repository: ButtonConfigRepository
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_button_config)
+
+ repository = ButtonConfigRepository(this)
+
+ setupRecyclerView()
+ setupButtons()
+ }
+
+ private fun setupRecyclerView() {
+ recyclerView = findViewById(R.id.button_list)
+ recyclerView.layoutManager = LinearLayoutManager(this)
+
+ // Load current configuration
+ val configs = repository.loadConfigs().toMutableList()
+
+ adapter = ButtonConfigAdapter(configs) { viewHolder ->
+ itemTouchHelper.startDrag(viewHolder)
+ }
+
+ recyclerView.adapter = adapter
+
+ // Setup drag-and-drop
+ val callback = ButtonConfigItemTouchHelperCallback(adapter)
+ itemTouchHelper = ItemTouchHelper(callback)
+ itemTouchHelper.attachToRecyclerView(recyclerView)
+ }
+
+ private fun setupButtons() {
+ findViewById