From 88119add5a1061ca244827bb055dade2a7809933 Mon Sep 17 00:00:00 2001 From: Paul Klauser Date: Tue, 11 Jun 2024 17:36:15 -0400 Subject: [PATCH] Abstract the model for showing a Phrase on the home screen We didn't need as much data as we were passing up, and this lets us better assert that the content hasn't changed when observing reactively, reducing flickers. --- .../com/willowtree/vocable/AppKoinModule.kt | 2 +- .../vocable/presets/NumberPadFragment.kt | 4 +- .../vocable/presets/PhraseGridItem.kt | 17 ++ .../vocable/presets/PhrasesFragment.kt | 4 +- .../vocable/presets/PresetsFragment.kt | 6 +- .../vocable/presets/PresetsViewModel.kt | 27 +- .../vocable/presets/adapter/PhraseAdapter.kt | 65 ++--- .../utils/ILocalizedResourceUtility.kt | 2 + .../utils/locale/LocalizedResourceUtility.kt | 2 +- .../vocable/presets/PresetsViewModelTest.kt | 239 +++++++++--------- .../utils/FakeLocalizedResourceUtility.kt | 11 + 11 files changed, 202 insertions(+), 177 deletions(-) create mode 100644 app/src/main/java/com/willowtree/vocable/presets/PhraseGridItem.kt diff --git a/app/src/main/java/com/willowtree/vocable/AppKoinModule.kt b/app/src/main/java/com/willowtree/vocable/AppKoinModule.kt index e0224cb7..7f1332d3 100644 --- a/app/src/main/java/com/willowtree/vocable/AppKoinModule.kt +++ b/app/src/main/java/com/willowtree/vocable/AppKoinModule.kt @@ -101,7 +101,7 @@ val vocableKoinModule = module { single { VocableDatabase.createVocableDatabase(get()) } single { get().presetPhrasesDao() } single { VocableEnvironmentImpl() } - viewModel { PresetsViewModel(get(), get(), get(named())) } + viewModel { PresetsViewModel(get(), get(), get(named()), get()) } viewModel { EditCategoriesViewModel(get()) } viewModel { EditCategoryPhrasesViewModel(get(), get(), get()) } viewModel { AddUpdateCategoryViewModel(get(), get(), get()) } diff --git a/app/src/main/java/com/willowtree/vocable/presets/NumberPadFragment.kt b/app/src/main/java/com/willowtree/vocable/presets/NumberPadFragment.kt index 59b5cf50..dd6cd938 100644 --- a/app/src/main/java/com/willowtree/vocable/presets/NumberPadFragment.kt +++ b/app/src/main/java/com/willowtree/vocable/presets/NumberPadFragment.kt @@ -19,7 +19,7 @@ class NumberPadFragment : BaseFragment() { private const val KEY_PHRASES = "KEY_PHRASES" const val MAX_PHRASES = 12 - fun newInstance(phrases: List) = NumberPadFragment().apply { + fun newInstance(phrases: List) = NumberPadFragment().apply { arguments = bundleOf(KEY_PHRASES to ArrayList(phrases)) } } @@ -37,7 +37,7 @@ class NumberPadFragment : BaseFragment() { val numColumns = resources.getInteger(R.integer.number_pad_columns) val numRows = resources.getInteger(R.integer.number_pad_rows) - val phrases = arguments?.getParcelableArrayList(KEY_PHRASES) + val phrases = arguments?.getParcelableArrayList(KEY_PHRASES) phrases?.let { with(binding.phrasesContainer) { diff --git a/app/src/main/java/com/willowtree/vocable/presets/PhraseGridItem.kt b/app/src/main/java/com/willowtree/vocable/presets/PhraseGridItem.kt new file mode 100644 index 00000000..d6d24a23 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/presets/PhraseGridItem.kt @@ -0,0 +1,17 @@ +package com.willowtree.vocable.presets + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +sealed class PhraseGridItem : Parcelable { + + @Parcelize + data class Phrase( + val phraseId: String, + val text: String + ) : PhraseGridItem() + + @Parcelize + object AddPhrase : PhraseGridItem() +} diff --git a/app/src/main/java/com/willowtree/vocable/presets/PhrasesFragment.kt b/app/src/main/java/com/willowtree/vocable/presets/PhrasesFragment.kt index fa6454b5..db11b95b 100644 --- a/app/src/main/java/com/willowtree/vocable/presets/PhrasesFragment.kt +++ b/app/src/main/java/com/willowtree/vocable/presets/PhrasesFragment.kt @@ -19,7 +19,7 @@ class PhrasesFragment : BaseFragment() { companion object { private const val KEY_PHRASES = "KEY_PHRASES" - fun newInstance(phrases: List): PhrasesFragment { + fun newInstance(phrases: List): PhrasesFragment { return PhrasesFragment().apply { arguments = Bundle().apply { putParcelableArrayList(KEY_PHRASES, ArrayList(phrases)) @@ -41,7 +41,7 @@ class PhrasesFragment : BaseFragment() { val numColumns = resources.getInteger(R.integer.phrases_columns) val numRows = resources.getInteger(R.integer.phrases_rows) - val phrases = arguments?.getParcelableArrayList(KEY_PHRASES) + val phrases = arguments?.getParcelableArrayList(KEY_PHRASES) phrases?.let { with(binding.phrasesContainer) { layoutManager = GridLayoutManager(requireContext(), numColumns) diff --git a/app/src/main/java/com/willowtree/vocable/presets/PresetsFragment.kt b/app/src/main/java/com/willowtree/vocable/presets/PresetsFragment.kt index 92885266..d9e2a5df 100644 --- a/app/src/main/java/com/willowtree/vocable/presets/PresetsFragment.kt +++ b/app/src/main/java/com/willowtree/vocable/presets/PresetsFragment.kt @@ -261,7 +261,7 @@ class PresetsFragment : BaseFragment() { } } - private fun handlePhrases(phrases: List) { + private fun handlePhrases(phrases: List) { binding.emptyPhrasesText.isVisible = phrases.isEmpty() && !recentsCategorySelected && categoriesAdapter.getSize() > 0 binding.emptyAddPhraseButton.isVisible = @@ -306,9 +306,9 @@ class PresetsFragment : BaseFragment() { } inner class PhrasesPagerAdapter(fm: FragmentManager) : - VocableFragmentStateAdapter(fm, viewLifecycleOwner.lifecycle) { + VocableFragmentStateAdapter(fm, viewLifecycleOwner.lifecycle) { - override fun setItems(items: List) { + override fun setItems(items: List) { super.setItems(items) setPagingButtonsEnabled(phrasesAdapter.numPages > 1) } diff --git a/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt b/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt index 2070deb1..920b8391 100644 --- a/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt +++ b/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt @@ -7,11 +7,13 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.willowtree.vocable.ICategoriesUseCase import com.willowtree.vocable.IPhrasesUseCase +import com.willowtree.vocable.utils.ILocalizedResourceUtility import com.willowtree.vocable.utils.IdlingResourceContainer import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest @@ -23,7 +25,8 @@ import kotlinx.coroutines.launch class PresetsViewModel( private val categoriesUseCase: ICategoriesUseCase, private val phrasesUseCase: IPhrasesUseCase, - private val idlingResourceContainer: IdlingResourceContainer + private val idlingResourceContainer: IdlingResourceContainer, + private val localizedResourceUtility: ILocalizedResourceUtility ) : ViewModel() { val categoryList: LiveData> = categoriesUseCase.categories() @@ -53,24 +56,30 @@ class PresetsViewModel( }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), null) val selectedCategoryLiveData: LiveData = selectedCategory.asLiveData() - val currentPhrases: LiveData> = selectedCategoryId + val currentPhrases: LiveData> = selectedCategoryId .filterNotNull() .flatMapLatest { categoryId -> phrasesUseCase.getPhrasesForCategoryFlow(categoryId).map { phrases -> - val phrasesToReturn = phrases.run { + val phraseGridItems: List = phrases.run { if (categoryId != PresetCategories.RECENTS.id) { sortedBy { it.sortOrder } } else { this } - }.toMutableList() + }.map { + PhraseGridItem.Phrase( + it.phraseId, + localizedResourceUtility.getTextFromPhrase(it) + ) + } if (categoryId != PresetCategories.RECENTS.id && categoryId != PresetCategories.USER_KEYPAD.id && phrases.isNotEmpty()) { - //Add null to end of normal non empty category phrase list for the "+ Add Phrase" button - phrasesToReturn.add(null) + phraseGridItems + PhraseGridItem.AddPhrase + } else { + phraseGridItems } - phrasesToReturn } } + .distinctUntilChanged() .asLiveData() private val liveNavToAddPhrase = MutableLiveData() @@ -90,10 +99,10 @@ class PresetsViewModel( selectedCategoryId.update { categoryId } } - fun addToRecents(phrase: Phrase) { + fun addToRecents(phraseId: String) { viewModelScope.launch { idlingResourceContainer.run { - phrasesUseCase.updatePhraseLastSpokenTime(phrase.phraseId) + phrasesUseCase.updatePhraseLastSpokenTime(phraseId) } } } diff --git a/app/src/main/java/com/willowtree/vocable/presets/adapter/PhraseAdapter.kt b/app/src/main/java/com/willowtree/vocable/presets/adapter/PhraseAdapter.kt index 9b6e928a..1affb149 100644 --- a/app/src/main/java/com/willowtree/vocable/presets/adapter/PhraseAdapter.kt +++ b/app/src/main/java/com/willowtree/vocable/presets/adapter/PhraseAdapter.kt @@ -8,56 +8,51 @@ import androidx.recyclerview.widget.RecyclerView import com.willowtree.vocable.R import com.willowtree.vocable.databinding.PhraseButtonAddBinding import com.willowtree.vocable.databinding.PhraseButtonBinding -import com.willowtree.vocable.presets.Phrase -import com.willowtree.vocable.utils.locale.LocalizedResourceUtility -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject +import com.willowtree.vocable.presets.PhraseGridItem import java.util.Locale class PhraseAdapter( - private val phrases: List, + private val phrases: List, private val numRows: Int, - private val phraseClickAction: ((Phrase) -> Unit)?, + private val phraseClickAction: ((String) -> Unit)?, private val phraseAddClickAction: (() -> Unit)? -) : - RecyclerView.Adapter(), KoinComponent { +) : RecyclerView.Adapter() { abstract inner class PhraseViewHolder( itemView: View ) : RecyclerView.ViewHolder(itemView) { - abstract fun bind(text: String, position: Int) + abstract fun bind(position: Int) } - inner class PhraseItemViewHolder(itemView: View) : PhraseAdapter.PhraseViewHolder(itemView) { + inner class PhraseGridItemViewHolder(itemView: View) : + PhraseAdapter.PhraseViewHolder(itemView) { - override fun bind(text: String, position: Int) { - val binding = PhraseButtonBinding.bind(itemView) - binding.root.setText(text, Locale.getDefault()) - binding.root.action = { - phrases[position]?.let { phraseClickAction?.invoke(it) } - } - } - } - - inner class PhraseAddItemViewHolder(itemView: View) : PhraseAdapter.PhraseViewHolder(itemView) { + override fun bind(position: Int) { + when (val gridItem = phrases[position]) { + is PhraseGridItem.Phrase -> { + val binding = PhraseButtonBinding.bind(itemView) + binding.root.setText(gridItem.text, Locale.getDefault()) + binding.root.action = { + phraseClickAction?.invoke(gridItem.phraseId) + } + } - override fun bind(text: String, position: Int) { - val binding = PhraseButtonAddBinding.bind(itemView) - binding.root.action = { - phraseAddClickAction?.invoke() + PhraseGridItem.AddPhrase -> { + val binding = PhraseButtonAddBinding.bind(itemView) + binding.root.action = { + phraseAddClickAction?.invoke() + } + } } } } - private val localizedResourceUtility: LocalizedResourceUtility by inject() - private var _minHeight: Int? = null override fun getItemViewType(position: Int): Int { - return if (phrases[position] == null) { - R.layout.phrase_button_add - } else { - R.layout.phrase_button + return when (phrases[position]) { + is PhraseGridItem.Phrase -> R.layout.phrase_button + PhraseGridItem.AddPhrase -> R.layout.phrase_button_add } } @@ -74,16 +69,12 @@ class PhraseAdapter( isInvisible = false } } - return if (viewType == R.layout.phrase_button_add) { - PhraseAddItemViewHolder(itemView) - } else { - PhraseItemViewHolder(itemView) - } + + return PhraseGridItemViewHolder(itemView) } override fun onBindViewHolder(holder: PhraseAdapter.PhraseViewHolder, position: Int) { - val text = localizedResourceUtility.getTextFromPhrase(phrases[position]) - holder.bind(text, position) + holder.bind(position) } private fun getMinHeight(parent: ViewGroup): Int { diff --git a/app/src/main/java/com/willowtree/vocable/utils/ILocalizedResourceUtility.kt b/app/src/main/java/com/willowtree/vocable/utils/ILocalizedResourceUtility.kt index c74ae5f0..6b5ac72b 100644 --- a/app/src/main/java/com/willowtree/vocable/utils/ILocalizedResourceUtility.kt +++ b/app/src/main/java/com/willowtree/vocable/utils/ILocalizedResourceUtility.kt @@ -1,7 +1,9 @@ package com.willowtree.vocable.utils import com.willowtree.vocable.presets.Category +import com.willowtree.vocable.presets.Phrase interface ILocalizedResourceUtility { fun getTextFromCategory(category: Category?): String + fun getTextFromPhrase(phrase: Phrase?): String } \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/utils/locale/LocalizedResourceUtility.kt b/app/src/main/java/com/willowtree/vocable/utils/locale/LocalizedResourceUtility.kt index 76f6b9a8..a64e10b1 100644 --- a/app/src/main/java/com/willowtree/vocable/utils/locale/LocalizedResourceUtility.kt +++ b/app/src/main/java/com/willowtree/vocable/utils/locale/LocalizedResourceUtility.kt @@ -13,7 +13,7 @@ class LocalizedResourceUtility( return category?.text(context) ?: "" } - fun getTextFromPhrase(phrase: Phrase?): String { + override fun getTextFromPhrase(phrase: Phrase?): String { return phrase?.text(context) ?: "" } } \ No newline at end of file diff --git a/app/src/test/java/com/willowtree/vocable/presets/PresetsViewModelTest.kt b/app/src/test/java/com/willowtree/vocable/presets/PresetsViewModelTest.kt index 676d670c..c3d1b104 100644 --- a/app/src/test/java/com/willowtree/vocable/presets/PresetsViewModelTest.kt +++ b/app/src/test/java/com/willowtree/vocable/presets/PresetsViewModelTest.kt @@ -8,6 +8,7 @@ import com.willowtree.vocable.MainDispatcherRule import com.willowtree.vocable.getOrAwaitValue import com.willowtree.vocable.room.CategoryDto import com.willowtree.vocable.room.PhraseDto +import com.willowtree.vocable.utils.FakeLocalizedResourceUtility import com.willowtree.vocable.utils.IdlingResourceContainerImpl import com.willowtree.vocable.utils.locale.LocalesWithText import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -34,7 +35,8 @@ class PresetsViewModelTest { return PresetsViewModel( fakeCategoriesUseCase, fakePhrasesUseCase, - prodIdlingResourceContainer + prodIdlingResourceContainer, + FakeLocalizedResourceUtility() ) } @@ -109,126 +111,129 @@ class PresetsViewModelTest { @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `selected category is hidden and next immediate category is shown`() = runTest(UnconfinedTestDispatcher()) { - fakeCategoriesUseCase._categories.update { - listOf( - Category.StoredCategory( - categoryId = "1", - localizedName = LocalesWithText(mapOf("en_US" to "category")), - hidden = true, - sortOrder = 0 - ), - Category.StoredCategory( - categoryId = "2", - localizedName = LocalesWithText(mapOf("en_US" to "second category")), - hidden = false, - sortOrder = 1 + fun `selected category is hidden and next immediate category is shown`() = + runTest(UnconfinedTestDispatcher()) { + fakeCategoriesUseCase._categories.update { + listOf( + Category.StoredCategory( + categoryId = "1", + localizedName = LocalesWithText(mapOf("en_US" to "category")), + hidden = true, + sortOrder = 0 + ), + Category.StoredCategory( + categoryId = "2", + localizedName = LocalesWithText(mapOf("en_US" to "second category")), + hidden = false, + sortOrder = 1 + ) ) - ) - } + } - val vm = createViewModel() + val vm = createViewModel() - vm.onCategorySelected("1") + vm.onCategorySelected("1") - vm.selectedCategory.test { - assertEquals( - Category.StoredCategory( - categoryId = "2", - localizedName = LocalesWithText(mapOf("en_US" to "second category")), - hidden = false, - sortOrder = 1 - ), - awaitItem() - ) + vm.selectedCategory.test { + assertEquals( + Category.StoredCategory( + categoryId = "2", + localizedName = LocalesWithText(mapOf("en_US" to "second category")), + hidden = false, + sortOrder = 1 + ), + awaitItem() + ) + } } - } @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `selected category (last in list) is hidden and first category is shown`() = runTest(UnconfinedTestDispatcher()) { - fakeCategoriesUseCase._categories.update { - listOf( - Category.StoredCategory( - categoryId = "1", - localizedName = LocalesWithText(mapOf("en_US" to "category")), - hidden = false, - sortOrder = 0 - ), - Category.StoredCategory( - categoryId = "2", - localizedName = LocalesWithText(mapOf("en_US" to "second category")), - hidden = false, - sortOrder = 1 - ), - Category.StoredCategory( - categoryId = "3", - localizedName = LocalesWithText(mapOf("en_US" to "third category")), - hidden = true, - sortOrder = 2 + fun `selected category (last in list) is hidden and first category is shown`() = + runTest(UnconfinedTestDispatcher()) { + fakeCategoriesUseCase._categories.update { + listOf( + Category.StoredCategory( + categoryId = "1", + localizedName = LocalesWithText(mapOf("en_US" to "category")), + hidden = false, + sortOrder = 0 + ), + Category.StoredCategory( + categoryId = "2", + localizedName = LocalesWithText(mapOf("en_US" to "second category")), + hidden = false, + sortOrder = 1 + ), + Category.StoredCategory( + categoryId = "3", + localizedName = LocalesWithText(mapOf("en_US" to "third category")), + hidden = true, + sortOrder = 2 + ) ) - ) - } + } - val vm = createViewModel() + val vm = createViewModel() - vm.onCategorySelected("3") + vm.onCategorySelected("3") - vm.selectedCategory.test { - assertEquals( - Category.StoredCategory( - categoryId = "1", - localizedName = LocalesWithText(mapOf("en_US" to "category")), - hidden = false, - sortOrder = 0 - ), - awaitItem() - ) + vm.selectedCategory.test { + assertEquals( + Category.StoredCategory( + categoryId = "1", + localizedName = LocalesWithText(mapOf("en_US" to "category")), + hidden = false, + sortOrder = 0 + ), + awaitItem() + ) + } } - } @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `selected category is hidden and next non-hidden category is shown`() = runTest(UnconfinedTestDispatcher()) { - fakeCategoriesUseCase._categories.update { - listOf( - Category.StoredCategory( - categoryId = "1", - localizedName = LocalesWithText(mapOf("en_US" to "category")), - hidden = true, - sortOrder = 0 - ), - Category.StoredCategory( - categoryId = "2", - localizedName = LocalesWithText(mapOf("en_US" to "second category")), - hidden = false, - sortOrder = 1 - ), - Category.StoredCategory( - categoryId = "3", - localizedName = LocalesWithText(mapOf("en_US" to "third category")), - hidden = true, - sortOrder = 2 + fun `selected category is hidden and next non-hidden category is shown`() = + runTest(UnconfinedTestDispatcher()) { + fakeCategoriesUseCase._categories.update { + listOf( + Category.StoredCategory( + categoryId = "1", + localizedName = LocalesWithText(mapOf("en_US" to "category")), + hidden = true, + sortOrder = 0 + ), + Category.StoredCategory( + categoryId = "2", + localizedName = LocalesWithText(mapOf("en_US" to "second category")), + hidden = false, + sortOrder = 1 + ), + Category.StoredCategory( + categoryId = "3", + localizedName = LocalesWithText(mapOf("en_US" to "third category")), + hidden = true, + sortOrder = 2 + ) ) - ) - } + } - val vm = createViewModel() + val vm = createViewModel() - vm.onCategorySelected("3") + vm.onCategorySelected("3") - vm.selectedCategory.test { - assertEquals( - Category.StoredCategory( - categoryId = "2", - localizedName = LocalesWithText(mapOf("en_US" to "second category")), - hidden = false, - sortOrder = 1 - ), - awaitItem() - ) + vm.selectedCategory.test { + assertEquals( + Category.StoredCategory( + categoryId = "2", + localizedName = LocalesWithText(mapOf("en_US" to "second category")), + hidden = false, + sortOrder = 1 + ), + awaitItem() + ) + } } - } @Test fun `current phrases updated when category ID changed`() { @@ -277,13 +282,11 @@ class PresetsViewModelTest { assertEquals( listOf( - CustomPhrase( + PhraseGridItem.Phrase( phraseId = "2", - localizedUtterance = LocalesWithText(mapOf("en_US" to "Goodbye")), - sortOrder = 0, - lastSpokenDate = null, + text = "Goodbye", ), - null + PhraseGridItem.AddPhrase ), vm.currentPhrases.getOrAwaitValue() ) @@ -326,19 +329,15 @@ class PresetsViewModelTest { vm.onCategorySelected("2") assertEquals( listOf( - CustomPhrase( + PhraseGridItem.Phrase( phraseId = "2", - localizedUtterance = LocalesWithText(mapOf("en_US" to "Goodbye")), - sortOrder = 0, - lastSpokenDate = null, + text = "Goodbye", ), - CustomPhrase( + PhraseGridItem.Phrase( phraseId = "1", - localizedUtterance = LocalesWithText(mapOf("en_US" to "Hello")), - sortOrder = 1, - lastSpokenDate = null, + text = "Hello" ), - null + PhraseGridItem.AddPhrase ), vm.currentPhrases.getOrAwaitValue() ) @@ -381,17 +380,13 @@ class PresetsViewModelTest { vm.onCategorySelected(PresetCategories.RECENTS.id) assertEquals( listOf( - CustomPhrase( + PhraseGridItem.Phrase( phraseId = "1", - localizedUtterance = LocalesWithText(mapOf("en_US" to "Hello")), - sortOrder = 1, - lastSpokenDate = null, + text = "Hello", ), - CustomPhrase( + PhraseGridItem.Phrase( phraseId = "2", - localizedUtterance = LocalesWithText(mapOf("en_US" to "Goodbye")), - sortOrder = 0, - lastSpokenDate = null, + text = "Goodbye", ) ), vm.currentPhrases.getOrAwaitValue() diff --git a/app/src/test/java/com/willowtree/vocable/utils/FakeLocalizedResourceUtility.kt b/app/src/test/java/com/willowtree/vocable/utils/FakeLocalizedResourceUtility.kt index 26ed4ee4..fd18295e 100644 --- a/app/src/test/java/com/willowtree/vocable/utils/FakeLocalizedResourceUtility.kt +++ b/app/src/test/java/com/willowtree/vocable/utils/FakeLocalizedResourceUtility.kt @@ -1,6 +1,9 @@ package com.willowtree.vocable.utils import com.willowtree.vocable.presets.Category +import com.willowtree.vocable.presets.CustomPhrase +import com.willowtree.vocable.presets.Phrase +import com.willowtree.vocable.presets.PresetPhrase class FakeLocalizedResourceUtility : ILocalizedResourceUtility { override fun getTextFromCategory(category: Category?): String { @@ -11,4 +14,12 @@ class FakeLocalizedResourceUtility : ILocalizedResourceUtility { null -> "" } } + + override fun getTextFromPhrase(phrase: Phrase?): String { + return when(phrase) { + is CustomPhrase -> phrase.localizedUtterance?.localesTextMap?.entries?.first()?.value ?: "" + is PresetPhrase -> phrase.phraseId + null -> "" + } + } } \ No newline at end of file