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 f1c00a6569a..efa855326d6 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 @@ -42,6 +42,7 @@ import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.utility.SplitScreenManager import org.oppia.android.app.utility.lifecycle.LifecycleSafeTimerFactory import org.oppia.android.databinding.StateFragmentBinding +import org.oppia.android.domain.exploration.ExplorationRetriever import org.oppia.android.domain.exploration.ExplorationProgressController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.survey.SurveyGatingController @@ -54,6 +55,7 @@ import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType import org.oppia.android.util.system.OppiaClock import javax.inject.Inject +import kotlinx.coroutines.runBlocking const val STATE_FRAGMENT_PROFILE_ID_ARGUMENT_KEY = "StateFragmentPresenter.state_fragment_profile_id" @@ -71,6 +73,7 @@ class StateFragmentPresenter @Inject constructor( private val fragment: Fragment, private val context: Context, private val lifecycleSafeTimerFactory: LifecycleSafeTimerFactory, + private val explorationRetriever: ExplorationRetriever, private val explorationProgressController: ExplorationProgressController, private val storyProgressController: StoryProgressController, private val oppiaLogger: OppiaLogger, @@ -99,6 +102,7 @@ class StateFragmentPresenter @Inject constructor( private var forceAnnouncedForHintsBar = false private lateinit var recyclerViewAssembler: StatePlayerRecyclerViewAssembler + private lateinit var explorationStateOrderData: Pair> private val ephemeralStateLiveData: LiveData> by lazy { explorationProgressController.getCurrentState().toLiveData() } @@ -124,6 +128,9 @@ class StateFragmentPresenter @Inject constructor( container, /* attachToRoot= */ false ) + + explorationStateOrderData = fetchExplorationStateOrder(explorationId) + recyclerViewAssembler = createRecyclerViewAssembler( assemblerBuilderFactory.create(resourceBucketName, entityType, profileId), binding.congratulationsTextView, @@ -294,6 +301,18 @@ class StateFragmentPresenter @Inject constructor( ) } + private fun fetchExplorationStateOrder(explorationId: String): Pair> { + var explorationStateOrderResult: Pair> + runBlocking { + explorationStateOrderResult = loadExplorationStateOrderAsync(explorationId) + } + return explorationStateOrderResult + } + + private suspend fun loadExplorationStateOrderAsync(explorationId: String): Pair> { + return explorationRetriever.loadExplorationPosition(explorationId) + } + private fun processEphemeralStateResult(result: AsyncResult) { when (result) { is AsyncResult.Failure -> @@ -320,6 +339,8 @@ class StateFragmentPresenter @Inject constructor( currentState = ephemeralState.state currentStateName = ephemeralState.state.name + updateStateProgress(ephemeralState.state) + showOrHideAudioByState(ephemeralState.state) val dataPair = recyclerViewAssembler.compute( @@ -341,6 +362,27 @@ class StateFragmentPresenter @Inject constructor( } } + private fun updateStateProgress(state: State) { + val currentStateName = state.name + val currentPosition = getPositionOfState(currentStateName, explorationStateOrderData) + val totalStates = explorationStateOrderData.first + + var stateProgress = calculateStateProgress(currentPosition, totalStates) + viewModel.setStateProgress(stateProgress) + } + + private fun getPositionOfState(currentStateName: String?, explorationStateOrderData: Pair>): Int? { + return explorationStateOrderData.second[currentStateName] + } + + private fun calculateStateProgress(currentPosition: Int?, totalStates: Int): Int { + var calculatedStateProgress : Double = 0.0 + if (currentPosition != null) { + calculatedStateProgress = ((currentPosition.toDouble() / totalStates) * 100) + } + return calculatedStateProgress.toInt() + } + /** Subscribes to the result of requesting to show a hint or solution. */ private fun subscribeToHintSolution(resultDataProvider: DataProvider) { resultDataProvider.toLiveData().observe( 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 82071abed1f..1b7a53ea582 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 @@ -45,6 +45,8 @@ class StateViewModel @Inject constructor( val itemList: ObservableList = ObservableArrayList() val rightItemList: ObservableList = ObservableArrayList() + var stateProgress = ObservableField(0) + val isSplitView = ObservableField(false) val centerGuidelinePercentage = ObservableField(0.5f) @@ -83,6 +85,10 @@ class StateViewModel @Inject constructor( this.profileId = profileId } + fun setStateProgress(progress: Int) { + stateProgress.set(progress) + } + fun setAudioBarVisibility(audioBarVisible: Boolean) { isAudioBarVisible.set(audioBarVisible) } diff --git a/app/src/main/res/layout/state_fragment.xml b/app/src/main/res/layout/state_fragment.xml index dda21b4db6b..0ab5a9399d3 100755 --- a/app/src/main/res/layout/state_fragment.xml +++ b/app/src/main/res/layout/state_fragment.xml @@ -24,6 +24,22 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + app:layout_constraintTop_toBottomOf="@id/state_progress_bar" /> + app:layout_constraintTop_toBottomOf="@id/state_progress_bar" /> > } diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationRetrieverImpl.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationRetrieverImpl.kt index bc44d8e959e..9ee0645bf67 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationRetrieverImpl.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationRetrieverImpl.kt @@ -33,6 +33,11 @@ class ExplorationRetrieverImpl @Inject constructor( } } + override suspend fun loadExplorationPosition(explorationId: String): Pair> { + val explorationObject = jsonAssetRetriever.loadJsonFromAsset("$explorationId.json") + return parseJsonObjectToCalculatePosition(explorationObject!!) + } + // Returns an exploration given an assetName private fun loadExplorationFromAsset(explorationObject: JSONObject): Exploration { val innerExplorationObject = explorationObject.getJSONObject("exploration") @@ -68,4 +73,41 @@ class ExplorationRetrieverImpl @Inject constructor( } return statesMap } + + private fun parseJsonObjectToCalculatePosition(explorationObject: JSONObject): Pair> { + val states = explorationObject.getJSONObject("exploration").getJSONObject("states") + val initialState = explorationObject.getJSONObject("exploration").getString("init_state_name") + + val statePosition = mutableMapOf() + var position = 0 + + // Function to recursively traverse the states and set their positions + fun getStatePosition(stateName: String) { + if (!statePosition.containsKey(stateName)) { + statePosition[stateName] = ++position + + val state = states.getJSONObject(stateName) + val interaction = state.getJSONObject("interaction") + + if (interaction.has("default_outcome") && !interaction.isNull("default_outcome")) { + val defaultOutcome = interaction.getJSONObject("default_outcome") + val dest = defaultOutcome.getString("dest") + getStatePosition(dest) + } + + if (interaction.has("answer_groups") && interaction.getJSONArray("answer_groups").length() > 0) { + val answerGroups = interaction.getJSONArray("answer_groups") + for (i in answerGroups.length() - 1 downTo 0) { + val outcome = answerGroups.getJSONObject(i).getJSONObject("outcome") + val dest = outcome.getString("dest") + getStatePosition(dest) + } + } + } + } + + getStatePosition(initialState) + return position to statePosition + + } } diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/testing/FakeExplorationRetriever.kt b/domain/src/main/java/org/oppia/android/domain/exploration/testing/FakeExplorationRetriever.kt index 360be7e060f..b9bffcff09f 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/testing/FakeExplorationRetriever.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/testing/FakeExplorationRetriever.kt @@ -22,6 +22,11 @@ class FakeExplorationRetriever @Inject constructor( return productionImpl.loadExploration(expIdToLoad) } + override suspend fun loadExplorationPosition(explorationId: String): Pair> { + val expIdToLoad = explorationProxies[explorationId] ?: explorationId + return productionImpl.loadExplorationPosition(expIdToLoad) + } + /** * Sets the exploration ID that should be loaded in place of [expIdToLoad] on all future calls to * [loadExploration].