Skip to content

Commit

Permalink
Refactor the logic out of the SleepTimerDialogController into it's ow…
Browse files Browse the repository at this point in the history
…n viewmodel.

Ensure that the creation of the bookmark is not cancelled through the lifecycle scope moving the coroutine in a dedicated scope.
  • Loading branch information
PaulWoitaschek committed Jul 4, 2022
1 parent 2bb4174 commit cefe290
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 75 deletions.
Original file line number Diff line number Diff line change
@@ -1,34 +1,25 @@
package voice.sleepTimer

import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.squareup.anvil.annotations.ContributesTo
import de.paulwoitaschek.flowpref.Pref
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import voice.common.conductor.DialogController
import voice.common.pref.PrefKeys
import voice.common.AppScope
import voice.common.conductor.DialogController
import voice.common.rootComponentAs
import voice.data.Book
import voice.data.getBookId
import voice.data.putBookId
import voice.data.repo.BookRepository
import voice.data.repo.BookmarkRepo
import voice.sleepTimer.databinding.DialogSleepBinding
import javax.inject.Inject
import javax.inject.Named

private const val NI_BOOK_ID = "ni#bookId"
private const val SI_MINUTES = "si#time"

/**
* Simple dialog for activating the sleep timer
*/
class SleepTimerDialogController(bundle: Bundle) : DialogController(bundle) {

constructor(bookId: Book.Id) : this(
Expand All @@ -38,93 +29,59 @@ class SleepTimerDialogController(bundle: Bundle) : DialogController(bundle) {
)

@Inject
lateinit var bookmarkRepo: BookmarkRepo

@Inject
lateinit var sleepTimer: SleepTimer

@Inject
lateinit var bookRepo: BookRepository
lateinit var viewModel: SleepTimerDialogViewModel

init {
rootComponentAs<Component>().inject(this)
}

@field:[Inject Named(PrefKeys.SLEEP_TIME)]
lateinit var sleepTimePref: Pref<Int>

private var selectedMinutes = 0

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SI_MINUTES, selectedMinutes)
}

override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val binding = DialogSleepBinding.inflate(activity!!.layoutInflater)

fun updateTimeState() {
binding.time.text = activity!!.getString(R.string.min, selectedMinutes.toString())

if (selectedMinutes > 0) binding.fab.show()
else binding.fab.hide()
}

@SuppressLint("SetTextI18n")
fun appendNumber(number: Int) {
val newNumber = selectedMinutes * 10 + number
if (newNumber > 999) return
selectedMinutes = newNumber
updateTimeState()
listOf(
binding.zero,
binding.one,
binding.two,
binding.three,
binding.four,
binding.five,
binding.six,
binding.seven,
binding.eight,
binding.nine,
).forEachIndexed { index, textView ->
textView.setOnClickListener {
viewModel.onNumberClicked(index)
}
}

selectedMinutes = savedViewState?.getInt(SI_MINUTES) ?: sleepTimePref.value
updateTimeState()

// find views and prepare clicks
binding.one.setOnClickListener { appendNumber(1) }
binding.two.setOnClickListener { appendNumber(2) }
binding.three.setOnClickListener { appendNumber(3) }
binding.four.setOnClickListener { appendNumber(4) }
binding.five.setOnClickListener { appendNumber(5) }
binding.six.setOnClickListener { appendNumber(6) }
binding.seven.setOnClickListener { appendNumber(7) }
binding.eight.setOnClickListener { appendNumber(8) }
binding.nine.setOnClickListener { appendNumber(9) }
binding.zero.setOnClickListener { appendNumber(0) }
// upon delete remove the last number
binding.delete.setOnClickListener {
selectedMinutes /= 10
updateTimeState()
viewModel.onNumberDeleteClicked()
}
// upon long click remove all numbers
binding.delete.setOnLongClickListener {
selectedMinutes = 0
updateTimeState()
viewModel.onNumberDeleteLongClicked()
true
}

lifecycleScope.launch {
viewModel.viewState().collectLatest { viewState ->
binding.time.text = activity!!.getString(R.string.min, viewState.selectedMinutes.toString())

binding.fab.setOnClickListener {
require(selectedMinutes > 0) { "fab should be hidden when time is invalid" }
sleepTimePref.value = selectedMinutes

lifecycleScope.launch {
val book = bookRepo.get(args.getBookId(NI_BOOK_ID)!!) ?: return@launch
bookmarkRepo.addBookmarkAtBookPosition(
book = book,
setBySleepTimer = true,
title = null
)
if (viewState.showFab) {
binding.fab.show()
} else {
binding.fab.hide()
}
}

sleepTimer.setActive(true)
}
binding.fab.setOnClickListener {
viewModel.onConfirmButtonClicked(args.getBookId(NI_BOOK_ID)!!)
dismissDialog()
}

return BottomSheetDialog(activity!!).apply {
setContentView(binding.root)
// hide the background so the fab looks overlapping
// hide the background so the fab appears overlapping
setOnShowListener {
val parentView = binding.root.parent as View
parentView.background = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package voice.sleepTimer

import de.paulwoitaschek.flowpref.Pref
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import voice.common.pref.PrefKeys
import voice.data.Book
import voice.data.repo.BookRepository
import voice.data.repo.BookmarkRepo
import javax.inject.Inject
import javax.inject.Named

class SleepTimerDialogViewModel
@Inject constructor(
private val bookmarkRepo: BookmarkRepo,
private val sleepTimer: SleepTimer,
private val bookRepo: BookRepository,
@Named(PrefKeys.SLEEP_TIME)
private val sleepTimePref: Pref<Int>
) {

private val scope = MainScope()

private val selectedMinutes = MutableStateFlow(sleepTimePref.value)

fun viewState(): Flow<SleepTimerDialogViewState> {
return selectedMinutes
.map { selectedMinutes ->
SleepTimerDialogViewState(
selectedMinutes = selectedMinutes,
showFab = selectedMinutes > 0
)
}
}

fun onNumberClicked(number: Int) {
require(number in 0..9)
selectedMinutes.update { oldValue ->
val newValue = (oldValue * 10 + number)
if (newValue > 999) {
oldValue
} else {
sleepTimePref.value = newValue
newValue
}
}
}

fun onNumberDeleteClicked() {
selectedMinutes.update { it / 10 }
}

fun onNumberDeleteLongClicked() {
selectedMinutes.update { 0 }
}

fun onConfirmButtonClicked(bookId: Book.Id) {
check(selectedMinutes.value > 0)

scope.launch {
val book = bookRepo.get(bookId) ?: return@launch
bookmarkRepo.addBookmarkAtBookPosition(
book = book,
setBySleepTimer = true,
title = null
)
}
sleepTimer.setActive(true)
}
}

data class SleepTimerDialogViewState(
val selectedMinutes: Int,
val showFab: Boolean,
)

0 comments on commit cefe290

Please sign in to comment.