From b39e47d042ea60e6557461de241a2be5608d748e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 16 Jun 2022 13:40:18 -0700 Subject: [PATCH 01/38] SW translation fixes + better UI. This fixes a bunch of Swahili string bugs by stripping out unnecessary prefixed whitespace. This also introduces a fast language switch bar between English and Swahili. It's not currently tested or gated behind a platform flag, but both of these will be changed if the team decides to proceed with this solution. --- .../player/state/StateFragmentPresenter.kt | 16 +- .../app/player/state/StateViewModel.kt | 102 +++++++- app/src/main/res/layout/state_fragment.xml | 91 ++++--- app/src/main/res/values-sw/strings.xml | 240 +++++++++--------- .../main/res/values/untranslated_strings.xml | 3 + 5 files changed, 286 insertions(+), 166 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index f0d22cecad7..4382907bb81 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -33,7 +33,6 @@ import org.oppia.android.app.player.state.listener.RouteToHintsAndSolutionListen import org.oppia.android.app.player.stopplaying.StopStatePlayingSessionWithSavedProgressListener import org.oppia.android.app.topic.conceptcard.ConceptCardFragment.Companion.CONCEPT_CARD_DIALOG_FRAGMENT_TAG import org.oppia.android.app.utility.SplitScreenManager -import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.StateFragmentBinding import org.oppia.android.domain.exploration.ExplorationProgressController import org.oppia.android.domain.oppialogger.OppiaLogger @@ -61,14 +60,14 @@ class StateFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val context: Context, - private val viewModelProvider: ViewModelProvider, private val explorationProgressController: ExplorationProgressController, private val storyProgressController: StoryProgressController, private val oppiaLogger: OppiaLogger, @DefaultResourceBucketName private val resourceBucketName: String, private val assemblerBuilderFactory: StatePlayerRecyclerViewAssembler.Builder.Factory, private val splitScreenManager: SplitScreenManager, - private val oppiaClock: OppiaClock + private val oppiaClock: OppiaClock, + private val viewModel: StateViewModel ) { private val routeToHintsAndSolutionListener = activity as RouteToHintsAndSolutionListener @@ -84,9 +83,6 @@ class StateFragmentPresenter @Inject constructor( private lateinit var recyclerViewAdapter: RecyclerView.Adapter<*> private lateinit var helpIndex: HelpIndex - private val viewModel: StateViewModel by lazy { - getStateViewModel() - } private lateinit var recyclerViewAssembler: StatePlayerRecyclerViewAssembler private val ephemeralStateLiveData: LiveData> by lazy { explorationProgressController.getCurrentState().toLiveData() @@ -106,6 +102,7 @@ class StateFragmentPresenter @Inject constructor( this.topicId = topicId this.storyId = storyId this.explorationId = explorationId + viewModel.initializeProfile(profileId) binding = StateFragmentBinding.inflate( inflater, @@ -256,10 +253,6 @@ class StateFragmentPresenter @Inject constructor( subscribeToHintSolution(explorationProgressController.submitSolutionIsRevealed()) } - private fun getStateViewModel(): StateViewModel { - return viewModelProvider.getForFragment(fragment, StateViewModel::class.java) - } - private fun getAudioFragment(): Fragment? { return fragment.childFragmentManager.findFragmentByTag(TAG_AUDIO_FRAGMENT) } @@ -427,8 +420,7 @@ class StateFragmentPresenter @Inject constructor( ) } - fun setAudioBarVisibility(visibility: Boolean) = - getStateViewModel().setAudioBarVisibility(visibility) + fun setAudioBarVisibility(visibility: Boolean) = viewModel.setAudioBarVisibility(visibility) fun scrollToTop() { binding.stateRecyclerView.smoothScrollToPosition(0) diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/StateViewModel.kt index a46208966b1..4739914eb82 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StateViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateViewModel.kt @@ -2,19 +2,39 @@ package org.oppia.android.app.player.state import androidx.databinding.ObservableField import androidx.databinding.ObservableList +import androidx.fragment.app.Fragment +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralState +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel import org.oppia.android.app.viewmodel.ObservableArrayList import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.domain.exploration.ExplorationProgressController +import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.translation.TranslationController +import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import org.oppia.android.util.locale.OppiaLocale import javax.inject.Inject /** [ViewModel] for state-fragment. */ @FragmentScope -class StateViewModel @Inject constructor() : ObservableViewModel() { +class StateViewModel @Inject constructor( + private val explorationProgressController: ExplorationProgressController, + private val translationController: TranslationController, + private val machineLocale: OppiaLocale.MachineLocale, + private val oppiaLogger: OppiaLogger, + private val fragment: Fragment +) : ObservableViewModel() { val itemList: ObservableList = ObservableArrayList() val rightItemList: ObservableList = ObservableArrayList() @@ -26,9 +46,27 @@ class StateViewModel @Inject constructor() : ObservableViewModel() { val isHintBulbVisible = ObservableField(false) val isHintOpenedAndUnRevealed = ObservableField(false) + val hasSwahiliTranslations: LiveData by lazy { + Transformations.map( + explorationProgressController.getCurrentState().toLiveData(), + ::processWhetherSwahiliIsSupported + ) + } + val hasEnabledSwahiliTranslations: LiveData by lazy { + Transformations.map( + translationController.getWrittenTranslationContentLanguage(profileId).toLiveData(), + ::processIsCurrentLanguageSwahili + ) + } + var currentStateName = ObservableField(null as? String?) private val canSubmitAnswer = ObservableField(false) + private lateinit var profileId: ProfileId + + fun initializeProfile(profileId: ProfileId) { + this.profileId = profileId + } fun setAudioBarVisibility(audioBarVisible: Boolean) { isAudioBarVisible.set(audioBarVisible) @@ -56,6 +94,33 @@ class StateViewModel @Inject constructor() : ObservableViewModel() { ) ?: UserAnswer.getDefaultInstance() } + fun toggleContentLanguage(isSwahiliEnabled: Boolean) { + val languageSelection = WrittenTranslationLanguageSelection.newBuilder().apply { + selectedLanguage = if (isSwahiliEnabled) OppiaLanguage.ENGLISH else OppiaLanguage.SWAHILI + }.build() + val updateResultProvider = + translationController.updateWrittenTranslationContentLanguage(profileId, languageSelection) + val updateResultLiveData = updateResultProvider.toLiveData() + updateResultLiveData.observe( + fragment, + object : Observer> { + override fun onChanged(result: AsyncResult?) { + if (result is AsyncResult.Failure) { + oppiaLogger.e( + "StateViewModel", + "Failed to update content language to:" + + " ${languageSelection.selectedLanguage.ordinal}.", + result.error + ) + } + if (result !is AsyncResult.Pending) { + updateResultLiveData.removeObserver(this) + } + } + } + ) + } + private fun getPendingAnswerWithoutError( answerHandler: InteractionAnswerHandler? ): UserAnswer? { @@ -73,4 +138,39 @@ class StateViewModel @Inject constructor() : ObservableViewModel() { itemList } } + + private fun processIsCurrentLanguageSwahili(languageResult: AsyncResult): Boolean { + return when (languageResult) { + is AsyncResult.Pending -> false + is AsyncResult.Success -> languageResult.value == OppiaLanguage.SWAHILI + is AsyncResult.Failure -> { + oppiaLogger.e( + "StateViewModel", "Failed to retrieve content language.", languageResult.error + ) + false + } + } + } + + private fun processWhetherSwahiliIsSupported(stateResult: AsyncResult): Boolean { + return when (stateResult) { + is AsyncResult.Pending -> false + is AsyncResult.Success -> { + // It would be nice if there was a domain utility to do this if it's needed elsewhere (or, + // better yet, just using the language protos directly in the state structure so no raw language codes need to be processed). + val supportedLanguageCodes = stateResult.value.state.writtenTranslationsMap.values.flatMap { + it.translationMappingMap.keys + }.toSet() + supportedLanguageCodes.any { + machineLocale.run { + it.toMachineLowerCase() == "sw" + } + } + } + is AsyncResult.Failure -> { + oppiaLogger.e("StateViewModel", "Failed to retrieve state.", stateResult.error) + false + } + } + } } diff --git a/app/src/main/res/layout/state_fragment.xml b/app/src/main/res/layout/state_fragment.xml index eb122dac2a0..12b9df7bca8 100755 --- a/app/src/main/res/layout/state_fragment.xml +++ b/app/src/main/res/layout/state_fragment.xml @@ -100,47 +100,72 @@ android:layout_height="match_parent" /> + android:layout_gravity="bottom|start"> - - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@+id/hints_and_solution_fragment_container" + android:background="@color/color_palette_background_color" + android:visibility="@{viewModel.hasSwahiliTranslations ? View.VISIBLE : View.GONE}"> + + - + android:background="@color/audio_component_background" + android:visibility="@{viewModel.isHintBulbVisible() ? View.VISIBLE : View.GONE}"> + + + + + + + Samahani,vipeo vingi havitumiki na programu.Tafadhali rekebisha jibu lako. Samahani, nguvu za juu zaidi ya 5 haiungwi mkono na programu. Tafadhali rekebisha jibu lako. Samahani, nguvu zinazorudiwa/vielezo hazihimiliwi na programu. Tafadhali punguza jibu lako kwa nguvu moja. - \nPembejeo haipo kwa mzizi wa mraba. - \nKugawanya kwa sufuri si halali. Tafadhali rekebisha jibu lako. + Pembejeo haipo kwa mzizi wa mraba. + Kugawanya kwa sufuri si halali. Tafadhali rekebisha jibu lako. Inaonekana umeingiza baadhi ya vigezo. Tafadhali hakikisha kuwa jibu lako lina nambari pekee na uondoe vigezo vyovyote kutoka kwa jibu lako. - \nTafadhali tumia vigezo vilivyobainishwa katika swali na si %s. + Tafadhali tumia vigezo vilivyobainishwa katika swali na si %s. Mlinganyo yako inakosa ishara \'=\'. - \nMlinganyo wako una ishara nyingi sana \'=\'. Inapaswa kuwa na moja tu. - \nMoja ya pande za \'=\' katika mlinganyo wako ni tupu. - \nChaguo la kukokotoa \'%s\' haitumiki. Tafadhali rekebisha jibu lako. - \nUlimaanisha sqrt? Ikiwa sivyo, tafadhali tenganisha vigeu kwa kutumia alama za kuzidisha. + Mlinganyo wako una ishara nyingi sana \'=\'. Inapaswa kuwa na moja tu. + Moja ya pande za \'=\' katika mlinganyo wako ni tupu. + Chaguo la kukokotoa \'%s\' haitumiki. Tafadhali rekebisha jibu lako. + Ulimaanisha sqrt? Ikiwa sivyo, tafadhali tenganisha vigeu kwa kutumia alama za kuzidisha. Samahani, hatukuweza kuelewa jibu lako. Tafadhali iangalie ili kuhakikisha kuwa hakuna hitilafu zozote. Washa sauti kwa somo hili. Hadithi Zilizochezwa hivi karibuni @@ -158,126 +158,126 @@ Jibu lako lina koloni mbili (:) karibu na kila moja. Idadi ya masharti si sawa na masharti yanayohitajika. Uwiano hauwezi kuwa na 0 kama kipengele. - \nUkubwa usiojulikana - \n Baiti %s + Ukubwa usiojulikana + Baiti %s %s KB %s MB %s GB - \nSahihi! - \nMada: %s - \n%1$s katika %2$s + Sahihi! + Mada: %s + %1$s katika %2$s Sura 1\n - \n\n Sura %s \n + Sura %s \n Hadithi 1\n - \n Hadithi %s\n + Hadithi %s\n %s kati ya Sura %s Imekamilika - %s ya Sura %s Imekamilika + %s ya Sura %s Imekamilika Somo 1\n - \n Masomo %s \n + Masomo %s \n - \nUkurasa wa kuchagua wasifu - \nMsimamizi - \nChagua wasifu wako - \nOngeza Wasifu + Ukurasa wa kuchagua wasifu + Msimamizi + Chagua wasifu wako + Ongeza Wasifu Weka Wasifu Nyingi - \nOngeza hadi watumiaji 10 kwenye akaunti yako. Nzuri sana kwa familia na madarasa. - \nUdhibiti vya Msimamizi + Ongeza hadi watumiaji 10 kwenye akaunti yako. Nzuri sana kwa familia na madarasa. + Udhibiti vya Msimamizi Lugha Vidhibiti vya Msimamizi - \nIdhinisha kuongeza wasifu + Idhinisha kuongeza wasifu Idhinisha kufikia Vidhibiti vya Msimamizi - \nIdhini ya Msimamizi Inahitajika + Idhini ya Msimamizi Inahitajika Weka Nambari ya Siri ya Msimamizi ili kuunda akaunti mpya. Weka Nambari ya Siri ya Msimamizi ili kufikia Vidhibiti vya Msimamizi. - \nNambari ya Siri ya Msimamizi + Nambari ya Siri ya Msimamizi Nambari ya Siri ya Msimamizi si Sahihi. Tafadhali jaribu tena. - \nTafadhali weka Nambari ya Siri ya Msimamizi. - \nWasilisha + Tafadhali weka Nambari ya Siri ya Msimamizi. + Wasilisha Funga - \nKabla ya kuongeza wasifu, tunahitaji kulinda akaunti yako kwa kuunda Nambari ya Siri. Hii hukupa uwezo wa kuidhinisha upakuaji na kudhibiti wasifu kwenye kifaa. - \nTumia Nambari yako ya siri uliyoweka kwa akaunti za kibinafsi kama vile benki au usalama wa kijamii. - \nNambari mpya ya Siri ya tarakimu 5 + Kabla ya kuongeza wasifu, tunahitaji kulinda akaunti yako kwa kuunda Nambari ya Siri. Hii hukupa uwezo wa kuidhinisha upakuaji na kudhibiti wasifu kwenye kifaa. + Tumia Nambari yako ya siri uliyoweka kwa akaunti za kibinafsi kama vile benki au usalama wa kijamii. + Nambari mpya ya Siri ya tarakimu 5 Thibitisha Nambari ya Siri ya tarakimu 5 - \nNambari yako ya Siri inapaswa kuwa na urefu wa tarakimu 5. - \nTafadhali hakikisha kwamba Nambari za Siri zote mbili zinalingana. + Nambari yako ya Siri inapaswa kuwa na urefu wa tarakimu 5. + Tafadhali hakikisha kwamba Nambari za Siri zote mbili zinalingana. Hifadhi - \nIdhinisha kuongeza wasifu - \nOngeza Wasifu - \nOngeza Wasifu + Idhinisha kuongeza wasifu + Ongeza Wasifu + Ongeza Wasifu Jina* - \nNambari ya Siri ya tarakimu 3* + Nambari ya Siri ya tarakimu 3* Thibitisha Nambari ya Siri ya tarakimu 3* - \nRuhusu Upakuaji wa Ufikiaji - \nMtumiaji anaweza kupakua na kufuta maudhui bila Nambari ya Siri ya Msimamizi. + Ruhusu Upakuaji wa Ufikiaji + Mtumiaji anaweza kupakua na kufuta maudhui bila Nambari ya Siri ya Msimamizi. Anzisha Funga - \nUkiwa na Nambari ya Siri, hakuna mtu mwingine anayeweza kufikia wasifu kando na mtumiaji huyu aliyekabidhiwa. - \nTumeshindwa kuhifadhi picha yako. Tafadhali jaribu tena. + Ukiwa na Nambari ya Siri, hakuna mtu mwingine anayeweza kufikia wasifu kando na mtumiaji huyu aliyekabidhiwa. + Tumeshindwa kuhifadhi picha yako. Tafadhali jaribu tena. Jina hili tayari linatumiwa na wasifu mwingine. Tafadhali weka jina la wasifu huu. - \nMajina yanaweza kuwa na herufi pekee. Jaribu jina lingine? - \nNambari yako ya Siri inapaswa kuwa na urefu wa tarakimu 3. - \nTafadhali hakikisha kwamba Nambari za Siri zote mbili zinalingana. - \nMaelezo zaidi kuhusu Nambari za Siri zenye tarakimu 3. - \nSehemu zilizo na alama ya * zinahitajika. + Majina yanaweza kuwa na herufi pekee. Jaribu jina lingine? + Nambari yako ya Siri inapaswa kuwa na urefu wa tarakimu 3. + Tafadhali hakikisha kwamba Nambari za Siri zote mbili zinalingana. + Maelezo zaidi kuhusu Nambari za Siri zenye tarakimu 3. + Sehemu zilizo na alama ya * zinahitajika. Picha ya sasa ya wasifu - \nHariri picha ya wasifu - \nKaribu kwa %s! + Hariri picha ya wasifu + Karibu kwa %s! Jifunze chochote unachotaka kwa njia bora na ya kufurahisha. Ongeza watumiaji kwenye akaunti yako. Elezea uzoefu na uunde hadi wasifu 10. - \nPakua ya nje ya mtandao. + Pakua ya nje ya mtandao. Endelea kujifunza masomo yako bila muunganisho wa mtandao. - \nFurahia! + Furahia! Furahia matukio yako ya kujifunza kwa masomo yetu ya bure na ya ufanisi. Ruka Inayofuata Anza - \nSlaidi %s ya %s - \nHabari, %s! + Slaidi %s ya %s + Habari, %s! Tafadhali weka Nambari yako ya Siri ya Msimamizi. - \nTafadhali weka Nambari yako ya Siri. - \nNambari ya Siri ya Tarakimu 5 ya Msimamizi. - \nNambari ya Siri ya Tarakimu 3 ya Mtumiaji. + Tafadhali weka Nambari yako ya Siri. + Nambari ya Siri ya Tarakimu 5 ya Msimamizi. + Nambari ya Siri ya Tarakimu 3 ya Mtumiaji. Nilisahau nambari yangu. Nambari ya Siri si sahihi. Onyesha ficha Funga - \nMabadiliko ya Nambari ya Siri yamefaulu + Mabadiliko ya Nambari ya Siri yamefaulu Je, umesahau Nambari ya Siri? Ili kuweka upya Nambari yako ya Siri, tafadhali ondoa %s kisha uisakinishe upya.\n\nKumbuka kwamba ikiwa kifaa hakijakuwa mtandaoni, unaweza kupoteza maendeleo ya mtumiaji kwenye akaunti nyingi. Nenda kwenye hifadhi ya Google play. - \nOnyesha/Ficha ishara ya nenosiri - \nIshara ya nenosiri iliyoonyeshwa - \nIshara ya nenosiri iliyofichwa - \nWeka Nambari yako ya Siri - \nWeka Nambari ya Siri + Onyesha/Ficha ishara ya nenosiri + Ishara ya nenosiri iliyoonyeshwa + Ishara ya nenosiri iliyofichwa + Weka Nambari yako ya Siri + Weka Nambari ya Siri Nambari ya Siri ya Msimamizi Ufikiaji wa mipangilio ya msimamizi - \nNambari ya Siri ya Msimamizi inahitajika ili kubadilisha Nambari ya Siri ya mtumiaji + Nambari ya Siri ya Msimamizi inahitajika ili kubadilisha Nambari ya Siri ya mtumiaji Ghairi Wasilisha Nambari ya Siri ya msimamizi si Sahihi. Tafadhali jaribu tena. - \nNambari ya Siri mpya ya %1$s. - \nWeka Nambari mpya ya Siri - \nVipakuliwa Vyangu - \nVipakuliwa + Nambari ya Siri mpya ya %1$s. + Weka Nambari mpya ya Siri + Vipakuliwa Vyangu + Vipakuliwa Sasisho (2) Je, ungependa kuondoka kwenye wasifu wako? Ghairi Toka Mwanzo - \nWasifu + Wasifu Ilitengenezwa kwa %s - \nMara ya mwisho kutumika + Mara ya mwisho kutumika Badilisha jina Weka upya Nambari ya Siri Ufutaji wa Wasifu @@ -285,8 +285,8 @@ Maendeleo yote yatafutwa na hayawezi kurejeshwa. Futa Ghairi - \nRuhusu Ufikiaji wa Upakuaji - \nMtumiaji anaweza kupakua na kufuta maudhui bila Nambari ya Siri ya msimamizi. + Ruhusu Ufikiaji wa Upakuaji + Mtumiaji anaweza kupakua na kufuta maudhui bila Nambari ya Siri ya msimamizi. Picha ya Wasifu Picha ya Wasifu Ghairi @@ -297,51 +297,51 @@ Hifadhi Weka upya Nambari ya Siri Weka Nambari mpya ya Siri ili mtumiaji aiweke anapofikia wasifu wake. - \nNambari ya Siri ya tarakimu 3* - \nNambari ya Siri ya tarakimu 5* + Nambari ya Siri ya tarakimu 3* + Nambari ya Siri ya tarakimu 5* Thibitisha Nambari ya Siri ya tarakimu 3* Thibitisha Nambari ya Siri ya tarakimu 5 - \nNambari yako ya Siri inapaswa kuwa na urefu wa tarakimu 3. - \nNambari yako ya Siri inapaswa kuwa na urefu wa tarakimu 5. + Nambari yako ya Siri inapaswa kuwa na urefu wa tarakimu 3. + Nambari yako ya Siri inapaswa kuwa na urefu wa tarakimu 5. Unda Nambari ya Siri ya tarakimu 3 - \nInahitajika - \nKitufe cha Nyuma + Inahitajika + Kitufe cha Nyuma Inayofuata Jumla - \nHariri akaunti - \nUsimamizi wa Wasifu + Hariri akaunti + Usimamizi wa Wasifu Hariri wasifu - \nRuhusa ya kupakua + Ruhusa ya kupakua Pakua na usasishe kwenye Wi-fi pekee Mada zitapakuliwa na kusasishwa kwenye Wi-fi pekee. Vipakuliwa au masasisho yoyote ya data ya mtandao wa simu yatawekwa kwenye foleni. Sasisha mada moja kwa moja - \nMada zilizopakuliwa ambazo zina maudhui mapya zinazopatikana zitasasishwa moja kwa moja. + Mada zilizopakuliwa ambazo zina maudhui mapya zinazopatikana zitasasishwa moja kwa moja. Maelezo ya programu. Toleo la Programu - \nVitendo vya akaunti + Vitendo vya akaunti Toka Ghairi Sawa Je,una uhakika unataka kutoka kwenye wasifu wako? - \nToleo la Programu %s + Toleo la Programu %s Sasisho la mwisho lilisakinishwa kwenye %s. Tumia nambari ya toleo iliyo hapo juu kutuma maoni kuhusu hitilafu. Toleo la Programu Lugha ya Programu Lugha chaguomsingi ya Sauti Ukubwa wa Maandishi ya Kusoma Ukubwa wa Maandishi ya Kusoma - \nNakala ya hadithi itaonekana hivi. + Nakala ya hadithi itaonekana hivi. A - \nSauti Chaguomsingi + Sauti Chaguomsingi Lugha ya Programu Ukubwa wa Maandishi ya Kusoma Ndogo Kati Kubwa - \nKubwa Zaidi - \nTelezesha upau wa utafutaji ili kudhibiti ukubwa wa maandishi. - \nWasifu - \nHadithi 2 + Kubwa Zaidi + Telezesha upau wa utafutaji ili kudhibiti ukubwa wa maandishi. + Wasifu + Hadithi 2 Mada zinazoendelea Mada zinazoendelea Hadithi Zimekamilika @@ -349,43 +349,43 @@ Chaguzi Hadithi Zimekamilika Mwongozo wa programu - \nJifunze ujuzi mpya wa hesabu katika hadithi zinazokuonyesha jinsi ya kuzitumia katika maisha yako ya kila siku - \n\"Karibu %s!\" + Jifunze ujuzi mpya wa hesabu katika hadithi zinazokuonyesha jinsi ya kuzitumia katika maisha yako ya kila siku + \"Karibu %s!\" Unataka kujifunza nini? - \nMakuu + Makuu Hebu \ntuanze. Ndiyo - \nHapana… - \nChagua \nmada tofauti. + Hapana… + Chagua \nmada tofauti. Je, unavutiwa na:\n%s? Kidokezo kipya kinapatikana - \nOnyesha vidokezo na suluhisho + Onyesha vidokezo na suluhisho Nenda juu Vidokezo Fichua Suluhisho - \nFichua Kidokezo + Fichua Kidokezo Onyesha/Ficha orodha ya vidokezo ya %s - \nOnyesha/Ficha suluhisho + Onyesha/Ficha suluhisho Suluhisho pekee ni: Hii itafichua suluhisho. Una uhakika? Fichua sasa hivi - \nhivi karibuni + hivi karibuni %s iliyopita - \njana + jana Rudi kwenye mada Ufafanuzi: - \nIkiwa vitu viwili ni sawa, viunganishe. + Ikiwa vitu viwili ni sawa, viunganishe. Unganisha na kipengee %s Tenganisha vipengee katika %s Hamisha kipengee chini hadi %s - \nHamisha kipengee juu hadi %s + Hamisha kipengee juu hadi %s Juu - \nChini + Chini %s %s dakika - dakika %s + dakika %s saa @@ -396,16 +396,16 @@ siku %s mada_marudio_mtazamo wa kuchakata tena_tag - \nInaendelea_kuchakata tena_mtazamo_tag - \nTafadhali chagua angalau chaguo moja. - \nToleo la programu lisilotumika + Inaendelea_kuchakata tena_mtazamo_tag + Tafadhali chagua angalau chaguo moja. + Toleo la programu lisilotumika Toleo hili la programu halitumiki tena. Tafadhali isasishe kupitia hifadhi ya michezo. Funga programu - \nkwa - \nWeka uwiano katika fomu x:y. + kwa + Weka uwiano katika fomu x:y. Maandishi madogo zaidi Maandishi kubwa zaidi - \nInakuja Hivi Karibuni + Inakuja Hivi Karibuni Hadithi Zinazopendekezwa Hadithi Kwa Ajili Yako Hali ya Mazoezi @@ -417,28 +417,28 @@ Jibu sahihi lililowasilishwa Jibu sahihi lililowasilishwa: %s Jibu lililowasilishwa lisilo sahihi - \nJibu lililowasilishwa lisilo sahihi: %s + Jibu lililowasilishwa lisilo sahihi: %s Tegemeo ya mhusika wa tatu toleo %s Leseni za Hakimiliki - \nMtazamo wa Leseni ya Hakimiliki - \nNenda nyuma hadi %s - \norodha ya tegemezi ya mhusika wa tatu + Mtazamo wa Leseni ya Hakimiliki + Nenda nyuma hadi %s + orodha ya tegemezi ya mhusika wa tatu orodha ya leseni za hakimiliki - \nRejesha Somo + Rejesha Somo Endelea - \nAnza tena - \nHabari ya asubuhi, - \nHabari ya mchana, + Anza tena + Habari ya asubuhi, + Habari ya mchana, Habari ya jioni, - \nNinawezaje kuunda wasifu mpya? - \nNinawezaje kufuta wasifu? + Ninawezaje kuunda wasifu mpya? + Ninawezaje kufuta wasifu? Nitabadilisha aje barua pepe/nambari yangu ya simu? - \n%s ni nini? + %s ni nini? Msimamizi ni nani? - \nKwa nini kicheza Uchunguzi hakipakii? + Kwa nini kicheza Uchunguzi hakipakii? Kwa nini sauti yangu haichezwi? - \nNitapakua aje Mada? + Nitapakua aje Mada? Sijapata swali langu hapa. Nini sasa? <p>Ikiwa ni mara yako ya kwanza kuunda wasifu na huna Nambari ya Siri: </p> <p> 1. Kutoka kwa Kichagua Wasifu, gusa <strong>Weka Wasifu Nyingi</strong>. </p> <p> 2. Unda Nambari ya Siri na <strong>Hifadhi</strong>. </p> <p> 3. Jaza sehemu zote za wasifu. </p> <ol> <li> (Si lazima) Pakia picha. </li> <li> Weka jina. </li> <li> (Si lazima) Weka Nambari ya Siri yenye tarakimu 3. </li> </ol> <p> 4. Gusa <strong>Unda</strong>. Wasifu huu umeongezwa kwa Kichagua Wasifu wako! <br/> <br/> Ikiwa umeunda wasifu hapo awali na una Nambari ya Siri: </p> <p> 1. Kutoka kwa Kichagua Wasifu, gusa <strong>Ongeza Wasifu</strong>. </p> <p> 2. Weka Nambari yako ya Siri na uguse <strong>Wasilisha</strong>. </p> <p> 3. Jaza sehemu zote za wasifu. </p> <ol> <li> (Si lazima) Pakia picha. </li> <li> Weka jina. </li> <li> (Si lazima) Weka Nambari ya Siri yenye tarakimu 3. </li> </ol> <p> 4. Gusa <strong>Unda</strong>. Wasifu huu umeongezwa kwa Kichagua Wasifu wako! <br/> <br/> Kumbuka: <u>Msimamizi pekee</u> ndiye anayeweza kudhibiti wasifu.</p> <p>Wasifu unapofutwa:</p> <p><br></p> <p> <li> Wasifu hauwezi kurejeshwa. </li> </p> <p> <li> Taarifa ya wasifu kama vile jina, picha na maendeleo yatafutwa kabisa. </li> </p> <p><br></p> <p>Ili kufuta wasifu (bila kujumuisha <u>Msimamizi</u>):</p> <p>1. Kutoka kwa Ukurasa wa Mwanzo wa Msimamizi, gusa kitufe cha menyu kilicho upande wa juu kushoto.</p> <p>2. Gusa <strong>Vidhibiti vya Msimamizi</strong>.</p> <p>3. Gusa <strong>Hariri Wasifu</strong>.</p> <p>4. Gonga Wasifu ambao ungependa kufuta.</p> <p>5. Katika sehemu ya chini ya skrini, gusa <strong>Ufutaji wa Wasifu</strong>.</p> <p>6. Gusa <strong>Futa</strong> ili kuthibitisha kufuta.</p><p><br></p><p>Kumbuka: <u>Msimamizi</u> pekee ndiye anayeweza kudhibiti wasifu.</ p> diff --git a/app/src/main/res/values/untranslated_strings.xml b/app/src/main/res/values/untranslated_strings.xml index a0482e4ba72..788c6d3de50 100644 --- a/app/src/main/res/values/untranslated_strings.xml +++ b/app/src/main/res/values/untranslated_strings.xml @@ -79,4 +79,7 @@ Please connect to a WiFi or Cellular network in order to upload profile data. %s installation ID %s\'s learner ID + + Switch lesson to Swahili + Switch lesson to English From f2cbc0d0e720ba355538af172cfec3fe0dceff8c Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 24 Aug 2022 13:38:51 -0500 Subject: [PATCH 02/38] Expand concept card support. This adds support for opening concept cards from hints, solutions, and other concept cards. --- .../HintsAndSolutionDialogFragment.kt | 12 +- ...HintsAndSolutionDialogFragmentPresenter.kt | 45 ++++-- .../player/exploration/ExplorationActivity.kt | 9 +- .../testing/StateFragmentTestActivity.kt | 8 +- .../ConceptCardFragmentPresenter.kt | 44 ++++-- .../questionplayer/QuestionPlayerActivity.kt | 31 +--- .../QuestionPlayerActivityPresenter.kt | 41 ++++++ app/src/main/res/layout/hints_summary.xml | 3 +- app/src/main/res/layout/solution_summary.xml | 3 +- .../conceptcard/ConceptCardFragmentTest.kt | 136 +++++++++++------- .../player/state/StateFragmentLocalTest.kt | 103 +++++++++++++ domain/src/main/assets/skills.json | 2 +- domain/src/main/assets/skills.textproto | 2 +- domain/src/main/assets/test_exp_id_2.json | 4 +- .../src/main/assets/test_exp_id_2.textproto | 4 +- .../android/util/caching/CachingModule.kt | 2 +- 16 files changed, 328 insertions(+), 121 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt index 0552263f5a4..fb803de04cc 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt @@ -15,6 +15,7 @@ import org.oppia.android.util.extensions.getProto import org.oppia.android.util.extensions.getStringFromBundle import org.oppia.android.util.extensions.putProto import javax.inject.Inject +import org.oppia.android.app.model.ProfileId private const val CURRENT_EXPANDED_ITEMS_LIST_SAVED_KEY = "HintsAndSolutionDialogFragment.current_expanded_list_index" @@ -47,6 +48,8 @@ class HintsAndSolutionDialogFragment : internal const val HELP_INDEX_KEY = "HintsAndSolutionDialogFragment.help_index" internal const val WRITTEN_TRANSLATION_CONTEXT_KEY = "HintsAndSolutionDialogFragment.written_translation_context" + internal const val PROFILE_ID_KEY = + "HintsAndSolutionDialogFragment.profile_id" /** * Creates a new instance of a DialogFragment to display hints and solution @@ -57,13 +60,15 @@ class HintsAndSolutionDialogFragment : * @param helpIndex the [HelpIndex] corresponding to the current hints/solution configuration * @param writtenTranslationContext the [WrittenTranslationContext] needed to translate the * hints/solution + * @param profileId the ID of the profile viewing the hint/solution * @return [HintsAndSolutionDialogFragment]: DialogFragment */ fun newInstance( id: String, state: State, helpIndex: HelpIndex, - writtenTranslationContext: WrittenTranslationContext + writtenTranslationContext: WrittenTranslationContext, + profileId: ProfileId ): HintsAndSolutionDialogFragment { return HintsAndSolutionDialogFragment().apply { arguments = Bundle().apply { @@ -71,6 +76,7 @@ class HintsAndSolutionDialogFragment : putProto(STATE_KEY, state) putProto(HELP_INDEX_KEY, helpIndex) putProto(WRITTEN_TRANSLATION_CONTEXT_KEY, writtenTranslationContext) + putProto(PROFILE_ID_KEY, profileId) } } } @@ -114,6 +120,7 @@ class HintsAndSolutionDialogFragment : val helpIndex = args.getProto(HELP_INDEX_KEY, HelpIndex.getDefaultInstance()) val writtenTranslationContext = args.getProto(WRITTEN_TRANSLATION_CONTEXT_KEY, WrittenTranslationContext.getDefaultInstance()) + val profileId = args.getProto(PROFILE_ID_KEY, ProfileId.getDefaultInstance()) return hintsAndSolutionDialogFragmentPresenter.handleCreateView( inflater, @@ -127,7 +134,8 @@ class HintsAndSolutionDialogFragment : index, isHintRevealed, solutionIndex, - isSolutionRevealed + isSolutionRevealed, + profileId ) } diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt index c7c56b44972..33ed5eb9878 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt @@ -25,6 +25,8 @@ import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType import org.oppia.android.util.parser.html.HtmlParser import java.lang.IllegalStateException import javax.inject.Inject +import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.topic.conceptcard.ConceptCardFragment const val TAG_REVEAL_SOLUTION_DIALOG = "REVEAL_SOLUTION_DIALOG" @@ -37,7 +39,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( @DefaultResourceBucketName private val resourceBucketName: String, @ExplorationHtmlParserEntityType private val entityType: String, private val resourceHandler: AppLanguageResourceHandler -) { +) : HtmlParser.CustomOppiaTagActionListener { private var index: Int? = null private var expandedItemsList = ArrayList() @@ -49,6 +51,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( private lateinit var state: State private lateinit var helpIndex: HelpIndex private lateinit var writtenTranslationContext: WrittenTranslationContext + private lateinit var profileId: ProfileId private lateinit var itemList: List private lateinit var bindingAdapter: BindableAdapter @@ -72,7 +75,8 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( index: Int?, isHintRevealed: Boolean?, solutionIndex: Int?, - isSolutionRevealed: Boolean? + isSolutionRevealed: Boolean?, + profileId: ProfileId ): View { binding = HintsAndSolutionFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) @@ -97,6 +101,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( this.state = state this.helpIndex = helpIndex this.writtenTranslationContext = writtenTranslationContext + this.profileId = profileId val newAvailableHintIndex = computeNewAvailableHintIndex(helpIndex) viewModel.newAvailableHintIndex.set(newAvailableHintIndex) @@ -218,9 +223,13 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( resourceBucketName, entityType, hintsViewModel.explorationId.get()!!, - /* imageCenterAlign= */ true + customOppiaTagActionListener = this, + imageCenterAlign = true ).parseOppiaHtml( - hintsViewModel.hintsAndSolutionSummary.get()!!, binding.hintsAndSolutionSummary + hintsViewModel.hintsAndSolutionSummary.get()!!, + binding.hintsAndSolutionSummary, + supportsLinks = true, + supportsConceptCards = true ) if (hintsViewModel.hintCanBeRevealed.get()!!) { @@ -233,7 +242,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( } } - binding.root.setOnClickListener { + binding.expandableHintHeader.setOnClickListener { if (hintsViewModel.isHintRevealed.get()!!) { expandOrCollapseItem(position) } @@ -280,11 +289,19 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( } else { binding.solutionCorrectAnswer.text = solutionViewModel.correctAnswer.get() } - binding.solutionSummary.text = htmlParserFactory.create( - resourceBucketName, entityType, viewModel.explorationId.get()!!, /* imageCenterAlign= */ true - ).parseOppiaHtml( - solutionViewModel.solutionSummary.get()!!, binding.solutionSummary - ) + binding.solutionSummary.text = + htmlParserFactory.create( + resourceBucketName, + entityType, + viewModel.explorationId.get()!!, + customOppiaTagActionListener = this, + imageCenterAlign = true + ).parseOppiaHtml( + solutionViewModel.solutionSummary.get()!!, + binding.solutionSummary, + supportsLinks = true, + supportsConceptCards = true + ) if (solutionViewModel.solutionCanBeRevealed.get()!!) { binding.root.visibility = View.VISIBLE @@ -293,7 +310,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( } } - binding.root.setOnClickListener { + binding.expandableSolutionHeader.setOnClickListener { if (solutionViewModel.isSolutionRevealed.get()!!) { expandOrCollapseItem(position) } @@ -365,4 +382,10 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( this.solutionIndex = solutionIndex this.isSolutionRevealed = isSolutionRevealed } + + override fun onConceptCardLinkClicked(view: View, skillId: String) { + ConceptCardFragment + .newInstance(skillId, profileId) + .showNow(fragment.childFragmentManager, ConceptCardFragment.CONCEPT_CARD_DIALOG_FRAGMENT_TAG) + } } diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt index 29c46c4c7bf..3fb3feffbe2 100755 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt @@ -22,6 +22,7 @@ import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener import org.oppia.android.app.player.stopplaying.StopStatePlayingSessionWithSavedProgressListener import org.oppia.android.app.topic.conceptcard.ConceptCardListener import javax.inject.Inject +import org.oppia.android.app.model.ProfileId const val TAG_HINTS_AND_SOLUTION_DIALOG = "HINTS_AND_SOLUTION_DIALOG" @@ -41,7 +42,7 @@ class ExplorationActivity : @Inject lateinit var explorationActivityPresenter: ExplorationActivityPresenter - private var internalProfileId: Int = -1 + private lateinit var profileId: ProfileId private lateinit var topicId: String private lateinit var storyId: String private lateinit var explorationId: String @@ -53,7 +54,8 @@ class ExplorationActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) - internalProfileId = intent.getIntExtra(EXPLORATION_ACTIVITY_PROFILE_ID_ARGUMENT_KEY, -1) + val internalProfileId = intent.getIntExtra(EXPLORATION_ACTIVITY_PROFILE_ID_ARGUMENT_KEY, -1) + profileId = ProfileId.newBuilder().apply { internalId = internalProfileId }.build() topicId = checkNotNull(intent.getStringExtra(EXPLORATION_ACTIVITY_TOPIC_ID_ARGUMENT_KEY)) { "Expected $EXPLORATION_ACTIVITY_TOPIC_ID_ARGUMENT_KEY to be in intent extras." } @@ -178,7 +180,8 @@ class ExplorationActivity : explorationId, state, helpIndex, - writtenTranslationContext + writtenTranslationContext, + profileId ) hintsAndSolutionDialogFragment.showNow(supportFragmentManager, TAG_HINTS_AND_SOLUTION_DIALOG) } diff --git a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt index 29cb26a42e4..6a2117728c6 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt @@ -19,6 +19,7 @@ import org.oppia.android.app.player.state.listener.RouteToHintsAndSolutionListen import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener import org.oppia.android.app.player.stopplaying.StopStatePlayingSessionWithSavedProgressListener import javax.inject.Inject +import org.oppia.android.app.model.ProfileId internal const val TEST_ACTIVITY_PROFILE_ID_EXTRA_KEY = "StateFragmentTestActivity.test_activity_profile_id" @@ -46,10 +47,14 @@ class StateFragmentTestActivity : lateinit var stateFragmentTestActivityPresenter: StateFragmentTestActivityPresenter private lateinit var state: State private lateinit var writtenTranslationContext: WrittenTranslationContext + private lateinit var profileId: ProfileId override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) + profileId = ProfileId.newBuilder().apply { + internalId = intent.getIntExtra(TEST_ACTIVITY_PROFILE_ID_EXTRA_KEY, -1) + }.build() stateFragmentTestActivityPresenter.handleOnCreate() } @@ -109,7 +114,8 @@ class StateFragmentTestActivity : explorationId, state, helpIndex, - writtenTranslationContext + writtenTranslationContext, + profileId ) hintsAndSolutionFragment.showNow(supportFragmentManager, TAG_HINTS_AND_SOLUTION_DIALOG) } diff --git a/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt index 28c8d1b8415..5e970d065b0 100644 --- a/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt @@ -26,7 +26,9 @@ class ConceptCardFragmentPresenter @Inject constructor( @DefaultResourceBucketName private val resourceBucketName: String, private val viewModelProvider: ViewModelProvider, private val translationController: TranslationController -) { +) : HtmlParser.CustomOppiaTagActionListener { + private lateinit var profileId: ProfileId + /** * Sets up data binding and toolbar. * Host activity must inherit ConceptCardListener to dismiss this fragment. @@ -37,6 +39,7 @@ class ConceptCardFragmentPresenter @Inject constructor( skillId: String, profileId: ProfileId ): View? { + this.profileId = profileId val binding = ConceptCardFragmentBinding.inflate( inflater, container, @@ -62,18 +65,27 @@ class ConceptCardFragmentPresenter @Inject constructor( } viewModel.conceptCardLiveData.observe( - fragment, - { ephemeralConceptCard -> - val explanationHtml = - translationController.extractString( - ephemeralConceptCard.conceptCard.explanation, - ephemeralConceptCard.writtenTranslationContext - ) - view.text = htmlParserFactory - .create(resourceBucketName, entityType, skillId, imageCenterAlign = true) - .parseOppiaHtml(explanationHtml, view) - } - ) + fragment + ) { ephemeralConceptCard -> + val explanationHtml = + translationController.extractString( + ephemeralConceptCard.conceptCard.explanation, + ephemeralConceptCard.writtenTranslationContext + ) + view.text = + htmlParserFactory.create( + resourceBucketName, + entityType, + skillId, + customOppiaTagActionListener = this, + imageCenterAlign = true + ).parseOppiaHtml( + explanationHtml, + view, + supportsLinks = true, + supportsConceptCards = true + ) + } return binding.root } @@ -85,4 +97,10 @@ class ConceptCardFragmentPresenter @Inject constructor( private fun logConceptCardEvent(skillId: String) { oppiaLogger.logImportantEvent(oppiaLogger.createOpenConceptCardContext(skillId)) } + + override fun onConceptCardLinkClicked(view: View, skillId: String) { + ConceptCardFragment + .newInstance(skillId, profileId) + .showNow(fragment.childFragmentManager, ConceptCardFragment.CONCEPT_CARD_DIALOG_FRAGMENT_TAG) + } } diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt index d4153be4460..4ac9df43fe8 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt @@ -46,9 +46,6 @@ class QuestionPlayerActivity : @Inject lateinit var questionPlayerActivityPresenter: QuestionPlayerActivityPresenter - private lateinit var state: State - private lateinit var writtenTranslationContext: WrittenTranslationContext - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) @@ -105,39 +102,19 @@ class QuestionPlayerActivity : questionPlayerActivityPresenter.revealSolution() } - private fun getHintsAndSolution(): HintsAndSolutionDialogFragment? { - return supportFragmentManager.findFragmentByTag( - TAG_HINTS_AND_SOLUTION_DIALOG - ) as HintsAndSolutionDialogFragment? - } - override fun routeToHintsAndSolution( - questionId: String, + id: String, helpIndex: HelpIndex ) { - if (getHintsAndSolution() == null) { - val hintsAndSolutionDialogFragment = - HintsAndSolutionDialogFragment.newInstance( - questionId, - state, - helpIndex, - writtenTranslationContext - ) - hintsAndSolutionDialogFragment.showNow(supportFragmentManager, TAG_HINTS_AND_SOLUTION_DIALOG) - } + questionPlayerActivityPresenter.routeToHintsAndSolution(id, helpIndex) } - override fun dismiss() { - getHintsAndSolution()?.dismiss() - } + override fun dismiss() = questionPlayerActivityPresenter.dismissHintsAndSolutionDialog() override fun onQuestionStateLoaded( state: State, writtenTranslationContext: WrittenTranslationContext - ) { - this.state = state - this.writtenTranslationContext = writtenTranslationContext - } + ) = questionPlayerActivityPresenter.loadQuestionState(state, writtenTranslationContext) override fun dismissConceptCard() { questionPlayerActivityPresenter.dismissConceptCard() diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt index 6e58b27a68e..2d37b395747 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt @@ -12,6 +12,11 @@ import org.oppia.android.domain.question.QuestionTrainingController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.app.hintsandsolution.HintsAndSolutionDialogFragment +import org.oppia.android.app.model.HelpIndex +import org.oppia.android.app.model.State +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.app.player.exploration.TAG_HINTS_AND_SOLUTION_DIALOG const val TAG_QUESTION_PLAYER_FRAGMENT = "TAG_QUESTION_PLAYER_FRAGMENT" private const val TAG_HINTS_AND_SOLUTION_QUESTION_MANAGER = "HINTS_AND_SOLUTION_QUESTION_MANAGER" @@ -24,6 +29,8 @@ class QuestionPlayerActivityPresenter @Inject constructor( private val oppiaLogger: OppiaLogger ) { private lateinit var profileId: ProfileId + private lateinit var state: State + private lateinit var writtenTranslationContext: WrittenTranslationContext fun handleOnCreate(profileId: ProfileId) { this.profileId = profileId @@ -155,6 +162,30 @@ class QuestionPlayerActivityPresenter @Inject constructor( ) as QuestionPlayerFragment? } + fun loadQuestionState(state: State, writtenTranslationContext: WrittenTranslationContext) { + this.state = state + this.writtenTranslationContext = writtenTranslationContext + } + + fun routeToHintsAndSolution( + questionId: String, + helpIndex: HelpIndex + ) { + if (getHintsAndSolutionDialogFragment() == null) { + val hintsAndSolutionDialogFragment = + HintsAndSolutionDialogFragment.newInstance( + questionId, + state, + helpIndex, + writtenTranslationContext, + profileId + ) + hintsAndSolutionDialogFragment.showNow( + activity.supportFragmentManager, TAG_HINTS_AND_SOLUTION_DIALOG + ) + } + } + fun revealHint(hintIndex: Int) { val questionPlayerFragment = activity.supportFragmentManager.findFragmentByTag( @@ -171,5 +202,15 @@ class QuestionPlayerActivityPresenter @Inject constructor( questionPlayerFragment.revealSolution() } + fun dismissHintsAndSolutionDialog() { + getHintsAndSolutionDialogFragment()?.dismiss() + } + fun dismissConceptCard() = getQuestionPlayerFragment()?.dismissConceptCard() + + private fun getHintsAndSolutionDialogFragment(): HintsAndSolutionDialogFragment? { + return activity.supportFragmentManager.findFragmentByTag( + TAG_HINTS_AND_SOLUTION_DIALOG + ) as? HintsAndSolutionDialogFragment + } } diff --git a/app/src/main/res/layout/hints_summary.xml b/app/src/main/res/layout/hints_summary.xml index c91b9d98052..c378745d90a 100644 --- a/app/src/main/res/layout/hints_summary.xml +++ b/app/src/main/res/layout/hints_summary.xml @@ -45,7 +45,8 @@ + android:layout_height="wrap_content" + android:id="@+id/expandable_hint_header"> + android:layout_height="wrap_content" + android:id="@+id/expandable_solution_header"> = hasClickableSpanWithText(text) + + override fun perform(uiController: UiController?, view: View?) { + // The view shouldn't be null if the constraints are being met. + (view as? TextView)?.getClickableSpans()?.findMatchingTextOrNull(text)?.onClick(view) + } + } + } + + private fun hasClickableSpanWithText(text: String): Matcher { + return object : TypeSafeMatcher(TextView::class.java) { + override fun describeTo(description: Description?) { + description?.appendText("has ClickableSpan with text")?.appendValue(text) + } + + override fun matchesSafely(item: View?): Boolean { + return (item as? TextView)?.getClickableSpans()?.findMatchingTextOrNull(text) != null + } + } + } + + private fun TextView.getClickableSpans(): List> { + val viewText = text + return (viewText as Spannable).getSpans( + /* start= */ 0, /* end= */ text.length, ClickableSpan::class.java + ).map { + viewText.subSequence(viewText.getSpanStart(it), viewText.getSpanEnd(it)).toString() to it + } + } + + private fun List>.findMatchingTextOrNull(text: String) = + find { text in it.first }?.second + @Module class TestModule { @Provides diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index 5589132a0f7..0f4b243d1cf 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -163,6 +163,10 @@ import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton +import android.text.style.ClickableSpan +import androidx.test.espresso.UiController +import android.text.Spannable +import android.widget.TextView /** * Tests for [StateFragment] that can only be run locally, e.g. using Robolectric, and not on an @@ -1740,6 +1744,49 @@ class StateFragmentLocalTest { } } + @Test + fun testStateFragment_openHint_clickConceptCardLink_opensConceptCard() { + launchForExploration(TEST_EXPLORATION_ID_2).use { + startPlayingExploration() + clickContinueButton() + submitTwoWrongAnswersToTestExpState2() + openHintsAndSolutionsDialog() + pressRevealHintButton(hintPosition = 0) + + // Click on the link for opening the concept card. + onView(withId(R.id.hints_and_solution_summary)) + .inRoot(isDialog()) + .perform(openClickableSpan("test_skill_id_1 concept card")) + testCoroutineDispatchers.runCurrent() + + onView(withText("Concept Card")).inRoot(isDialog()).check(matches(isDisplayed())) + onView(withId(R.id.concept_card_heading_text)) + .inRoot(isDialog()) + .check(matches(withText("Another important skill"))) + } + } + + @Test + fun testStateFragment_openSolution_clickConceptCardLink_opensConceptCard() { + launchForExploration(TEST_EXPLORATION_ID_2).use { scenario -> + startPlayingExploration() + clickContinueButton() + produceAndViewNextHint(hintPosition = 0) { submitTwoWrongAnswersToTestExpState2() } + produceAndViewSolution(scenario) { submitWrongAnswerToTestExpState2() } + + // Click on the link for opening the concept card. + onView(withId(R.id.solution_summary)) + .inRoot(isDialog()) + .perform(openClickableSpan("test_skill_id_1 concept card")) + testCoroutineDispatchers.runCurrent() + + onView(withText("Concept Card")).inRoot(isDialog()).check(matches(isDisplayed())) + onView(withId(R.id.concept_card_heading_text)) + .inRoot(isDialog()) + .check(matches(withText("Another important skill"))) + } + } + private fun createAudioUrl(explorationId: String, audioFileName: String): String { return "https://storage.googleapis.com/oppiaserver-resources/" + "exploration/$explorationId/assets/audio/$audioFileName" @@ -2091,6 +2138,15 @@ class StateFragmentLocalTest { ) } + private fun submitWrongAnswerToTestExpState2() { + submitFractionAnswer(answerText = "1/4") + } + + private fun submitTwoWrongAnswersToTestExpState2() { + submitWrongAnswerToTestExpState2() + submitWrongAnswerToTestExpState2() + } + /** * Causes a hint after the first one to be shown (at approximately the specified recycler view * index for scrolling purposes), and then reveals it and closes the hints & solutions dialog. @@ -2102,6 +2158,16 @@ class StateFragmentLocalTest { closeHintsAndSolutionsDialog() } + private fun produceAndViewSolution( + activityScenario: ActivityScenario, submitAnswer: () -> Unit + ) { + submitAnswer() + testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) + openHintsAndSolutionsDialog() + showRevealSolutionDialog() + clickConfirmRevealSolutionButton(activityScenario) + } + private fun produceAndViewThreeHintsInFractionsState13() { submitWrongAnswerToFractionsState13() produceAndViewNextHint( @@ -2250,6 +2316,43 @@ class StateFragmentLocalTest { }) } + private fun openClickableSpan(text: String): ViewAction { + return object : ViewAction { + override fun getDescription(): String = "openClickableSpan" + + override fun getConstraints(): Matcher = hasClickableSpanWithText(text) + + override fun perform(uiController: UiController?, view: View?) { + // The view shouldn't be null if the constraints are being met. + (view as? TextView)?.getClickableSpans()?.findMatchingTextOrNull(text)?.onClick(view) + } + } + } + + private fun hasClickableSpanWithText(text: String): Matcher { + return object : TypeSafeMatcher(TextView::class.java) { + override fun describeTo(description: Description?) { + description?.appendText("has ClickableSpan with text")?.appendValue(text) + } + + override fun matchesSafely(item: View?): Boolean { + return (item as? TextView)?.getClickableSpans()?.findMatchingTextOrNull(text) != null + } + } + } + + private fun TextView.getClickableSpans(): List> { + val viewText = text + return (viewText as Spannable).getSpans( + /* start= */ 0, /* end= */ text.length, ClickableSpan::class.java + ).map { + viewText.subSequence(viewText.getSpanStart(it), viewText.getSpanEnd(it)).toString() to it + } + } + + private fun List>.findMatchingTextOrNull(text: String) = + find { text in it.first }?.second + // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/domain/src/main/assets/skills.json b/domain/src/main/assets/skills.json index 8bf249793b0..871db18559a 100644 --- a/domain/src/main/assets/skills.json +++ b/domain/src/main/assets/skills.json @@ -130,7 +130,7 @@ "skill_contents": { "explanation": { "content_id": "explanation", - "html": "Explanation with rich text." + "html": "Explanation with rich text.

Click on this .

" }, "worked_examples": [{ "question": { diff --git a/domain/src/main/assets/skills.textproto b/domain/src/main/assets/skills.textproto index cb1b7396271..7bdd492fcd2 100644 --- a/domain/src/main/assets/skills.textproto +++ b/domain/src/main/assets/skills.textproto @@ -152,7 +152,7 @@ concept_cards { skill_id: "test_skill_id_1" skill_description: "Another important skill" explanation { - html: "Explanation with rich text." + html: "Explanation with rich text.

Click on this .

" content_id: "explanation" } worked_example { diff --git a/domain/src/main/assets/test_exp_id_2.json b/domain/src/main/assets/test_exp_id_2.json index 49e1dfc7495..e52d6fe3f3b 100644 --- a/domain/src/main/assets/test_exp_id_2.json +++ b/domain/src/main/assets/test_exp_id_2.json @@ -444,7 +444,7 @@ "hints": [{ "hint_content": { "content_id": "hint_1", - "html": "

Remember that two halves, when added together, make one whole.

" + "html": "

Remember that two halves, when added together, make one whole.

Click on this .

" } }], "solution": { @@ -457,7 +457,7 @@ }, "explanation": { "content_id": "solution", - "html": "

Half of something has one part in the numerator for every two parts in the denominator.

" + "html": "

Half of something has one part in the numerator for every two parts in the denominator.

Click on this .

" } } }, diff --git a/domain/src/main/assets/test_exp_id_2.textproto b/domain/src/main/assets/test_exp_id_2.textproto index d7882d51023..1392b92423d 100644 --- a/domain/src/main/assets/test_exp_id_2.textproto +++ b/domain/src/main/assets/test_exp_id_2.textproto @@ -682,13 +682,13 @@ states { numerator: 1 } explanation { - html: "

Half of something has one part in the numerator for every two parts in the denominator.

" + html: "

Half of something has one part in the numerator for every two parts in the denominator.

Click on this .

" content_id: "solution" } } hint { hint_content { - html: "

Remember that two halves, when added together, make one whole.

" + html: "

Remember that two halves, when added together, make one whole.

Click on this .

" content_id: "hint_1" } } diff --git a/utility/src/main/java/org/oppia/android/util/caching/CachingModule.kt b/utility/src/main/java/org/oppia/android/util/caching/CachingModule.kt index 0979764b659..172a6f8a979 100644 --- a/utility/src/main/java/org/oppia/android/util/caching/CachingModule.kt +++ b/utility/src/main/java/org/oppia/android/util/caching/CachingModule.kt @@ -19,7 +19,7 @@ class CachingModule { @Provides @LoadLessonProtosFromAssets - fun provideLoadLessonProtosFromAssets(): Boolean = false + fun provideLoadLessonProtosFromAssets(): Boolean = true @Provides @LoadImagesFromAssets From 76d38ed882a9a895c3c0eb81110cbd4e5fd86407 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 24 Aug 2022 13:43:16 -0500 Subject: [PATCH 03/38] Lint fixes. --- .../HintsAndSolutionDialogFragment.kt | 2 +- .../HintsAndSolutionDialogFragmentPresenter.kt | 4 ++-- .../player/exploration/ExplorationActivity.kt | 2 +- .../state/testing/StateFragmentTestActivity.kt | 2 +- .../questionplayer/QuestionPlayerActivity.kt | 2 -- .../QuestionPlayerActivityPresenter.kt | 10 +++++----- .../conceptcard/ConceptCardFragmentTest.kt | 18 +++++++++--------- .../app/player/state/StateFragmentLocalTest.kt | 11 ++++++----- 8 files changed, 25 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt index fb803de04cc..532b1dac6b4 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt @@ -9,13 +9,13 @@ import org.oppia.android.R import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableDialogFragment import org.oppia.android.app.model.HelpIndex +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.State import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.util.extensions.getProto import org.oppia.android.util.extensions.getStringFromBundle import org.oppia.android.util.extensions.putProto import javax.inject.Inject -import org.oppia.android.app.model.ProfileId private const val CURRENT_EXPANDED_ITEMS_LIST_SAVED_KEY = "HintsAndSolutionDialogFragment.current_expanded_list_index" diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt index 33ed5eb9878..e96df7fec97 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt @@ -11,9 +11,11 @@ import org.oppia.android.app.model.HelpIndex.IndexTypeCase.EVERYTHING_REVEALED import org.oppia.android.app.model.HelpIndex.IndexTypeCase.LATEST_REVEALED_HINT_INDEX import org.oppia.android.app.model.HelpIndex.IndexTypeCase.NEXT_AVAILABLE_HINT_INDEX import org.oppia.android.app.model.HelpIndex.IndexTypeCase.SHOW_SOLUTION +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.State import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.recyclerview.BindableAdapter +import org.oppia.android.app.topic.conceptcard.ConceptCardFragment import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.HintsAndSolutionFragmentBinding @@ -25,8 +27,6 @@ import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType import org.oppia.android.util.parser.html.HtmlParser import java.lang.IllegalStateException import javax.inject.Inject -import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.topic.conceptcard.ConceptCardFragment const val TAG_REVEAL_SOLUTION_DIALOG = "REVEAL_SOLUTION_DIALOG" diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt index 3fb3feffbe2..2974ee95b87 100755 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt @@ -13,6 +13,7 @@ import org.oppia.android.app.hintsandsolution.HintsAndSolutionListener import org.oppia.android.app.hintsandsolution.RevealHintListener import org.oppia.android.app.hintsandsolution.RevealSolutionInterface import org.oppia.android.app.model.HelpIndex +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.model.State import org.oppia.android.app.model.WrittenTranslationContext @@ -22,7 +23,6 @@ import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener import org.oppia.android.app.player.stopplaying.StopStatePlayingSessionWithSavedProgressListener import org.oppia.android.app.topic.conceptcard.ConceptCardListener import javax.inject.Inject -import org.oppia.android.app.model.ProfileId const val TAG_HINTS_AND_SOLUTION_DIALOG = "HINTS_AND_SOLUTION_DIALOG" diff --git a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt index 6a2117728c6..a7cc03bd0ac 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt @@ -10,6 +10,7 @@ import org.oppia.android.app.hintsandsolution.HintsAndSolutionListener import org.oppia.android.app.hintsandsolution.RevealHintListener import org.oppia.android.app.hintsandsolution.RevealSolutionInterface import org.oppia.android.app.model.HelpIndex +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.State import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.audio.AudioButtonListener @@ -19,7 +20,6 @@ import org.oppia.android.app.player.state.listener.RouteToHintsAndSolutionListen import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener import org.oppia.android.app.player.stopplaying.StopStatePlayingSessionWithSavedProgressListener import javax.inject.Inject -import org.oppia.android.app.model.ProfileId internal const val TEST_ACTIVITY_PROFILE_ID_EXTRA_KEY = "StateFragmentTestActivity.test_activity_profile_id" diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt index 4ac9df43fe8..bcc747aecb8 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt @@ -5,7 +5,6 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity -import org.oppia.android.app.hintsandsolution.HintsAndSolutionDialogFragment import org.oppia.android.app.hintsandsolution.HintsAndSolutionListener import org.oppia.android.app.hintsandsolution.RevealHintListener import org.oppia.android.app.hintsandsolution.RevealSolutionInterface @@ -13,7 +12,6 @@ import org.oppia.android.app.model.HelpIndex import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.State import org.oppia.android.app.model.WrittenTranslationContext -import org.oppia.android.app.player.exploration.TAG_HINTS_AND_SOLUTION_DIALOG import org.oppia.android.app.player.state.listener.RouteToHintsAndSolutionListener import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener import org.oppia.android.app.player.stopplaying.RestartPlayingSessionListener diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt index 2d37b395747..b9685035387 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt @@ -5,18 +5,18 @@ import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.app.hintsandsolution.HintsAndSolutionDialogFragment +import org.oppia.android.app.model.HelpIndex import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.State +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.app.player.exploration.TAG_HINTS_AND_SOLUTION_DIALOG import org.oppia.android.databinding.QuestionPlayerActivityBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.question.QuestionTrainingController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -import org.oppia.android.app.hintsandsolution.HintsAndSolutionDialogFragment -import org.oppia.android.app.model.HelpIndex -import org.oppia.android.app.model.State -import org.oppia.android.app.model.WrittenTranslationContext -import org.oppia.android.app.player.exploration.TAG_HINTS_AND_SOLUTION_DIALOG const val TAG_QUESTION_PLAYER_FRAGMENT = "TAG_QUESTION_PLAYER_FRAGMENT" private const val TAG_HINTS_AND_SOLUTION_QUESTION_MANAGER = "HINTS_AND_SOLUTION_QUESTION_MANAGER" diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt index 59437fa016c..99f298f2b21 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt @@ -2,18 +2,22 @@ package org.oppia.android.app.topic.conceptcard import android.app.Application import android.content.Context -import android.widget.TextView +import android.text.Spannable +import android.text.style.ClickableSpan import android.view.View +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ActivityScenario -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withId @@ -24,9 +28,12 @@ import dagger.Component import dagger.Module import dagger.Provides import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.Description +import org.hamcrest.Matcher import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.instanceOf import org.hamcrest.Matchers.not +import org.hamcrest.TypeSafeMatcher import org.junit.After import org.junit.Before import org.junit.Rule @@ -114,13 +121,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import android.text.style.ClickableSpan -import androidx.test.espresso.UiController -import androidx.test.espresso.ViewAction -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.TypeSafeMatcher -import android.text.Spannable /** Tests for [ConceptCardFragment]. */ @RunWith(AndroidJUnit4::class) diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index 0f4b243d1cf..e4b1d349632 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -2,8 +2,11 @@ package org.oppia.android.app.player.state import android.app.Application import android.content.Context +import android.text.Spannable +import android.text.style.ClickableSpan import android.view.View import android.view.ViewGroup +import android.widget.TextView import androidx.annotation.IdRes import androidx.appcompat.app.AppCompatActivity import androidx.core.view.children @@ -14,6 +17,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.PerformException +import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAssertion import androidx.test.espresso.action.ViewActions.click @@ -163,10 +167,6 @@ import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton -import android.text.style.ClickableSpan -import androidx.test.espresso.UiController -import android.text.Spannable -import android.widget.TextView /** * Tests for [StateFragment] that can only be run locally, e.g. using Robolectric, and not on an @@ -2159,7 +2159,8 @@ class StateFragmentLocalTest { } private fun produceAndViewSolution( - activityScenario: ActivityScenario, submitAnswer: () -> Unit + activityScenario: ActivityScenario, + submitAnswer: () -> Unit ) { submitAnswer() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) From 94a4140aa5a587fbf4da1473fc571f63fd85f619 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 24 Aug 2022 22:46:30 -0500 Subject: [PATCH 04/38] Revert flag enabled for development. --- .../main/java/org/oppia/android/util/caching/CachingModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/caching/CachingModule.kt b/utility/src/main/java/org/oppia/android/util/caching/CachingModule.kt index 172a6f8a979..0979764b659 100644 --- a/utility/src/main/java/org/oppia/android/util/caching/CachingModule.kt +++ b/utility/src/main/java/org/oppia/android/util/caching/CachingModule.kt @@ -19,7 +19,7 @@ class CachingModule { @Provides @LoadLessonProtosFromAssets - fun provideLoadLessonProtosFromAssets(): Boolean = true + fun provideLoadLessonProtosFromAssets(): Boolean = false @Provides @LoadImagesFromAssets From 19f0e697056f836124afc35927ad8fb854970043 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 12 Jan 2023 14:04:48 -0800 Subject: [PATCH 05/38] Fixes a few things for the upcoming MR6 release. Fixes: - Removed extra spacing around a few Swahili strings (all should now be fixed). - Replaced 'reveal' English wording with 'show'. - Added a new event for logging voiceover pausing. - Updated the play/pause voiceover events to include language codes. Documentation and/or tests may not yet be completed. --- app/src/main/AndroidManifest.xml | 2 +- .../app/player/audio/AudioViewModel.kt | 8 +++--- app/src/main/res/values-sw/strings.xml | 14 +++++----- app/src/main/res/values/strings.xml | 8 +++--- .../domain/audio/AudioPlayerController.kt | 21 ++++++++++++--- .../analytics/LearnerAnalyticsLogger.kt | 26 ++++++++++++++++--- model/src/main/proto/oppia_logger.proto | 10 +++++++ .../util/logging/EventBundleCreator.kt | 9 ++++--- ...entTypeToHumanReadableNameConverterImpl.kt | 1 + ...entTypeToHumanReadableNameConverterImpl.kt | 1 + 10 files changed, 73 insertions(+), 27 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c112106d33..6c3512d1552 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ + android:value="false" /> diff --git a/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt b/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt index 8de03ce0ca3..33708dcebb9 100644 --- a/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt @@ -117,7 +117,7 @@ class AudioViewModel @Inject constructor( val ensuredLanguageCode = if (languages.contains("en")) "en" else languages.first() fallbackLanguageCode = ensuredLanguageCode audioPlayerController.changeDataSource( - voiceOverToUri(voiceoverMap[ensuredLanguageCode]), currentContentId + voiceOverToUri(voiceoverMap[ensuredLanguageCode]), currentContentId, ensuredLanguageCode ) } } @@ -128,20 +128,20 @@ class AudioViewModel @Inject constructor( selectedLanguageCode = languageCode currentLanguageCode.set(languageCode) audioPlayerController.changeDataSource( - voiceOverToUri(voiceoverMap[languageCode]), currentContentId + voiceOverToUri(voiceoverMap[languageCode]), currentContentId, languageCode ) } /** Plays or pauses AudioController depending on passed in state */ fun togglePlayPause(type: UiAudioPlayStatus?) { if (type == UiAudioPlayStatus.PLAYING) { - audioPlayerController.pause() + audioPlayerController.pause(isFromExplicitUserAction = true) } else { audioPlayerController.play(isPlayingFromAutoPlay = false, reloadingMainContent = false) } } - fun pauseAudio() = audioPlayerController.pause() + fun pauseAudio() = audioPlayerController.pause(isFromExplicitUserAction = false) fun handleSeekTo(position: Int) = audioPlayerController.seekTo(position) fun handleRelease() = audioPlayerController.releaseMediaPlayer() diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index 3a7be1bece1..06b45cdc46c 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -158,7 +158,7 @@ Idadi ya masharti si sawa na masharti yanayohitajika. Uwiano hauwezi kuwa na 0 kama kipengele. Ukubwa usiojulikana - Baiti %s + Baiti %s %s KB %s MB %s GB @@ -166,20 +166,20 @@ Mada: %s %1$s katika %2$s - Sura 1\n - \n Sura %s \n + Sura 1 + Sura %s - Hadithi 1\n - Hadithi %s\n + Hadithi 1 + Hadithi %s %s kati ya Sura %s Imekamilika %s ya Sura %s Imekamilika - Somo 1\n - Masomo %s \n + Somo 1 + Masomo %s Ukurasa wa kuchagua wasifu Msimamizi diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 073d27dcaf3..c08f86fc988 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -452,13 +452,13 @@ Navigate up Hints Show solution - Reveal Solution + Show Solution Show Hint Hide Hint Hide solution - The only solution is : - This will reveal the solution. Are you sure? - Reveal + The only solution is: + This will show the solution. Are you sure? + Show just now recently diff --git a/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt b/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt index 98940dd6536..b0c30755cfd 100644 --- a/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt +++ b/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt @@ -91,6 +91,7 @@ class AudioPlayerController @Inject constructor( private var duration = 0 private var completed = false private var currentContentId: String? = null + private var currentLanguageCode: String? = null private val SEEKBAR_UPDATE_FREQUENCY = TimeUnit.SECONDS.toMillis(1) @@ -117,10 +118,11 @@ class AudioPlayerController @Inject constructor( * Changes audio source to specified. * Stops sending seek bar updates and put MediaPlayer in preparing state. */ - fun changeDataSource(url: String, contentId: String?) { + fun changeDataSource(url: String, contentId: String?, languageCode: String) { audioLock.withLock { prepared = false currentContentId = contentId + currentLanguageCode = languageCode stopUpdatingSeekBar() mediaPlayer.reset() prepareDataSource(url) @@ -210,7 +212,7 @@ class AudioPlayerController @Inject constructor( if (!isPlayingFromAutoPlay || !reloadingMainContent) { val explorationLogger = learnerAnalyticsLogger.explorationAnalyticsLogger.value val stateLogger = explorationLogger?.stateAnalyticsLogger?.value - stateLogger?.logPlayVoiceOver(currentContentId) + stateLogger?.logPlayVoiceOver(currentContentId, currentLanguageCode) } } } @@ -218,9 +220,14 @@ class AudioPlayerController @Inject constructor( /** * Puts MediaPlayer in paused state and stops sending seek bar updates. - * Controller must already have audio prepared. + * + * The controller must already have audio prepared. + * + * @param isFromExplicitUserAction indicates whether this pause is from an explicit user action + * (like clicking a pause button) vs. an incidental one (like an autoplay transition or + * closing the audio bar) */ - fun pause() { + fun pause(isFromExplicitUserAction: Boolean) { audioLock.withLock { check(prepared) { "Media Player not in a prepared state" } if (mediaPlayer.isPlaying) { @@ -230,6 +237,12 @@ class AudioPlayerController @Inject constructor( ) mediaPlayer.pause() stopUpdatingSeekBar() + + if (isFromExplicitUserAction) { + val explorationLogger = learnerAnalyticsLogger.explorationAnalyticsLogger.value + val stateLogger = explorationLogger?.stateAnalyticsLogger?.value + stateLogger?.logPauseVoiceOver(currentContentId, currentLanguageCode) + } } } } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt index 727b53dc722..d30bf80de40 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt @@ -377,11 +377,27 @@ class LearnerAnalyticsLogger @Inject constructor( /** * Logs that the learner started playing a voice over audio track corresponding to [contentId] - * (or null if something failed when retrieving the content ID--note that this may affect - * whether the event is logged). + * with language code [languageCode] (or null if something failed when retrieving the content ID + * or language code--note that this may affect whether the event is logged). */ - fun logPlayVoiceOver(contentId: String?) { - logStateEvent(contentId, ::createPlayVoiceOverContext, EventBuilder::setPlayVoiceOverContext) + fun logPlayVoiceOver(contentId: String?, languageCode: String?) { + logStateEvent( + contentId, languageCode, ::createPlayVoiceOverContext, EventBuilder::setPlayVoiceOverContext + ) + } + + /** + * Logs that the learner stopped playing a voice over audio track corresponding to [contentId] + * with language code [languageCode] (see [logPlayVoiceOver] for caveats for both [contentId] + * and [languageCode]). + */ + fun logPauseVoiceOver(contentId: String?, languageCode: String?) { + logStateEvent( + contentId, + languageCode, + ::createPlayVoiceOverContext, + EventBuilder::setPauseVoiceOverContext + ) } /** @@ -538,9 +554,11 @@ class LearnerAnalyticsLogger @Inject constructor( private fun createPlayVoiceOverContext( contentId: String?, + languageCode: String?, explorationDetails: ExplorationContext ) = PlayVoiceOverContext.newBuilder().apply { contentId?.let { this.contentId = it } + languageCode?.let { this.languageCode = languageCode } this.explorationDetails = explorationDetails }.build() diff --git a/model/src/main/proto/oppia_logger.proto b/model/src/main/proto/oppia_logger.proto index 6f2a37fdd2f..62f77ee3284 100644 --- a/model/src/main/proto/oppia_logger.proto +++ b/model/src/main/proto/oppia_logger.proto @@ -94,6 +94,9 @@ message EventLog { // The event being logged is related to playing a voiceover. PlayVoiceOverContext play_voice_over_context = 23; + // The event being logged is related to pausing a voiceover. + PlayVoiceOverContext pause_voice_over_context = 35; + // The event being logged is related to backgrounding of the application. LearnerDetailsContext app_in_background_context = 24; @@ -207,8 +210,15 @@ message EventLog { // Defined attributes that are common among other exploration related event log contexts. ExplorationContext exploration_details = 1; + // Not sure if this was ever actually a value, but reserving it to ensure no incompatibilities + // occur. + reserved 2; + // The content id of the voiceover recording being played. string content_id = 3; + + // The language code being played. + string language_code = 4; } // Represents event context which contains learner-specific details that are logged with every diff --git a/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt b/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt index db860e80ed0..ed2fc66b2bd 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt @@ -51,7 +51,7 @@ import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.Em import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.ExplorationContext import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.HintContext import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.LearnerDetailsContext -import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.PlayVoiceOverContext +import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.PlayPauseVoiceOverContext import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.QuestionContext import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.RevisionCardContext import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.SensitiveStringContext @@ -70,6 +70,7 @@ import javax.inject.Inject import javax.inject.Singleton import org.oppia.android.app.model.EventLog.CardContext as CardEventContext import org.oppia.android.app.model.EventLog.ConceptCardContext as ConceptCardEventContext +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PAUSE_VOICE_OVER_CONTEXT import org.oppia.android.app.model.EventLog.ExplorationContext as ExplorationEventContext import org.oppia.android.app.model.EventLog.HintContext as HintEventContext import org.oppia.android.app.model.EventLog.LearnerDetailsContext as LearnerDetailsEventContext @@ -164,7 +165,8 @@ class EventBundleCreator @Inject constructor( SOLUTION_OFFERED_CONTEXT -> ExplorationContext(activityName, solutionOfferedContext) ACCESS_SOLUTION_CONTEXT -> ExplorationContext(activityName, accessSolutionContext) SUBMIT_ANSWER_CONTEXT -> SubmitAnswerContext(activityName, submitAnswerContext) - PLAY_VOICE_OVER_CONTEXT -> PlayVoiceOverContext(activityName, playVoiceOverContext) + PLAY_VOICE_OVER_CONTEXT -> PlayPauseVoiceOverContext(activityName, playVoiceOverContext) + PAUSE_VOICE_OVER_CONTEXT -> PlayPauseVoiceOverContext(activityName, pauseVoiceOverContext) APP_IN_BACKGROUND_CONTEXT -> LearnerDetailsContext(activityName, appInBackgroundContext) APP_IN_FOREGROUND_CONTEXT -> LearnerDetailsContext(activityName, appInForegroundContext) EXIT_EXPLORATION_CONTEXT -> ExplorationContext(activityName, exitExplorationContext) @@ -364,13 +366,14 @@ class EventBundleCreator @Inject constructor( } /** The [EventActivityContext] corresponding to [PlayVoiceOverEventContext]s. */ - class PlayVoiceOverContext( + class PlayPauseVoiceOverContext( activityName: String, value: PlayVoiceOverEventContext ) : EventActivityContext(activityName, value) { override fun PlayVoiceOverEventContext.storeValue(store: PropertyStore) { store.putProperties("exploration_details", explorationDetails, ::ExplorationContext) store.putNonSensitiveValue("content_id", contentId) + store.putNonSensitiveValue("language_code", languageCode) } } diff --git a/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt b/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt index 9c5fd669585..d0ccb5d228e 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt @@ -32,6 +32,7 @@ class KenyaAlphaEventTypeToHumanReadableNameConverterImpl @Inject constructor() ActivityContextCase.ACCESS_SOLUTION_CONTEXT -> "access_solution_context" ActivityContextCase.SUBMIT_ANSWER_CONTEXT -> "submit_answer_context" ActivityContextCase.PLAY_VOICE_OVER_CONTEXT -> "play_voice_over_context" + ActivityContextCase.PAUSE_VOICE_OVER_CONTEXT -> "pause_voice_over_context" ActivityContextCase.APP_IN_BACKGROUND_CONTEXT -> "app_in_background_context" ActivityContextCase.APP_IN_FOREGROUND_CONTEXT -> "app_in_foreground_context" ActivityContextCase.EXIT_EXPLORATION_CONTEXT -> "exit_exploration_context" diff --git a/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt b/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt index 8d0ccb69a04..3008aa9bb76 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt @@ -42,6 +42,7 @@ class StandardEventTypeToHumanReadableNameConverterImpl @Inject constructor() : ActivityContextCase.ACCESS_SOLUTION_CONTEXT -> "reveal_solution" ActivityContextCase.SUBMIT_ANSWER_CONTEXT -> "submit_answer" ActivityContextCase.PLAY_VOICE_OVER_CONTEXT -> "click_play_voiceover_button" + ActivityContextCase.PAUSE_VOICE_OVER_CONTEXT -> "click_pause_voiceover_button" ActivityContextCase.APP_IN_BACKGROUND_CONTEXT -> "send_app_to_background" ActivityContextCase.APP_IN_FOREGROUND_CONTEXT -> "bring_app_to_foreground" ActivityContextCase.EXIT_EXPLORATION_CONTEXT -> "leave_exploration" From b58c7989bcf9761d1d4fd01b1d60211d09ec1163 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 13 Jan 2023 14:32:48 -0800 Subject: [PATCH 06/38] Add mark chapter supported for admins. With the learner study parameter enabled, admins can now force-complete lessons on a per-profile basis for all profiles in the app. This leverages the existing developer-only options menu so this isn't something we would want to incorporate in the long-term without more refinement (none of the UI strings are even translated). --- .../devoptions/DeveloperOptionsActivity.kt | 9 +- .../markchapterscompleted/ChapterSelector.kt | 13 -- .../MarkChaptersCompletedActivity.kt | 17 ++- .../MarkChaptersCompletedActivityPresenter.kt | 6 +- .../MarkChaptersCompletedFragment.kt | 33 +++-- .../MarkChaptersCompletedFragmentPresenter.kt | 124 +++++++++++++----- .../MarkChaptersCompletedTestActivity.kt | 4 +- .../testing/DeveloperOptionsTestActivity.kt | 5 +- .../settings/profile/ProfileEditActivity.kt | 2 +- .../profile/ProfileEditFragmentPresenter.kt | 9 ++ .../settings/profile/ProfileEditViewModel.kt | 7 +- .../main/res/layout/profile_edit_fragment.xml | 22 +++- app/src/main/res/values/strings.xml | 1 + .../main/res/values/untranslated_strings.xml | 4 + 14 files changed, 172 insertions(+), 84 deletions(-) delete mode 100644 app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/ChapterSelector.kt diff --git a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt index 2d6b82eb17b..bf8f287edb7 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt @@ -47,8 +47,9 @@ class DeveloperOptionsActivity : override fun routeToMarkChaptersCompleted() { startActivity( - MarkChaptersCompletedActivity - .createMarkChaptersCompletedIntent(this, internalProfileId) + MarkChaptersCompletedActivity.createMarkChaptersCompletedIntent( + context = this, internalProfileId, showConfirmationNotice = false + ) ) } @@ -86,10 +87,6 @@ class DeveloperOptionsActivity : decorateWithScreenName(DEVELOPER_OPTIONS_ACTIVITY) } } - - fun getIntentKey(): String { - return NAVIGATION_PROFILE_ID_ARGUMENT_KEY - } } override fun forceCrash() { diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/ChapterSelector.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/ChapterSelector.kt deleted file mode 100644 index b5214304acb..00000000000 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/ChapterSelector.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.oppia.android.app.devoptions.markchapterscompleted - -/** Interface to update the selectedChapterList in [MarkChaptersCompletedFragmentPresenter]. */ -interface ChapterSelector { - /** This chapter will get added to selectedTopicList in [MarkChaptersCompletedFragmentPresenter]. */ - fun chapterSelected(chapterIndex: Int, nextStoryIndex: Int, explorationId: String) - - /** - * Chapters from 'chapterIndex' until 'nextStoryIndex' will get removed from selectedTopicList in - * [MarkChaptersCompletedFragmentPresenter]. - */ - fun chapterUnselected(chapterIndex: Int, nextStoryIndex: Int) -} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedActivity.kt index 27997cc5bb0..fcfc194cc96 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedActivity.kt @@ -21,13 +21,13 @@ class MarkChaptersCompletedActivity : InjectableAppCompatActivity() { @Inject lateinit var resourceHandler: AppLanguageResourceHandler - private var internalProfileId = -1 - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) - internalProfileId = intent.getIntExtra(PROFILE_ID_EXTRA_KEY, -1) - markChaptersCompletedActivityPresenter.handleOnCreate(internalProfileId) + val internalProfileId = intent.getIntExtra(PROFILE_ID_EXTRA_KEY, /* defaultValue= */ -1) + val showConfirmationNotice = + intent.getBooleanExtra(SHOW_CONFIRMATION_NOTICE_EXTRA_KEY, /* defaultValue= */ false) + markChaptersCompletedActivityPresenter.handleOnCreate(internalProfileId, showConfirmationNotice) title = resourceHandler.getStringInLocale(R.string.mark_chapters_completed_activity_title) } @@ -39,11 +39,16 @@ class MarkChaptersCompletedActivity : InjectableAppCompatActivity() { } companion object { - const val PROFILE_ID_EXTRA_KEY = "MarkChaptersCompletedActivity.profile_id" + private const val PROFILE_ID_EXTRA_KEY = "MarkChaptersCompletedActivity.profile_id" + private const val SHOW_CONFIRMATION_NOTICE_EXTRA_KEY = + "MarkChaptersCompletedActivity.show_confirmation_notice" - fun createMarkChaptersCompletedIntent(context: Context, internalProfileId: Int): Intent { + fun createMarkChaptersCompletedIntent( + context: Context, internalProfileId: Int, showConfirmationNotice: Boolean + ): Intent { return Intent(context, MarkChaptersCompletedActivity::class.java).apply { putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId) + putExtra(SHOW_CONFIRMATION_NOTICE_EXTRA_KEY, showConfirmationNotice) decorateWithScreenName(MARK_CHAPTERS_COMPLETED_ACTIVITY) } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedActivityPresenter.kt index a2714817cb2..6605456b86e 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedActivityPresenter.kt @@ -11,14 +11,14 @@ class MarkChaptersCompletedActivityPresenter @Inject constructor( private val activity: AppCompatActivity ) { - fun handleOnCreate(internalProfileId: Int) { + fun handleOnCreate(internalProfileId: Int, showConfirmationNotice: Boolean) { activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back_white_24dp) activity.setContentView(R.layout.mark_chapters_completed_activity) if (getMarkChaptersCompletedFragment() == null) { - val markChaptersCompletedFragment = MarkChaptersCompletedFragment - .newInstance(internalProfileId) + val markChaptersCompletedFragment = + MarkChaptersCompletedFragment.newInstance(internalProfileId, showConfirmationNotice) activity.supportFragmentManager.beginTransaction().add( R.id.mark_chapters_completed_container, markChaptersCompletedFragment diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragment.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragment.kt index e36bfbdd844..18f0b3c12b4 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragment.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragment.kt @@ -15,17 +15,22 @@ class MarkChaptersCompletedFragment : InjectableFragment() { lateinit var markChaptersCompletedFragmentPresenter: MarkChaptersCompletedFragmentPresenter companion object { - internal const val PROFILE_ID_ARGUMENT_KEY = - "MarkChaptersCompletedFragment.profile_id" - + private const val PROFILE_ID_ARGUMENT_KEY = "MarkChaptersCompletedFragment.profile_id" + private const val SHOW_CONFIRMATION_NOTICE_ARGUMENT_KEY = + "MarkChaptersCompletedFragment.show_confirmation_notice" private const val EXPLORATION_ID_LIST_ARGUMENT_KEY = "MarkChaptersCompletedFragment.exploration_id_list" + private const val EXPLORATION_TITLE_LIST_ARGUMENT_KEY = + "MarkChaptersCompletedFragment.exploration_title_list" /** Returns a new [MarkChaptersCompletedFragment]. */ - fun newInstance(internalProfileId: Int): MarkChaptersCompletedFragment { + fun newInstance( + internalProfileId: Int, showConfirmationNotice: Boolean + ): MarkChaptersCompletedFragment { val markChaptersCompletedFragment = MarkChaptersCompletedFragment() val args = Bundle() args.putInt(PROFILE_ID_ARGUMENT_KEY, internalProfileId) + args.putBoolean(SHOW_CONFIRMATION_NOTICE_ARGUMENT_KEY, showConfirmationNotice) markChaptersCompletedFragment.arguments = args return markChaptersCompletedFragment } @@ -43,18 +48,16 @@ class MarkChaptersCompletedFragment : InjectableFragment() { ): View? { val args = checkNotNull(arguments) { "Expected arguments to be passed to MarkChaptersCompletedFragment" } - val internalProfileId = args - .getInt(PROFILE_ID_ARGUMENT_KEY, -1) - var selectedExplorationIdList = ArrayList() - if (savedInstanceState != null) { - selectedExplorationIdList = - savedInstanceState.getStringArrayList(EXPLORATION_ID_LIST_ARGUMENT_KEY)!! - } + val internalProfileId = args.getInt(PROFILE_ID_ARGUMENT_KEY, /* defaultValue= */ -1) + val showConfirmationNotice = + args.getBoolean(SHOW_CONFIRMATION_NOTICE_ARGUMENT_KEY, /* defaultValue= */ false) return markChaptersCompletedFragmentPresenter.handleCreateView( inflater, container, internalProfileId, - selectedExplorationIdList + showConfirmationNotice, + savedInstanceState?.getStringArrayList(EXPLORATION_ID_LIST_ARGUMENT_KEY) ?: listOf(), + savedInstanceState?.getStringArrayList(EXPLORATION_TITLE_LIST_ARGUMENT_KEY) ?: listOf() ) } @@ -62,7 +65,11 @@ class MarkChaptersCompletedFragment : InjectableFragment() { super.onSaveInstanceState(outState) outState.putStringArrayList( EXPLORATION_ID_LIST_ARGUMENT_KEY, - markChaptersCompletedFragmentPresenter.selectedExplorationIdList + markChaptersCompletedFragmentPresenter.serializableSelectedExplorationIds + ) + outState.putStringArrayList( + EXPLORATION_TITLE_LIST_ARGUMENT_KEY, + markChaptersCompletedFragmentPresenter.serializableSelectedExplorationTitles ) } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt index 6c6e46c8266..140f2f47b11 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt @@ -3,6 +3,7 @@ package org.oppia.android.app.devoptions.markchapterscompleted import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager @@ -15,6 +16,8 @@ import org.oppia.android.databinding.MarkChaptersCompletedFragmentBinding import org.oppia.android.databinding.MarkChaptersCompletedStorySummaryViewBinding import org.oppia.android.domain.devoptions.ModifyLessonProgressController import javax.inject.Inject +import org.oppia.android.R +import org.oppia.android.app.translation.AppLanguageResourceHandler /** The presenter for [MarkChaptersCompletedFragment]. */ @FragmentScope @@ -23,19 +26,29 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( private val fragment: Fragment, private val viewModel: MarkChaptersCompletedViewModel, private val modifyLessonProgressController: ModifyLessonProgressController, - private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory -) : ChapterSelector { + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory, + private val resourceHandler: AppLanguageResourceHandler +) { private lateinit var binding: MarkChaptersCompletedFragmentBinding private lateinit var linearLayoutManager: LinearLayoutManager private lateinit var bindingAdapter: BindableAdapter - lateinit var selectedExplorationIdList: ArrayList private lateinit var profileId: ProfileId + private lateinit var alertDialog: AlertDialog + private val selectedExplorationIds = mutableListOf() + private val selectedExplorationTitles = mutableListOf() + + val serializableSelectedExplorationIds: ArrayList + get() = ArrayList(selectedExplorationIds) + val serializableSelectedExplorationTitles: ArrayList + get() = ArrayList(selectedExplorationTitles) fun handleCreateView( inflater: LayoutInflater, container: ViewGroup?, internalProfileId: Int, - selectedExplorationIdList: ArrayList + showConfirmationNotice: Boolean, + selectedExplorationIds: List, + selectedExplorationTitles: List ): View? { binding = MarkChaptersCompletedFragmentBinding.inflate( inflater, @@ -52,7 +65,8 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( this.viewModel = this@MarkChaptersCompletedFragmentPresenter.viewModel } - this.selectedExplorationIdList = selectedExplorationIdList + this.selectedExplorationIds += selectedExplorationIds + this.selectedExplorationTitles += selectedExplorationTitles profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() viewModel.setProfileId(profileId) @@ -74,7 +88,8 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( chapterSelected( viewModel.chapterIndex, viewModel.nextStoryIndex, - viewModel.chapterSummary.explorationId + viewModel.chapterSummary.explorationId, + viewModel.chapterTitle ) } } @@ -91,11 +106,9 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( } binding.markChaptersCompletedMarkCompletedTextView.setOnClickListener { - modifyLessonProgressController.markMultipleChaptersCompleted( - profileId = profileId, - chapterMap = viewModel.getChapterMap().filterKeys { selectedExplorationIdList.contains(it) } - ) - activity.finish() + if (showConfirmationNotice) { + showConfirmationDialog() + } else markChaptersAsCompleted() } return binding.root @@ -140,21 +153,19 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( binding.isChapterChecked = true binding.isChapterCheckboxEnabled = false } else { - binding.isChapterChecked = - selectedExplorationIdList.contains(model.chapterSummary.explorationId) + binding.isChapterChecked = model.chapterSummary.explorationId in selectedExplorationIds binding.isChapterCheckboxEnabled = !model.chapterSummary.hasMissingPrerequisiteChapter() || model.chapterSummary.hasMissingPrerequisiteChapter() && - selectedExplorationIdList.contains( - model.chapterSummary.missingPrerequisiteChapter.explorationId - ) + model.chapterSummary.missingPrerequisiteChapter.explorationId in selectedExplorationIds binding.markChaptersCompletedChapterCheckBox.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { chapterSelected( model.chapterIndex, model.nextStoryIndex, - model.chapterSummary.explorationId + model.chapterSummary.explorationId, + model.chapterTitle ) } else { chapterUnselected(model.chapterIndex, model.nextStoryIndex) @@ -163,39 +174,34 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( } } - override fun chapterSelected(chapterIndex: Int, nextStoryIndex: Int, explorationId: String) { - if (!selectedExplorationIdList.contains(explorationId)) { - selectedExplorationIdList.add(explorationId) + private fun chapterSelected(chapterIdx: Int, nextStoryIdx: Int, expId: String, expTitle: String) { + if (expId !in selectedExplorationIds) { + selectedExplorationIds += expId + selectedExplorationTitles += expTitle } - if (selectedExplorationIdList.size == - viewModel.getItemList().count { - it is ChapterSummaryViewModel && !it.checkIfChapterIsCompleted() - } - ) { + if (selectedExplorationIds.size == viewModel.getItemList().countIncompleteChapters()) { binding.isAllChecked = true } if (!binding.markChaptersCompletedRecyclerView.isComputingLayout && binding.markChaptersCompletedRecyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE ) { - bindingAdapter.notifyItemChanged(chapterIndex) - if (chapterIndex + 1 < nextStoryIndex) - bindingAdapter.notifyItemChanged(chapterIndex + 1) + bindingAdapter.notifyItemChanged(chapterIdx) + if (chapterIdx + 1 < nextStoryIdx) + bindingAdapter.notifyItemChanged(chapterIdx + 1) } } - override fun chapterUnselected(chapterIndex: Int, nextStoryIndex: Int) { + private fun chapterUnselected(chapterIndex: Int, nextStoryIndex: Int) { for (index in chapterIndex until nextStoryIndex) { val explorationId = (viewModel.getItemList()[index] as ChapterSummaryViewModel).chapterSummary.explorationId - if (selectedExplorationIdList.contains(explorationId)) { - selectedExplorationIdList.remove(explorationId) + val expIndex = selectedExplorationIds.indexOf(explorationId) + if (expIndex != -1) { + selectedExplorationIds.removeAt(expIndex) + selectedExplorationTitles.removeAt(expIndex) } } - if (selectedExplorationIdList.size != - viewModel.getItemList().count { - it is ChapterSummaryViewModel && !it.checkIfChapterIsCompleted() - } - ) { + if (selectedExplorationIds.size != viewModel.getItemList().countIncompleteChapters()) { binding.isAllChecked = false } if (!binding.markChaptersCompletedRecyclerView.isComputingLayout && @@ -208,8 +214,54 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( } } + private fun showConfirmationDialog() { + alertDialog = AlertDialog.Builder(activity, R.style.OppiaAlertDialogTheme).apply { + setTitle(R.string.mark_chapters_completed_confirm_setting_dialog_title) + setMessage( + resourceHandler.getStringInLocaleWithWrapping( + R.string.mark_chapters_completed_confirm_setting_dialog_message, + selectedExplorationTitles.joinToReadableString() + ) + ) + setNegativeButton( + R.string.mark_chapters_completed_confirm_setting_dialog_cancel_button_text + ) { dialog, _ -> dialog.dismiss() } + setPositiveButton( + R.string.mark_chapters_completed_confirm_setting_dialog_confirm_button_text + ) { dialog, _ -> + dialog.dismiss() + markChaptersAsCompleted() + } + }.create().also { + it.setCanceledOnTouchOutside(true) + it.show() + } + } + + private fun markChaptersAsCompleted() { + modifyLessonProgressController.markMultipleChaptersCompleted( + profileId = profileId, + chapterMap = viewModel.getChapterMap().filterKeys { it in selectedExplorationIds } + ) + activity.finish() + } + private enum class ViewType { VIEW_TYPE_STORY, VIEW_TYPE_CHAPTER } + + private companion object { + private fun List.joinToReadableString(): String { + return when (size) { + 0 -> "" + 1 -> single() + 2 -> "${this[0]} and ${this[1]}" + else -> "${asSequence().take(size - 1).joinToString()}, and ${last()}" + } + } + + private fun List.countIncompleteChapters() = + filterIsInstance().count { !it.checkIfChapterIsCompleted() } + } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/testing/MarkChaptersCompletedTestActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/testing/MarkChaptersCompletedTestActivity.kt index 66bb84cb5e0..5b854a4c25d 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/testing/MarkChaptersCompletedTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/testing/MarkChaptersCompletedTestActivity.kt @@ -21,8 +21,8 @@ class MarkChaptersCompletedTestActivity : InjectableAppCompatActivity() { setContentView(R.layout.mark_chapters_completed_activity) internalProfileId = intent.getIntExtra(PROFILE_ID_EXTRA_KEY, -1) if (getMarkChaptersCompletedFragment() == null) { - val markChaptersCompletedFragment = MarkChaptersCompletedFragment - .newInstance(internalProfileId) + val markChaptersCompletedFragment = + MarkChaptersCompletedFragment.newInstance(internalProfileId, showConfirmationNotice = false) supportFragmentManager.beginTransaction().add( R.id.mark_chapters_completed_container, markChaptersCompletedFragment diff --git a/app/src/main/java/org/oppia/android/app/devoptions/testing/DeveloperOptionsTestActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/testing/DeveloperOptionsTestActivity.kt index 046bb5ee117..b9dae33e7d8 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/testing/DeveloperOptionsTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/testing/DeveloperOptionsTestActivity.kt @@ -50,8 +50,9 @@ class DeveloperOptionsTestActivity : override fun routeToMarkChaptersCompleted() { startActivity( - MarkChaptersCompletedActivity - .createMarkChaptersCompletedIntent(this, internalProfileId) + MarkChaptersCompletedActivity.createMarkChaptersCompletedIntent( + context = this, internalProfileId, showConfirmationNotice = false + ) ) } diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditActivity.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditActivity.kt index f53b355b630..9399596698b 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditActivity.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditActivity.kt @@ -19,7 +19,7 @@ const val IS_MULTIPANE_EXTRA_KEY = "ProfileEditActivity.is_multipane" const val IS_PROFILE_DELETION_DIALOG_VISIBLE_KEY = "ProfileEditActivity.is_profile_deletion_dialog_visible" -/** Activity [ProfileEditActivity] that allows user to edit a profile. */ +/** Activity that allows admins to edit a profile. */ class ProfileEditActivity : InjectableAppCompatActivity() { @Inject lateinit var profileEditActivityPresenter: ProfileEditActivityPresenter diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt index 914bf3bade4..47d4fb8443f 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt @@ -18,6 +18,7 @@ import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.app.devoptions.markchapterscompleted.MarkChaptersCompletedActivity /** Argument key for profile deletion dialog in [ProfileEditFragment]. */ const val TAG_PROFILE_DELETION_DIALOG = "PROFILE_DELETION_DIALOG" @@ -74,6 +75,14 @@ class ProfileEditFragmentPresenter @Inject constructor( ) } + binding.profileMarkChaptersForCompletionButton?.setOnClickListener { + activity.startActivity( + MarkChaptersCompletedActivity.createMarkChaptersCompletedIntent( + activity, internalProfileId, showConfirmationNotice = true + ) + ) + } + binding.profileDeleteButton.setOnClickListener { showDeletionDialog(internalProfileId) } diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt index c15aa25d21b..b096bb45961 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt @@ -14,13 +14,15 @@ import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.platformparameter.EnableDownloadsSupport import org.oppia.android.util.platformparameter.PlatformParameterValue import javax.inject.Inject +import org.oppia.android.util.platformparameter.LearnerStudyAnalytics /** The ViewModel for [ProfileEditActivity]. */ @FragmentScope class ProfileEditViewModel @Inject constructor( private val oppiaLogger: OppiaLogger, private val profileManagementController: ProfileManagementController, - @EnableDownloadsSupport private val enableDownloadsSupport: PlatformParameterValue + @EnableDownloadsSupport private val enableDownloadsSupport: PlatformParameterValue, + @LearnerStudyAnalytics private val enableLearnerStudySupport: PlatformParameterValue ) : ObservableViewModel() { private lateinit var profileId: ProfileId @@ -29,6 +31,9 @@ class ProfileEditViewModel @Inject constructor( /** Download access enabled for the profile by the administrator. */ val isAllowedDownloadAccess: LiveData = isAllowedDownloadAccessMutableLiveData + /** Whether the admin is allowed to mark chapters as finished. */ + val isAllowedToMarkFinishedChapters: Boolean = enableLearnerStudySupport.value + /** List of all the current profiles registered in the app [ProfileListFragment]. */ val profile: LiveData by lazy { Transformations.map( diff --git a/app/src/main/res/layout/profile_edit_fragment.xml b/app/src/main/res/layout/profile_edit_fragment.xml index b1e0f7fdc10..6a10daaf230 100644 --- a/app/src/main/res/layout/profile_edit_fragment.xml +++ b/app/src/main/res/layout/profile_edit_fragment.xml @@ -115,6 +115,26 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/profile_rename_button" /> +