package it.danieleverducci.lunatracker import android.app.DatePickerDialog import android.app.TimePickerDialog import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.os.Handler import android.text.Editable 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.Button import android.widget.EditText import android.widget.LinearLayout 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.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 // list of all events var allEvents = arrayListOf() var currentPopupItems = listOf() } 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 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)) populateHeaderMenu() // Set listeners findViewById(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog(true) } 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_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 { if (logbook != null) { loadLogbook(logbook!!.name) } else { loadLogbookList() } } } private fun setListAdapter(items: ArrayList) { val adapter = LunaEventRecyclerAdapter(this, items) adapter.onItemClickListener = object : LunaEventRecyclerAdapter.OnItemClickListener { override fun onItemClick(event: LunaEvent) { showEventDetailDialog(event) } } recyclerView.adapter = adapter } fun showSettings() { val i = Intent(this, SettingsActivity::class.java) startActivity(i) } fun showLogbook() { // Show logbook if (logbook == null) Log.w(TAG, "showLogbook(): logbook is null!") allEvents = logbook?.logs ?: arrayListOf() setListAdapter(allEvents) populateHeaderMenu() } private fun populateHeaderMenu() { val settingsRepository = LocalSettingsRepository(this) val dynamicMenu = settingsRepository.loadDynamicMenu() val eventTypeStats = mutableMapOf() if (dynamicMenu) { val sampleSize = 100 // populate frequency map from first 100 events allEvents.take(sampleSize.coerceAtMost(allEvents.size)).forEach { eventTypeStats[it.type] = 1 + (eventTypeStats[it.type] ?: 0) } } // sort all event types by frequency or ordinal val eventTypesSorted = LunaEvent.Type.entries.toList().sortedWith( compareBy({ -1 * (eventTypeStats[it] ?: 0) }, { it.ordinal }) ).filter { it != LunaEvent.Type.UNKNOWN } fun setupMenu(maxButtonCount: Int, sortedEventTypes: List): Int { val row1 = findViewById(R.id.linear_layout_row1) val row1Button1 = findViewById(R.id.button1_row1) val row1Button2 = findViewById(R.id.button2_row1) val row2 = findViewById(R.id.linear_layout_row2) val row2Button1 = findViewById(R.id.button1_row2) val row2Button2 = findViewById(R.id.button2_row2) val row2Button3 = findViewById(R.id.button3_row2) val row3 = findViewById(R.id.linear_layout_row3) val row3Button1 = findViewById(R.id.button1_row3) val row3Button2 = findViewById(R.id.button2_row3) // hide all rows/buttons (except row 3) for (view in listOf(row1, row1Button1, row1Button2, row2, row2Button1, row2Button2, row2Button3, row3, row3Button1, row3Button2)) { view.visibility = View.GONE } row3.visibility = View.VISIBLE var showCounter = 0 fun show(vararg tvs: TextView) { for (tv in tvs) { val type = sortedEventTypes[showCounter] tv.text = LunaEvent.getHeaderEmoji(applicationContext, type) tv.setOnClickListener { showCreateDialog(type) } tv.visibility = View.VISIBLE // show parent row (tv.parent as View).visibility = View.VISIBLE showCounter += 1 } } when (maxButtonCount) { 0 -> { } // ignore - show empty row3 1 -> show(row3Button1) 2 -> show(row3Button1, row3Button2) 3 -> show(row1Button1, row3Button1) 4, 5, 6 -> show(row1Button1, row1Button2, row3Button1, row3Button2) else -> show(row1Button1, row1Button2, row2Button1, row2Button2, row2Button3, row3Button1, row3Button2) } return showCounter } val usedEventCount = eventTypeStats.count { it.value > 0 } val maxButtonCount = if (dynamicMenu) { usedEventCount } else { 7 } val eventsShown = setupMenu(maxButtonCount, eventTypesSorted) // store left over events for popup menu currentPopupItems = eventTypesSorted.subList(eventsShown, eventTypesSorted.size) } 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() // Update list dates recyclerView.adapter?.notifyDataSetChanged() 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) super.onStop() } fun addBabyBottleEvent(event: LunaEvent) { setToPreviousQuantity(event) askBabyBottleContent(event, true) { saveEvent(event) } } fun askBabyBottleContent(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) { val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_bottle, null) d.setTitle(event.getDialogTitle(this)) d.setMessage(event.getDialogMessage(this)) d.setView(dialogView) val numberPicker = dialogView.findViewById(R.id.dialog_number_picker) numberPicker.minValue = 1 // "10" numberPicker.maxValue = 34 // "340 numberPicker.displayedValues = ((10..340 step 10).map { it.toString() }.toTypedArray()) numberPicker.wrapSelectorWheel = false numberPicker.value = event.quantity / 10 val dateTV = dialogView.findViewById(R.id.dialog_date_picker) val pickedTime = datePickerHelper(event.time, dateTV) if (!showTime) { dateTV.visibility = View.GONE } d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> event.time = pickedTime.time.time / 1000 event.quantity = numberPicker.value * 10 onPositive() dialogInterface.dismiss() } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } val alertDialog = d.create() alertDialog.show() } fun addWeightEvent(event: LunaEvent) { setToPreviousQuantity(event) askWeightValue(event, true) { saveEvent(event) } } fun askWeightValue(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) { // Show number picker dialog val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_weight, null) d.setTitle(event.getDialogTitle(this)) d.setMessage(event.getDialogMessage(this)) d.setView(dialogView) val weightET = dialogView.findViewById(R.id.dialog_number_edittext) weightET.setText(event.quantity.toString()) val dateTV = dialogView.findViewById(R.id.dialog_date_picker) val pickedTime = datePickerHelper(event.time, dateTV) if (!showTime) { dateTV.visibility = View.GONE } d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> val weight = weightET.text.toString().toIntOrNull() if (weight != null) { event.time = pickedTime.time.time / 1000 event.quantity = weight onPositive() } else { Toast.makeText(this, R.string.toast_integer_error, Toast.LENGTH_SHORT).show() } dialogInterface.dismiss() } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } val alertDialog = d.create() alertDialog.show() } fun addTemperatureEvent(event: LunaEvent) { setToPreviousQuantity(event) askTemperatureValue(event, true) { saveEvent(event) } } fun askTemperatureValue(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) { // Show number picker dialog val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_temperature, null) d.setTitle(event.getDialogTitle(this)) d.setMessage(event.getDialogMessage(this)) 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 = if (event.quantity == 0) { range.third.toFloat() // default } else { event.quantity.toFloat() / 10 } val dateTV = dialogView.findViewById(R.id.dialog_date_picker) val pickedTime = datePickerHelper(event.time, dateTV) if (!showTime) { dateTV.visibility = View.GONE } val tempTextView = dialogView.findViewById(R.id.dialog_temperature_display) tempTextView.text = tempSlider.value.toString() tempSlider.addOnChangeListener({ s, v, b -> tempTextView.text = v.toString() }) d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> event.time = pickedTime.time.time / 1000 event.quantity = (tempSlider.value * 10).toInt() // temperature in tenth of a grade onPositive() dialogInterface.dismiss() } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } val alertDialog = d.create() alertDialog.show() } fun datePickerHelper(time: Long, dateTextView: TextView, onChange: (Long) -> Unit = {}): Calendar { dateTextView.text = DateUtils.formatDateTime(time) val dateTime = Calendar.getInstance() dateTime.time = Date(time * 1000) dateTextView.setOnClickListener { // Show datetime picker val startYear = dateTime.get(Calendar.YEAR) val startMonth = dateTime.get(Calendar.MONTH) val startDay = dateTime.get(Calendar.DAY_OF_MONTH) val startHour = dateTime.get(Calendar.HOUR_OF_DAY) val startMinute = dateTime.get(Calendar.MINUTE) DatePickerDialog(this, { _, year, month, day -> TimePickerDialog( this, { _, hour, minute -> dateTime.set(year, month, day, hour, minute) dateTextView.text = DateUtils.formatDateTime(dateTime.time.time / 1000) onChange.invoke(dateTime.time.time / 1000) }, startHour, startMinute, android.text.format.DateFormat.is24HourFormat(this@MainActivity) ).show() }, startYear, startMonth, startDay).show() } return dateTime } fun saveEvent(event: LunaEvent) { if (!allEvents.contains(event)) { // new event logEvent(event) } logbook?.sort() recyclerView.adapter?.notifyDataSetChanged() saveLogbook() } fun addSleepEvent(event: LunaEvent) { askSleepValue(event) { saveEvent(event) } } fun askSleepValue(event: LunaEvent, onPositive: () -> Unit) { val d = AlertDialog.Builder(this) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_duration, null) d.setTitle(event.getDialogTitle(this)) d.setMessage(event.getDialogMessage(this)) d.setView(dialogView) val durationTextView = dialogView.findViewById(R.id.dialog_date_duration) val durationNowButton = dialogView.findViewById