package it.danieleverducci.lunatracker import android.net.Uri import android.os.Bundle import android.view.View import android.widget.EditText import android.widget.RadioButton import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.switchmaterial.SwitchMaterial import com.thegrizzlylabs.sardineandroid.impl.SardineException 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.WebDAVLogbookRepository import okio.IOException import org.json.JSONArray import org.json.JSONObject open class SettingsActivity : AppCompatActivity() { protected lateinit var settingsRepository: LocalSettingsRepository protected lateinit var radioDataLocal: RadioButton protected lateinit var radioDataWebDAV: RadioButton protected lateinit var textViewWebDAVUrl: TextView protected lateinit var textViewWebDAVUser: TextView protected lateinit var textViewWebDAVPass: TextView protected lateinit var progressIndicator: LinearProgressIndicator protected lateinit var switchNoBreastfeeding: SwitchMaterial protected lateinit var textViewSignature: EditText // Activity Result Launchers for Export/Import 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_settings) radioDataLocal = findViewById(R.id.settings_data_local) radioDataWebDAV = findViewById(R.id.settings_data_webdav) textViewWebDAVUrl = findViewById(R.id.settings_data_webdav_url) textViewWebDAVUser = findViewById(R.id.settings_data_webdav_user) textViewWebDAVPass = findViewById(R.id.settings_data_webdav_pass) progressIndicator = findViewById(R.id.progress_indicator) switchNoBreastfeeding = findViewById(R.id.switch_no_breastfeeding) textViewSignature = findViewById(R.id.settings_signature) findViewById(R.id.settings_save).setOnClickListener({ validateAndSave() }) findViewById(R.id.settings_cancel).setOnClickListener({ finish() }) findViewById(R.id.settings_export).setOnClickListener({ startExport() }) findViewById(R.id.settings_import).setOnClickListener({ startImport() }) settingsRepository = LocalSettingsRepository(this) loadSettings() } fun loadSettings() { val dataRepo = settingsRepository.loadDataRepository() val webDavCredentials = settingsRepository.loadWebdavCredentials() val noBreastfeeding = settingsRepository.loadNoBreastfeeding() val signature = settingsRepository.loadSignature() when (dataRepo) { LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> radioDataLocal.isChecked = true LocalSettingsRepository.DATA_REPO.WEBDAV -> radioDataWebDAV.isChecked = true } textViewSignature.setText(signature) switchNoBreastfeeding.isChecked = noBreastfeeding if (webDavCredentials != null) { textViewWebDAVUrl.text = webDavCredentials[0] textViewWebDAVUser.text = webDavCredentials[1] textViewWebDAVPass.text = webDavCredentials[2] } } fun validateAndSave() { if (radioDataLocal.isChecked) { // No validation required, just save saveSettings() return } // Try to connect to WebDAV and check if the save file already exists val webDAVLogbookRepo = WebDAVLogbookRepository( textViewWebDAVUrl.text.toString(), textViewWebDAVUser.text.toString(), textViewWebDAVPass.text.toString() ) progressIndicator.visibility = View.VISIBLE webDAVLogbookRepo.listLogbooks(this, object: LogbookListObtainedListener{ override fun onLogbookListObtained(logbooksNames: ArrayList) { 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) { runOnUiThread({ progressIndicator.visibility = View.INVISIBLE Toast.makeText(this@SettingsActivity, getString(R.string.settings_network_error) + error.toString(), Toast.LENGTH_SHORT).show() }) } override fun onWebDAVError(error: SardineException) { runOnUiThread({ progressIndicator.visibility = View.INVISIBLE if(error.toString().contains("401")) { Toast.makeText(this@SettingsActivity, getString(R.string.settings_webdav_error_denied), Toast.LENGTH_SHORT).show() } else { Toast.makeText(this@SettingsActivity, getString(R.string.settings_webdav_error_generic) + error.toString(), Toast.LENGTH_SHORT).show() } }) } 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 Toast.makeText(this@SettingsActivity, getString(R.string.settings_json_error) + error.toString(), Toast.LENGTH_SHORT).show() }) } })*/ } fun saveSettings() { settingsRepository.saveDataRepository( if (radioDataWebDAV.isChecked) LocalSettingsRepository.DATA_REPO.WEBDAV else LocalSettingsRepository.DATA_REPO.LOCAL_FILE ) settingsRepository.saveNoBreastfeeding(switchNoBreastfeeding.isChecked) settingsRepository.saveSignature(textViewSignature.text.toString()) settingsRepository.saveWebdavCredentials( textViewWebDAVUrl.text.toString(), textViewWebDAVUser.text.toString(), textViewWebDAVPass.text.toString() ) finish() } /** * Copies the local logbooks to webdav. * @return success */ private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) { Thread(Runnable { val 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?) } // Export/Import functionality 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 version = json.optInt("version", 1) 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() } }