package it.danieleverducci.lunatracker import android.app.DatePickerDialog import android.app.TimePickerDialog import android.content.DialogInterface import android.content.Intent import android.content.res.ColorStateList import android.os.Bundle import android.os.Handler 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.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.slider.Slider import com.thegrizzlylabs.sardineandroid.impl.SardineException import it.danieleverducci.lunatracker.adapters.DaySeparatorDecoration import it.danieleverducci.lunatracker.adapters.LunaEventRecyclerAdapter 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 import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository import kotlinx.coroutines.Runnable import okio.IOException import org.json.JSONException import utils.DateUtils import utils.NumericUtils import java.util.Calendar import java.util.Date class MainActivity : AppCompatActivity() { companion object { const val TAG = "MainActivity" const val UPDATE_EVERY_SECS: Long = 30 const val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false } var logbook: Logbook? = null var pauseLogbookUpdate = false lateinit var progressIndicator: LinearProgressIndicator lateinit var buttonsContainer: ViewGroup lateinit var recyclerView: RecyclerView lateinit var handler: Handler var signature = "" var savingEvent = false val updateListRunnable: Runnable = Runnable { if (logbook != null && !pauseLogbookUpdate) loadLogbook(logbook!!.name) handler.postDelayed(updateListRunnable, 1000*60) } var logbookRepo: LogbookRepository? = null var showingOverflowPopupWindow = false // Breastfeeding timer state var bfTimerStartTime: Long = 0 var bfTimerType: String? = null var bfTimerDialog: AlertDialog? = null var bfTimerHandler: Handler? = null var bfTimerRunnable: Runnable? = null // Sleep timer state var sleepTimerStartTime: Long = 0 var sleepTimerDialog: AlertDialog? = null var sleepTimerHandler: Handler? = null var sleepTimerRunnable: Runnable? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) handler = Handler(mainLooper) // Show view setContentView(R.layout.activity_main) progressIndicator = findViewById(R.id.progress_indicator) buttonsContainer = findViewById(R.id.buttons_container) recyclerView = findViewById(R.id.list_events) recyclerView.setLayoutManager(LinearLayoutManager(applicationContext)) // Set listeners findViewById(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog(true) } findViewById(R.id.button_bottle).setOnClickListener { askBabyBottleContent() } findViewById(R.id.button_food).setOnClickListener { askNotes(LunaEvent(LunaEvent.TYPE_FOOD)) } findViewById(R.id.button_nipple_left).setOnClickListener { startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE) } findViewById(R.id.button_nipple_left).setOnLongClickListener { askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE) true } findViewById(R.id.button_nipple_both).setOnClickListener { startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE) } findViewById(R.id.button_nipple_both).setOnLongClickListener { askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE) true } findViewById(R.id.button_nipple_right).setOnClickListener { startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE) } findViewById(R.id.button_nipple_right).setOnLongClickListener { askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE) true } findViewById(R.id.button_change_poo).setOnClickListener { logEvent( LunaEvent( LunaEvent.TYPE_DIAPERCHANGE_POO ) ) } findViewById(R.id.button_change_pee).setOnClickListener { logEvent( LunaEvent( LunaEvent.TYPE_DIAPERCHANGE_PEE ) ) } val moreButton = findViewById(R.id.button_more) moreButton.setOnClickListener { showOverflowPopupWindow(moreButton) } findViewById(R.id.button_no_connection_settings).setOnClickListener { showSettings() } findViewById(R.id.button_settings).setOnClickListener { showSettings() } findViewById(R.id.button_statistics).setOnClickListener { showStatistics() } findViewById(R.id.button_no_connection_retry).setOnClickListener { // This may happen at start, when logbook is still null: better ask the logbook list loadLogbookList() } findViewById(R.id.button_sync).setOnClickListener { loadLogbookList() } } private fun setListAdapter(items: ArrayList) { val adapter = LunaEventRecyclerAdapter(this, items) adapter.onItemClickListener = object: LunaEventRecyclerAdapter.OnItemClickListener { override fun onItemClick(event: LunaEvent) { showEventDetailDialog(event, items) } } recyclerView.adapter = adapter // Tages-Trenner hinzufügen while (recyclerView.itemDecorationCount > 0) { recyclerView.removeItemDecorationAt(0) } recyclerView.addItemDecoration(DaySeparatorDecoration(this, items)) } fun showSettings() { val i = Intent(this, SettingsActivity::class.java) startActivity(i) } fun showStatistics() { val i = Intent(this, StatisticsActivity::class.java) i.putExtra(StatisticsActivity.EXTRA_LOGBOOK_NAME, logbook?.name ?: "") startActivity(i) } fun showLogbook() { // Show logbook if (logbook == null) Log.w(TAG, "showLogbook(): logbook is null!") setListAdapter(logbook?.logs ?: arrayListOf()) } override fun onStart() { super.onStart() val settingsRepository = LocalSettingsRepository(this) if (settingsRepository.loadDataRepository() == LocalSettingsRepository.DATA_REPO.WEBDAV) { val webDavCredentials = settingsRepository.loadWebdavCredentials() if (webDavCredentials == null) { throw IllegalStateException("Corrupted local settings: repo is webdav, but no webdav login data saved") } logbookRepo = WebDAVLogbookRepository( webDavCredentials[0], webDavCredentials[1], webDavCredentials[2] ) } else { logbookRepo = FileLogbookRepository() } signature = settingsRepository.loadSignature() val noBreastfeeding = settingsRepository.loadNoBreastfeeding() findViewById(R.id.layout_nipples).visibility = when (noBreastfeeding) { true -> View.GONE false -> View.VISIBLE } // Update list dates recyclerView.adapter?.notifyDataSetChanged() // Check for ongoing breastfeeding timer restoreBreastfeedingTimerIfNeeded() // Check for ongoing sleep timer restoreSleepTimerIfNeeded() if (logbook != null) { // Already running: reload data for currently selected logbook loadLogbook(logbook!!.name) } else { // First start: load logbook list loadLogbookList() } } override fun onStop() { handler.removeCallbacks(updateListRunnable) // Clean up breastfeeding timer UI (state is preserved in SharedPreferences) bfTimerRunnable?.let { bfTimerHandler?.removeCallbacks(it) } bfTimerDialog?.dismiss() // Clean up sleep timer UI (state is preserved in SharedPreferences) sleepTimerRunnable?.let { sleepTimerHandler?.removeCallbacks(it) } sleepTimerDialog?.dismiss() super.onStop() } fun askBabyBottleContent() { // Show number picker dialog val localSettings = LocalSettingsRepository(this) val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.number_picker_dialog, null) d.setTitle(R.string.log_bottle_dialog_title) d.setMessage(R.string.log_bottle_dialog_description) d.setView(dialogView) val numberPicker = dialogView.findViewById(R.id.dialog_number_picker) numberPicker.minValue = 1 // "10" numberPicker.maxValue = 25 // "250 numberPicker.displayedValues = ((10..250 step 10).map { it.toString() }.toTypedArray()) numberPicker.wrapSelectorWheel = false numberPicker.value = localSettings.loadBabyBottleContent() d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> logEvent(LunaEvent(LunaEvent.TYPE_BABY_BOTTLE, numberPicker.value * 10)) localSettings.saveBabyBottleContent(numberPicker.value) } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } val alertDialog = d.create() alertDialog.show() } fun askWeightValue() { // Show number picker dialog val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.number_edit_dialog, null) d.setTitle(R.string.log_weight_dialog_title) d.setMessage(R.string.log_weight_dialog_description) d.setView(dialogView) val weightET = dialogView.findViewById(R.id.dialog_number_edittext) d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> val weight = weightET.text.toString().toIntOrNull() if (weight != null) logEvent(LunaEvent(LunaEvent.TYPE_WEIGHT, weight)) else Toast.makeText(this, R.string.toast_integer_error, Toast.LENGTH_SHORT).show() } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } val alertDialog = d.create() alertDialog.show() } fun askTemperatureValue() { // Show number picker dialog val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.temperature_dialog, null) d.setTitle(R.string.log_temperature_dialog_title) d.setMessage(R.string.log_temperature_dialog_description) d.setView(dialogView) val tempSlider = dialogView.findViewById(R.id.dialog_temperature_value) val range = NumericUtils(this).getValidEventQuantityRange(LunaEvent.TYPE_TEMPERATURE)!! tempSlider.valueFrom = range.first.toFloat() tempSlider.valueTo = range.second.toFloat() tempSlider.value = range.third.toFloat() val tempTextView = dialogView.findViewById(R.id.dialog_temperature_display) tempTextView.text = range.third.toString() tempSlider.addOnChangeListener({s, v, b -> tempTextView.text = v.toString()}) d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> val temperature = (tempSlider.value * 10).toInt() // In tenth of a grade logEvent(LunaEvent(LunaEvent.TYPE_TEMPERATURE, temperature)) } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } val alertDialog = d.create() alertDialog.show() } fun askPukeValue() { val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.puke_dialog, null) d.setTitle(R.string.log_puke_dialog_title) d.setMessage(R.string.log_puke_dialog_description) d.setView(dialogView) val spinner = dialogView.findViewById(R.id.dialog_puke_value) spinner.adapter = ArrayAdapter.createFromResource(this, R.array.AmountLabels, android.R.layout.simple_spinner_dropdown_item) spinner.setSelection(1) d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> val pos = spinner.selectedItemPosition logEvent(LunaEvent(LunaEvent.TYPE_PUKE, pos)) } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } val alertDialog = d.create() alertDialog.show() } fun askNotes(lunaEvent: LunaEvent) { val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.dialog_notes, null) d.setTitle(lunaEvent.getTypeDescription(this)) d.setMessage(lunaEvent.getDialogMessage(this)) d.setView(dialogView) val notesET = dialogView.findViewById(R.id.notes_edittext) val qtyET = dialogView.findViewById(R.id.notes_qty_edittext) if (lunaEvent.type == LunaEvent.TYPE_NOTE || lunaEvent.type == LunaEvent.TYPE_CUSTOM) qtyET.visibility = View.GONE d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> val qtyStr = qtyET.text.toString() if (qtyStr.isNotEmpty()) { val qty = qtyStr.toIntOrNull() if (qty == null) { Toast.makeText(this, R.string.toast_integer_error, Toast.LENGTH_SHORT).show() return@setPositiveButton } lunaEvent.quantity = qty } val notes = notesET.text.toString() lunaEvent.notes = notes logEvent(lunaEvent) } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } val alertDialog = d.create() alertDialog.show() } fun startBreastfeedingTimer(eventType: String) { // Check if timer already running if (bfTimerType != null) { Toast.makeText(this, R.string.breastfeeding_timer_already_running, Toast.LENGTH_SHORT).show() return } // Save timer state bfTimerStartTime = System.currentTimeMillis() bfTimerType = eventType saveBreastfeedingTimerState() // Show timer dialog showBreastfeedingTimerDialog(eventType) } fun showBreastfeedingTimerDialog(eventType: String) { val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.breastfeeding_timer_dialog, null) d.setTitle(R.string.breastfeeding_timer_title) d.setView(dialogView) d.setCancelable(false) val timerDisplay = dialogView.findViewById(R.id.breastfeeding_timer_display) val sideEmoji = dialogView.findViewById(R.id.breastfeeding_side_emoji) sideEmoji.text = LunaEvent(eventType).getTypeEmoji(this) // Set up timer updates bfTimerHandler = Handler(mainLooper) bfTimerRunnable = object : Runnable { override fun run() { val elapsed = (System.currentTimeMillis() - bfTimerStartTime) / 1000 val minutes = elapsed / 60 val seconds = elapsed % 60 timerDisplay.text = String.format("%02d:%02d", minutes, seconds) bfTimerHandler?.postDelayed(this, 1000) } } bfTimerHandler?.post(bfTimerRunnable!!) d.setPositiveButton(R.string.breastfeeding_timer_stop) { _, _ -> stopBreastfeedingTimer() } d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ -> cancelBreastfeedingTimer() dialogInterface.dismiss() } bfTimerDialog = d.create() bfTimerDialog?.show() } fun stopBreastfeedingTimer() { bfTimerHandler?.removeCallbacks(bfTimerRunnable!!) val durationMillis = System.currentTimeMillis() - bfTimerStartTime val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute val eventType = bfTimerType clearBreastfeedingTimerState() if (eventType != null) { logEvent(LunaEvent(eventType, durationMinutes)) } } fun cancelBreastfeedingTimer() { bfTimerHandler?.removeCallbacks(bfTimerRunnable!!) clearBreastfeedingTimerState() } fun askBreastfeedingDuration(eventType: String) { val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null) d.setTitle(R.string.breastfeeding_duration_title) d.setMessage(R.string.breastfeeding_duration_description) d.setView(dialogView) val numberPicker = dialogView.findViewById(R.id.breastfeeding_duration_picker) numberPicker.minValue = 1 numberPicker.maxValue = 60 numberPicker.value = 15 // Default 15 minutes numberPicker.wrapSelectorWheel = false d.setPositiveButton(android.R.string.ok) { _, _ -> logEvent(LunaEvent(eventType, numberPicker.value)) } d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ -> dialogInterface.dismiss() } d.create().show() } fun saveBreastfeedingTimerState() { LocalSettingsRepository(this).saveBreastfeedingTimer(bfTimerStartTime, bfTimerType ?: "") } fun clearBreastfeedingTimerState() { bfTimerStartTime = 0 bfTimerType = null bfTimerDialog = null LocalSettingsRepository(this).clearBreastfeedingTimer() } fun restoreBreastfeedingTimerIfNeeded() { val timerState = LocalSettingsRepository(this).loadBreastfeedingTimer() if (timerState != null && timerState.first > 0 && timerState.second.isNotEmpty()) { bfTimerStartTime = timerState.first bfTimerType = timerState.second showBreastfeedingTimerDialog(timerState.second) } } // Sleep timer methods fun startSleepTimer() { // Check if timer already running if (sleepTimerStartTime > 0) { Toast.makeText(this, R.string.sleep_timer_already_running, Toast.LENGTH_SHORT).show() return } // Save timer state sleepTimerStartTime = System.currentTimeMillis() saveSleepTimerState() // Show timer dialog showSleepTimerDialog() } fun showSleepTimerDialog() { val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.sleep_timer_dialog, null) d.setTitle(R.string.sleep_timer_title) d.setView(dialogView) d.setCancelable(false) val timerDisplay = dialogView.findViewById(R.id.sleep_timer_display) // Set up timer updates sleepTimerHandler = Handler(mainLooper) sleepTimerRunnable = object : Runnable { override fun run() { val elapsed = (System.currentTimeMillis() - sleepTimerStartTime) / 1000 val hours = elapsed / 3600 val minutes = (elapsed % 3600) / 60 val seconds = elapsed % 60 timerDisplay.text = if (hours > 0) { String.format("%d:%02d:%02d", hours, minutes, seconds) } else { String.format("%02d:%02d", minutes, seconds) } sleepTimerHandler?.postDelayed(this, 1000) } } sleepTimerHandler?.post(sleepTimerRunnable!!) d.setPositiveButton(R.string.sleep_timer_stop) { _, _ -> stopSleepTimer() } d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ -> cancelSleepTimer() dialogInterface.dismiss() } sleepTimerDialog = d.create() sleepTimerDialog?.show() } fun stopSleepTimer() { sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!) val durationMillis = System.currentTimeMillis() - sleepTimerStartTime val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute clearSleepTimerState() logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, durationMinutes)) } fun cancelSleepTimer() { sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!) clearSleepTimerState() } fun askSleepDuration() { val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.sleep_duration_dialog, null) d.setTitle(R.string.sleep_duration_title) d.setMessage(R.string.sleep_duration_description) d.setView(dialogView) val numberPicker = dialogView.findViewById(R.id.sleep_duration_picker) numberPicker.minValue = 1 numberPicker.maxValue = 180 // Up to 3 hours numberPicker.value = 30 // Default 30 minutes numberPicker.wrapSelectorWheel = false d.setPositiveButton(android.R.string.ok) { _, _ -> logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, numberPicker.value)) } d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ -> dialogInterface.dismiss() } d.create().show() } fun saveSleepTimerState() { LocalSettingsRepository(this).saveSleepTimer(sleepTimerStartTime) } fun clearSleepTimerState() { sleepTimerStartTime = 0 sleepTimerDialog = null LocalSettingsRepository(this).clearSleepTimer() } fun restoreSleepTimerIfNeeded() { val startTime = LocalSettingsRepository(this).loadSleepTimer() if (startTime > 0) { sleepTimerStartTime = startTime showSleepTimerDialog() } } fun askToTrimLogbook() { val d = AlertDialog.Builder(this) d.setTitle(R.string.trim_logbook_dialog_title) d.setMessage( when (LocalSettingsRepository(this).loadDataRepository()) { LocalSettingsRepository.DATA_REPO.WEBDAV -> R.string.trim_logbook_dialog_message_dav else -> R.string.trim_logbook_dialog_message_local } ) d.setPositiveButton(R.string.trim_logbook_dialog_button_ok) { dialogInterface, i -> logbook?.trim() saveLogbook() } d.setNegativeButton(R.string.trim_logbook_dialog_button_cancel) { dialogInterface, i -> dialogInterface.dismiss() } val alertDialog = d.create() alertDialog.show() } fun getPreviousSameEvent(event: LunaEvent, items: ArrayList): LunaEvent? { var previousEvent: LunaEvent? = null for (item in items) { if (item.type == event.type && item.time < event.time) { if (previousEvent == null) { previousEvent = item } else if (previousEvent.time < item.time) { previousEvent = item } } } return previousEvent } fun getNextSameEvent(event: LunaEvent, items: ArrayList): LunaEvent? { var nextEvent: LunaEvent? = null for (item in items) { if (item.type == event.type && item.time > event.time) { if (nextEvent == null) { nextEvent = item } else if (nextEvent.time > item.time) { nextEvent = item } } } return nextEvent } fun showEventDetailDialog(event: LunaEvent, items: ArrayList) { // Do not update list while the detail is shown, to avoid changing the object below while it is changed by the user pauseLogbookUpdate = true val d = AlertDialog.Builder(this) d.setTitle(R.string.dialog_event_detail_title) val dialogView = layoutInflater.inflate(R.layout.dialog_event_detail, null) dialogView.findViewById(R.id.dialog_event_detail_type_emoji).text = event.getTypeEmoji(this) dialogView.findViewById(R.id.dialog_event_detail_type_description).text = event.getTypeDescription(this) dialogView.findViewById(R.id.dialog_event_detail_type_quantity).text = NumericUtils(this).formatEventQuantity(event) dialogView.findViewById(R.id.dialog_event_detail_type_notes).text = event.notes val currentDateTime = Calendar.getInstance() currentDateTime.time = Date(event.time * 1000) val dateTextView = dialogView.findViewById(R.id.dialog_event_detail_type_date) dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), DateUtils.formatDateTime(event.time)) dateTextView.setOnClickListener { // Show datetime picker val startYear = currentDateTime.get(Calendar.YEAR) val startMonth = currentDateTime.get(Calendar.MONTH) val startDay = currentDateTime.get(Calendar.DAY_OF_MONTH) val startHour = currentDateTime.get(Calendar.HOUR_OF_DAY) val startMinute = currentDateTime.get(Calendar.MINUTE) DatePickerDialog(this, { _, year, month, day -> TimePickerDialog(this, { _, hour, minute -> val pickedDateTime = Calendar.getInstance() pickedDateTime.set(year, month, day, hour, minute) // Save event and move it to the right position in the logbook event.time = pickedDateTime.time.time / 1000 // Seconds since epoch dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), DateUtils.formatDateTime(event.time)) logbook?.sort() recyclerView.adapter?.notifyDataSetChanged() saveLogbook() }, startHour, startMinute, android.text.format.DateFormat.is24HourFormat(this@MainActivity)).show() }, startYear, startMonth, startDay).show() } // Make quantity editable for breastfeeding and sleep events val quantityTextView = dialogView.findViewById(R.id.dialog_event_detail_type_quantity) val isBreastfeeding = event.type in listOf( LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE, LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE, LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE ) val isSleep = event.type == LunaEvent.TYPE_SLEEP if ((isBreastfeeding || isSleep) && event.quantity > 0) { quantityTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_edit, 0) quantityTextView.compoundDrawableTintList = ColorStateList.valueOf(getColor(R.color.accent)) quantityTextView.setOnClickListener { val pickerDialog = AlertDialog.Builder(this@MainActivity) val pickerView = if (isSleep) { layoutInflater.inflate(R.layout.sleep_duration_dialog, null) } else { layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null) } val picker = pickerView.findViewById( if (isSleep) R.id.sleep_duration_picker else R.id.breastfeeding_duration_picker ) picker.minValue = 1 picker.maxValue = if (isSleep) 180 else 60 picker.value = if (event.quantity > 0) Math.min(event.quantity, picker.maxValue) else if (isSleep) 30 else 15 pickerDialog.setTitle(if (isSleep) R.string.sleep_duration_title else R.string.breastfeeding_duration_title) pickerDialog.setView(pickerView) pickerDialog.setPositiveButton(android.R.string.ok) { _, _ -> event.quantity = picker.value quantityTextView.text = NumericUtils(this@MainActivity).formatEventQuantity(event) recyclerView.adapter?.notifyDataSetChanged() saveLogbook() } pickerDialog.setNegativeButton(android.R.string.cancel, null) pickerDialog.show() } } d.setView(dialogView) d.setPositiveButton(R.string.dialog_event_detail_close_button) { dialogInterface, i -> dialogInterface.dismiss() } d.setNeutralButton(R.string.dialog_event_detail_delete_button) { dialogInterface, i -> deleteEvent(event) } val alertDialog = d.create() alertDialog.show() alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL).setTextColor(ContextCompat.getColor(this, R.color.danger)) alertDialog.setOnDismissListener({ // Resume logbook update pauseLogbookUpdate = false }) // show optional signature if (event.signature.isNotEmpty()) { val signatureTextEdit = dialogView.findViewById(R.id.dialog_event_detail_type_signature) signatureTextEdit.text = String.format(getString(R.string.dialog_event_detail_signature), event.signature) signatureTextEdit.visibility = View.VISIBLE } // create next/previous links to events of the same type val previousTextView = dialogView.findViewById(R.id.dialog_event_previous) val nextTextView = dialogView.findViewById(R.id.dialog_event_next) val nextEvent = getNextSameEvent(event, items) val previousEvent = getPreviousSameEvent(event, items) if (previousEvent != null) { val emoji = previousEvent.getTypeEmoji(applicationContext) val time = DateUtils.formatTimeDuration(applicationContext, event.time - previousEvent.time) previousTextView.text = String.format("⬅️ %s %s", emoji, time) previousTextView.setOnClickListener { alertDialog.cancel() showEventDetailDialog(previousEvent, items) } } else { previousTextView.visibility = View.GONE } if (nextEvent != null) { val emoji = nextEvent.getTypeEmoji(applicationContext) val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.time - event.time) nextTextView.text = String.format("%s %s ➡️", time, emoji) nextTextView.setOnClickListener { alertDialog.cancel() showEventDetailDialog(nextEvent, items) } } else { nextTextView.visibility = View.GONE } } fun showAddLogbookDialog(requestedByUser: Boolean) { val d = AlertDialog.Builder(this) d.setTitle(R.string.dialog_add_logbook_title) val dialogView = layoutInflater.inflate(R.layout.dialog_add_logbook, null) dialogView.findViewById(R.id.dialog_add_logbook_message).text = getString( if (requestedByUser) R.string.dialog_add_logbook_message else R.string.dialog_add_logbook_message_intro ) val logbookNameEditText = dialogView.findViewById(R.id.dialog_add_logbook_logbookname) d.setView(dialogView) d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> addLogbook(logbookNameEditText.text.toString()) } if (requestedByUser) { d.setCancelable(true) d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } } else { d.setCancelable(false) } val alertDialog = d.create() alertDialog.show() } fun loadLogbookList() { setLoading(true) logbookRepo?.listLogbooks(this, object: LogbookListObtainedListener { override fun onLogbookListObtained(logbooksNames: ArrayList) { runOnUiThread({ if (logbooksNames.isEmpty()) { // First run, no logbook: create one showAddLogbookDialog(false) return@runOnUiThread } // Show logbooks dropdown val spinner = findViewById(R.id.logbooks_spinner) val sAdapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item) sAdapter.setDropDownViewResource(R.layout.row_logbook_spinner) for (ln in logbooksNames) { sAdapter.add( ln.ifEmpty { getString(R.string.default_logbook_name) } ) } spinner.adapter = sAdapter spinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener { override fun onItemSelected( parent: AdapterView<*>?, view: View?, position: Int, id: Long ) { // Changed logbook: empty list setListAdapter(arrayListOf()) // Load logbook loadLogbook(logbooksNames.get(position)) } override fun onNothingSelected(parent: AdapterView<*>?) {} } }) } override fun onIOError(error: IOException) { Log.e(TAG, "Unable to load logbooks list (IOError): $error") runOnUiThread({ setLoading(false) onRepoError(getString(R.string.settings_network_error) + error.toString()) }) } override fun onWebDAVError(error: SardineException) { Log.e(TAG, "Unable to load logbooks list (SardineException): $error") runOnUiThread({ setLoading(false) onRepoError( if(error.toString().contains("401")) { getString(R.string.settings_webdav_error_denied) } else if(error.toString().contains("503")) { getString(R.string.settings_webdav_error_server_offline) } else { getString(R.string.settings_webdav_error_generic) + error.toString() } ) }) } override fun onError(error: Exception) { Log.e(TAG, "Unable to load logbooks list: $error") runOnUiThread({ setLoading(false) onRepoError(getString(R.string.settings_generic_error) + error.toString()) }) } }) } fun addLogbook(logbookName: String) { val newLogbook = Logbook(logbookName) setLoading(true) logbookRepo?.saveLogbook(this, newLogbook, object: LogbookSavedListener{ override fun onLogbookSaved() { Log.d(TAG, "Logbook $logbookName created") runOnUiThread({ setLoading(false) loadLogbookList() Toast.makeText(this@MainActivity, getString(R.string.logbook_created) + logbookName, Toast.LENGTH_SHORT).show() }) } override fun onIOError(error: IOException) { runOnUiThread({ onRepoError(getString(R.string.settings_network_error) + error.toString()) }) } override fun onWebDAVError(error: SardineException) { runOnUiThread({ onRepoError( if(error.toString().contains("401")) { getString(R.string.settings_webdav_error_denied) } else if(error.toString().contains("503")) { getString(R.string.settings_webdav_error_server_offline) } else { getString(R.string.settings_webdav_error_generic) + error.toString() } ) }) } override fun onJSONError(error: JSONException) { runOnUiThread({ onRepoError(getString(R.string.settings_json_error) + error.toString()) }) } override fun onError(error: Exception) { runOnUiThread({ onRepoError(getString(R.string.settings_generic_error) + error.toString()) }) } }) } fun loadLogbook(name: String) { if (savingEvent) return // Reset time counter handler.removeCallbacks(updateListRunnable) handler.postDelayed(updateListRunnable, UPDATE_EVERY_SECS*1000) // Load data setLoading(true) logbookRepo?.loadLogbook(this, name, object: LogbookLoadedListener{ override fun onLogbookLoaded(lb: Logbook) { runOnUiThread({ setLoading(false) findViewById(R.id.no_connection_screen).visibility = View.GONE logbook = lb showLogbook() if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) { for (e in logbook?.logs ?: listOf()) { val em = e.getTypeEmoji(this@MainActivity) if (em == getString(R.string.event_unknown_type)) { Log.e(TAG, "UNKNOWN: ${e.type}") } } } }) } override fun onIOError(error: IOException) { runOnUiThread({ setLoading(false) onRepoError(getString(R.string.settings_network_error) + error.toString()) }) } override fun onWebDAVError(error: SardineException) { runOnUiThread({ setLoading(false) onRepoError( if(error.toString().contains("401")) { getString(R.string.settings_webdav_error_denied) } else if(error.toString().contains("503")) { getString(R.string.settings_webdav_error_server_offline) } else { getString(R.string.settings_webdav_error_generic) + error.toString() } ) }) } override fun onJSONError(error: JSONException) { runOnUiThread({ setLoading(false) onRepoError(getString(R.string.settings_json_error) + error.toString()) }) } override fun onError(error: Exception) { runOnUiThread({ setLoading(false) onRepoError(getString(R.string.settings_generic_error) + error.toString()) }) } }) } fun onRepoError(message: String){ runOnUiThread({ setLoading(false) findViewById(R.id.no_connection_screen).visibility = View.VISIBLE findViewById(R.id.no_connection_screen_message).text = message }) } fun logEvent(event: LunaEvent) { savingEvent(true) event.signature = signature setLoading(true) logbook?.logs?.add(0, event) recyclerView.adapter?.notifyItemInserted(0) recyclerView.smoothScrollToPosition(0) saveLogbook(event) // Check logbook size to avoid OOM errors if (logbook?.isTooBig() == true) { askToTrimLogbook() } } fun deleteEvent(event: LunaEvent) { // Update view savingEvent(true) // Update data setLoading(true) logbook?.logs?.remove(event) recyclerView.adapter?.notifyDataSetChanged() saveLogbook() } /** * Saves the logbook. If saving while adding an event, please specify the event so in case * of error can be removed from the list. */ fun saveLogbook(lastEventAdded: LunaEvent? = null) { if (logbook == null) { Log.e(TAG, "Trying to save logbook, but logbook is null!") return } logbookRepo?.saveLogbook(this, logbook!!, object: LogbookSavedListener{ override fun onLogbookSaved() { Log.d(TAG, "Logbook saved") runOnUiThread({ setLoading(false) Toast.makeText( this@MainActivity, if (lastEventAdded != null) R.string.toast_event_added else R.string.toast_logbook_saved, Toast.LENGTH_SHORT ).show() savingEvent(false) }) } override fun onIOError(error: IOException) { runOnUiThread({ setLoading(false) onRepoError(getString(R.string.settings_network_error) + error.toString()) if (lastEventAdded != null) onAddError(lastEventAdded, error.toString()) }) } override fun onWebDAVError(error: SardineException) { runOnUiThread({ setLoading(false) onRepoError( if(error.toString().contains("401")) { getString(R.string.settings_webdav_error_denied) } else if(error.toString().contains("503")) { getString(R.string.settings_webdav_error_server_offline) } else { getString(R.string.settings_webdav_error_generic) + error.toString() } ) if (lastEventAdded != null) onAddError(lastEventAdded, error.toString()) }) } override fun onJSONError(error: JSONException) { runOnUiThread({ setLoading(false) onRepoError(getString(R.string.settings_json_error) + error.toString()) if (lastEventAdded != null) onAddError(lastEventAdded, error.toString()) }) } override fun onError(error: Exception) { runOnUiThread({ setLoading(false) onRepoError(getString(R.string.settings_generic_error) + error.toString()) if (lastEventAdded != null) onAddError(lastEventAdded, error.toString()) }) } }) } private fun onAddError(event: LunaEvent, error: String) { Log.e(TAG, "Logbook was NOT saved! $error") runOnUiThread({ setLoading(false) Toast.makeText(this@MainActivity, R.string.toast_event_add_error, Toast.LENGTH_SHORT).show() recyclerView.adapter?.notifyDataSetChanged() savingEvent(false) }) } private fun setLoading(loading: Boolean) { if (loading) { progressIndicator.visibility = View.VISIBLE } else { progressIndicator.visibility = View.INVISIBLE } } private fun savingEvent(saving: Boolean) { if (saving) { savingEvent = true buttonsContainer.alpha = 0.2f } else { savingEvent = false buttonsContainer.alpha = 1f } } private fun showOverflowPopupWindow(anchor: View) { if (showingOverflowPopupWindow) return PopupWindow(anchor.context).apply { isOutsideTouchable = true val inflater = LayoutInflater.from(anchor.context) contentView = inflater.inflate(R.layout.more_events_popup, null) contentView.findViewById(R.id.button_sleep).setOnClickListener { startSleepTimer() dismiss() } contentView.findViewById(R.id.button_sleep).setOnLongClickListener { askSleepDuration() dismiss() true } contentView.findViewById(R.id.button_medicine).setOnClickListener { askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE)) dismiss() } contentView.findViewById(R.id.button_enema).setOnClickListener({ logEvent(LunaEvent(LunaEvent.TYPE_ENEMA)) dismiss() }) contentView.findViewById(R.id.button_note).setOnClickListener({ askNotes(LunaEvent(LunaEvent.TYPE_NOTE)) dismiss() }) contentView.findViewById(R.id.button_temperature).setOnClickListener({ askTemperatureValue() dismiss() }) contentView.findViewById(R.id.button_puke).setOnClickListener({ askPukeValue() dismiss() }) contentView.findViewById(R.id.button_colic).setOnClickListener({ logEvent( LunaEvent(LunaEvent.TYPE_COLIC) ) dismiss() }) contentView.findViewById(R.id.button_scale).setOnClickListener({ askWeightValue() dismiss() }) contentView.findViewById(R.id.button_bath).setOnClickListener({ logEvent( LunaEvent(LunaEvent.TYPE_BATH) ) dismiss() }) }.also { popupWindow -> popupWindow.setOnDismissListener({ Handler(mainLooper).postDelayed({ showingOverflowPopupWindow = false }, 500) }) popupWindow.showAsDropDown(anchor) showingOverflowPopupWindow = true } } }