package de.twomartens.troubleshooting import android.os.Bundle import android.view.View import android.widget.ArrayAdapter import android.widget.Spinner import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.udevel.widgetlab.TypingIndicatorView import de.twomartens.troubleshooting.databinding.ActivityMessageListBinding import kotlinx.coroutines.delay import kotlinx.coroutines.launch class MessageListActivity : AppCompatActivity() { private lateinit var recyclerView: RecyclerView private lateinit var binding: ActivityMessageListBinding private val viewAdapter: MessageListAdapter = MessageListAdapter() private lateinit var spinner: Spinner private lateinit var horizontalLayout: ConstraintLayout private lateinit var typingIndicator: TypingIndicatorView private lateinit var model: MessageListViewModel private lateinit var adapter: ArrayAdapter private var showSpinner = true override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMessageListBinding.inflate(layoutInflater) setContentView(binding.root) initVariables() adapter = initSpinner(model, spinner) bindMessageObserver(viewAdapter, model) initButton(::step, spinner) val messages = initFirstChoice(adapter, model, horizontalLayout) receiveMessages(messages, model, typingIndicator, recyclerView, horizontalLayout) } private fun initVariables() { val viewManager = LinearLayoutManager(this) viewManager.stackFromEnd = true viewManager.reverseLayout = false model = ViewModelProvider(this)[MessageListViewModel::class.java] horizontalLayout = binding.layoutChatbox typingIndicator = binding.indicator typingIndicator.visibility = View.GONE horizontalLayout.visibility = View.GONE spinner = binding.spinner recyclerView = binding.messageList.apply { // use this setting to improve performance if you know that changes // in content do not change the layout size of the RecyclerView setHasFixedSize(true) // use a linear layout manager layoutManager = viewManager // specify an viewAdapter (see also next example) adapter = viewAdapter } } private fun initSpinner(model: MessageListViewModel, spinner: Spinner): ArrayAdapter { val spinnerOptions: ArrayList? = model.spinnerOptions.value if (spinnerOptions != null) { val arrayAdapter = ArrayAdapter(this, R.layout.spinner_item, spinnerOptions) // Specify the layout to use when the list of choices appears arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) arrayAdapter.setNotifyOnChange(true) // Apply the adapter to the spinner spinner.adapter = arrayAdapter return arrayAdapter } return ArrayAdapter(this, android.R.layout.simple_spinner_item, ArrayList()) } private fun bindMessageObserver(viewAdapter: MessageListAdapter, model: MessageListViewModel) { val messageObserver = Observer> { messages -> viewAdapter.replaceMessages(messages) } model.getMessages().observe(this, messageObserver) } private fun initButton(step: (SpinnerOption) -> Unit, spinner: Spinner) { binding.buttonSend.setOnClickListener { val option = spinner.selectedItem if (option is SpinnerOption) { step(option) } } } private fun initFirstChoice(adapter: ArrayAdapter, model: MessageListViewModel, horizontalLayout: ConstraintLayout): List { val isInitialized = model.initialized.value return if (isInitialized != null && !isInitialized) { model.spinnerOptions.value?.addAll(SpinnerOption.getOptions( resources.getStringArray(R.array.components_values), resources.getStringArray(R.array.components), OptionType.COMPONENT)) adapter.notifyDataSetChanged() model.initialized.value = true listOf(Message(getString(R.string.greeting), false, System.currentTimeMillis())) } else { val spinnerVisible = model.spinnerVisible.value if (spinnerVisible != null && spinnerVisible) horizontalLayout.visibility = View.VISIBLE listOf() } } private fun step(selectedOption: SpinnerOption) { sendMessage(selectedOption, model, recyclerView, horizontalLayout) val messages = branch( selectedOption, model, adapter, ::componentSelected, ::problemSelected, ::didItWorkQuestion, ::pleaseTry, ::problemSolved, ::nextSolutionQuestion, ::doNotShowSpinner) receiveMessages(messages, model, typingIndicator, recyclerView, horizontalLayout, showSpinner) } private fun sendMessage(selectedOption: SpinnerOption, model: MessageListViewModel, recyclerView: RecyclerView, horizontalLayout: ConstraintLayout) { val message = Message(selectedOption.toString(), true, System.currentTimeMillis()) model.add(message) recyclerView.scrollToPosition(model.size() - 1) horizontalLayout.visibility = View.GONE model.spinnerVisible.value = false } private fun receiveMessages(messages: List, model: MessageListViewModel, typingIndicator: View, recyclerView: RecyclerView, horizontalLayout: ConstraintLayout, showSpinner: Boolean = true) { model.uiScope.launch { for (message in messages) { typingIndicator.visibility = View.VISIBLE delay(1500) typingIndicator.visibility = View.GONE model.add(message) recyclerView.scrollToPosition(model.size() - 1) } if (messages.isNotEmpty() && showSpinner) { horizontalLayout.visibility = View.VISIBLE model.spinnerVisible.value = true } } } private fun doNotShowSpinner() { showSpinner = false } private fun branch(selectedOption: SpinnerOption, model: MessageListViewModel, adapter: ArrayAdapter, componentSelected: (MessageListViewModel, ArrayAdapter) -> List, problemSelected: (MessageListViewModel, ArrayAdapter) -> List, didItWorkQuestion: (MessageListViewModel, ArrayAdapter) -> List, pleaseTry: () -> List, problemSolved: (MessageListViewModel, ArrayAdapter) -> List, nextSolutionQuestion: (MessageListViewModel, ArrayAdapter) -> List, doNotShowSpinner: () -> Unit): List { return when (selectedOption.type) { OptionType.COMPONENT -> { model.component.value = Component.valueOf(selectedOption.id) componentSelected(model, adapter) } OptionType.PROBLEM -> { model.problem.value = Problem.valueOf(selectedOption.id) problemSelected(model, adapter) } OptionType.BINARY_HAVE_YOU_TRIED -> { when (selectedOption.id) { "YES" -> didItWorkQuestion(model, adapter) else -> pleaseTry() } } OptionType.BINARY_DID_IT_WORK -> { when (selectedOption.id) { "YES" -> problemSolved(model, adapter) else -> nextSolutionQuestion(model, adapter) } } OptionType.STORY -> { doNotShowSpinner() if (selectedOption.id == "WHO_ARE_YOU") { listOf( Message( getString(R.string.gardner_second), false, System.currentTimeMillis() ) ) } else { listOf( Message(getString(R.string.okay), false, System.currentTimeMillis()), Message(getString(R.string.im_gardner), false, System.currentTimeMillis()), Message(getString(R.string.okay_saves_time), false, System.currentTimeMillis()), Message(getString(R.string.its_funny), false, System.currentTimeMillis())) } } } } private fun componentSelected(model: MessageListViewModel, adapter: ArrayAdapter): List { model.spinnerOptions.value?.clear() model.spinnerOptions.value?.addAll(SpinnerOption.getOptions( resources.getStringArray(resources.getIdentifier( model.component.value?.name.plus("_problems_values"), "array", packageName)), resources.getStringArray(resources.getIdentifier( model.component.value?.name.plus("_problems"), "array", packageName)), OptionType.PROBLEM)) adapter.notifyDataSetChanged() return listOf( Message(getString(R.string.okay), false, System.currentTimeMillis()), Message(getString(R.string.select_problem), false, System.currentTimeMillis()) ) } private fun problemSelected(model: MessageListViewModel, adapter: ArrayAdapter): List { model.solutions.value?.addAll(resources.getStringArray(resources.getIdentifier( model.component.value?.name.plus("_").plus(model.problem.value?.name), "array", packageName ))) val firstSolutionQuestion = getString(R.string.have_you_tried, model.solutions.value?. let { it[0] }) model.nextSolutionIndex.value = (model.nextSolutionIndex.value ?: 0) + 1 model.spinnerOptions.value?.clear() model.spinnerOptions.value?.addAll(SpinnerOption.getOptions( resources.getStringArray(R.array.binary_values), resources.getStringArray(R.array.binary), OptionType.BINARY_HAVE_YOU_TRIED)) adapter.notifyDataSetChanged() return listOf( Message(getString(R.string.okay), false, System.currentTimeMillis()), Message(firstSolutionQuestion, false, System.currentTimeMillis()) ) } private fun didItWorkQuestion(model: MessageListViewModel, adapter: ArrayAdapter): List { model.currentSolutionState.value = State.DID_IT_WORK model.spinnerOptions.value?.clear() model.spinnerOptions.value?.addAll(SpinnerOption.getOptions( resources.getStringArray(R.array.binary_values), resources.getStringArray(R.array.binary), OptionType.BINARY_DID_IT_WORK)) adapter.notifyDataSetChanged() return listOf( Message(getString(R.string.okay), false, System.currentTimeMillis()), Message(getString(R.string.did_it_work), false, System.currentTimeMillis()) ) } private fun pleaseTry(): List { return listOf( Message(getString(R.string.okay), false, System.currentTimeMillis()), Message(getString(R.string.please_try), false, System.currentTimeMillis()), Message(getString(R.string.i_wait), false, System.currentTimeMillis()), Message(getString(R.string.have_you_tried, model.solutions.value?.let { it[(model.nextSolutionIndex.value ?: 1) - 1]}), false, System.currentTimeMillis()) ) } private fun problemSolved(model: MessageListViewModel, adapter: ArrayAdapter): List { model.spinnerOptions.value?.clear() model.spinnerOptions.value?.add(SpinnerOption( "SAY_SOMETHING_DIFFERENT", getString(R.string.can_you_say_sth_different), OptionType.STORY)) adapter.notifyDataSetChanged() return listOf(Message(getString(R.string.okay), false, System.currentTimeMillis())) } private fun nextSolutionQuestion(model: MessageListViewModel, adapter: ArrayAdapter): List { if (model.nextSolutionIndex.value == model.solutions.value?.size) { // we exhausted all options var summary = """ Component: ${model.component.value.toString()} Problem: ${model.problem.value.toString()} Tried Solutions: """.trimIndent() val solutions: ArrayList? = model.solutions.value if (solutions != null) { for (solution in solutions) { val item = "\n - $solution" summary = summary.plus(item) } } model.spinnerOptions.value?.clear() model.spinnerOptions.value?.add(SpinnerOption( "WHO_ARE_YOU", getString(R.string.who_are_you), OptionType.STORY)) adapter.notifyDataSetChanged() return listOf( Message(getString(R.string.okay), false, System.currentTimeMillis()), Message(getString(R.string.sorry), false, System.currentTimeMillis()), Message(getString(R.string.cant_help), false, System.currentTimeMillis()), Message(getString(R.string.should_contact_support), false, System.currentTimeMillis()), Message(getString(R.string.okay_wait), false, System.currentTimeMillis()), Message(getString(R.string.tell_support), false, System.currentTimeMillis()), Message(summary, false, System.currentTimeMillis()) ) } else { val solutionQuestion = getString(R.string.have_you_tried, model.solutions.value?.let {it[model.nextSolutionIndex.value ?: 0]}) model.nextSolutionIndex.value = (model.nextSolutionIndex.value ?: 0) + 1 model.currentSolutionState.value = State.HAVE_YOU_TRIED model.spinnerOptions.value?.clear() model.spinnerOptions.value?.addAll(SpinnerOption.getOptions( resources.getStringArray(R.array.binary_values), resources.getStringArray(R.array.binary), OptionType.BINARY_HAVE_YOU_TRIED)) adapter.notifyDataSetChanged() return listOf( Message(getString(R.string.okay), false, System.currentTimeMillis()), Message(getString(R.string.sorry), false, System.currentTimeMillis()), Message(solutionQuestion, false, System.currentTimeMillis()) ) } } }