diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml index 7dde621a72f..7a024172368 100644 --- a/.github/workflows/build_tests.yml +++ b/.github/workflows/build_tests.yml @@ -153,7 +153,7 @@ jobs: run: | cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: oppia-bazel.apk path: /home/runner/work/oppia-android/oppia-android/oppia.apk diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 84cc12006a8..f5ba874970e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -64,7 +64,7 @@ jobs: # We require 'sudo' to avoid an error of the existing android sdk. See https://github.com/actions/starter-workflows/issues/58 run: sudo ./gradlew --full-stacktrace :utility:testDebugUnitTest -Dorg.gradle.java.home=$JAVA_HOME - name: Upload Utility Test Reports - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} # IMPORTANT: Upload reports regardless of success or failure status with: name: utility reports @@ -77,7 +77,7 @@ jobs: # We require 'sudo' to avoid an error of the existing android sdk. See https://github.com/actions/starter-workflows/issues/58 run: sudo ./gradlew --full-stacktrace :data:testDebugUnitTest -Dorg.gradle.java.home=$JAVA_HOME - name: Upload Data Test Reports - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} # IMPORTANT: Upload reports regardless of success or failure status with: name: data reports @@ -90,7 +90,7 @@ jobs: # We require 'sudo' to avoid an error of the existing android sdk. See https://github.com/actions/starter-workflows/issues/58 run: sudo ./gradlew --full-stacktrace :domain:testDebugUnitTest -Dorg.gradle.java.home=$JAVA_HOME - name: Upload Domain Test Reports - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} # IMPORTANT: Upload reports regardless of success or failure status with: name: domain reports @@ -103,7 +103,7 @@ jobs: # We require 'sudo' to avoid an error of the existing android sdk. See https://github.com/actions/starter-workflows/issues/58 run: sudo ./gradlew --full-stacktrace :testing:testDebugUnitTest -Dorg.gradle.java.home=$JAVA_HOME - name: Upload Testing Test Reports - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} # IMPORTANT: Upload reports regardless of success or failure status with: name: testing reports @@ -144,7 +144,7 @@ jobs: run: | sudo ./gradlew --full-stacktrace :app:testDebugUnitTest --${{ matrix.shard }} -Dorg.gradle.java.home=$JAVA_HOME - name: Upload App Test Reports - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} # IMPORTANT: Upload reports regardless of success or failure status with: name: app reports ${{ matrix.shard }} diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml index 15dd3e28e76..ae04da9c0f4 100644 --- a/.github/workflows/static_checks.yml +++ b/.github/workflows/static_checks.yml @@ -107,6 +107,8 @@ jobs: CACHE_DIRECTORY: ~/.bazel_cache steps: - uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Set up Bazel uses: abhinavsingh/setup-bazel@v3 @@ -205,6 +207,15 @@ jobs: run: | bazel run //scripts:string_resource_validation_check -- $(pwd) + - name: Binary files check + # The expression if: ${{ !cancelled() }} runs a job or step regardless of its success or failure while responding to cancellations, + # serving as a cancellation-compliant alternative to if: ${{ always() }} in concurrent workflows. + if: ${{ !cancelled() }} + run: | + bash /home/runner/work/oppia-android/oppia-android/scripts/pre-commit.sh + echo "No binary files found in commit" + echo "BINARY FILES CHECK PASSED" + # Note that caching is intentionally not enabled for this check since licenses should always be # verified without any potential influence from earlier builds (i.e. always from a clean build to # ensure the results exactly match the current state of the repository). diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml index de3ce5fadd5..74cbbeabbed 100644 --- a/.github/workflows/stats.yml +++ b/.github/workflows/stats.yml @@ -184,6 +184,63 @@ jobs: beta $(pwd)/oppia_beta_without_changes.aab $(pwd)/oppia_beta_with_changes.aab \ ga $(pwd)/oppia_ga_without_changes.aab $(pwd)/oppia_ga_with_changes.aab + - name: Find CI workflow run for PR + id: find-workflow-run + uses: actions/github-script@v7 + continue-on-error: true + with: + script: | + const { owner, repo } = context.repo; + const runsResponse = await github.rest.actions.listWorkflowRuns({ + owner, + repo, + workflow_id: 'stats.yml', + }); + + const runs = runsResponse.data.workflow_runs; + runs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + + const run = runs[1]; + if(!run) { + core.setFailed('Could not find a succesful workflow run'); + return; + } + console.log(run.id); + + core.setOutput('run-id', run.id); + + - name: Download previous build summary + uses: actions/download-artifact@v4 + with: + name: brief_build_summary_${{ matrix.prInfo.number }} + path: ./previous_build_logs + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ steps.find-workflow-run.outputs.run-id }} + continue-on-error: true # Ignore errors if the file doesn't exist (first run) + + - name: Compare current build summary with the previous one + id: build-comparison + run: | + if [ -f ./develop/brief_build_summary.log ]; then + echo "Comparing current and previous build summaries..." + if diff ./develop/brief_build_summary.log ./previous_build_logs/brief_build_summary.log > /dev/null; then + echo "No changes detected; skipping comment." + echo "skip_comment=true" >> $GITHUB_ENV + else + echo "Changes detected; proceeding with the comment." + echo "skip_comment=false" >> $GITHUB_ENV + fi + else + echo "No previous summary found; proceeding with the comment." + echo "skip_comment=false" >> $GITHUB_ENV + fi + + - name: Upload current build summary for future comparison + uses: actions/upload-artifact@v4 + with: + name: brief_build_summary_${{ matrix.prInfo.number }} + path: ./develop/brief_build_summary.log + # Reference: https://github.com/peter-evans/create-or-update-comment#setting-the-comment-body-from-a-file. # Also, for multi-line env values, see: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings. - name: Extract reports for uploading & commenting @@ -203,6 +260,7 @@ jobs: cp "$GITHUB_WORKSPACE/develop/full_build_summary.log" "$FULL_BUILD_SUMMARY_FILE_PATH" - name: Add build stats summary comment + if: ${{ env.skip_comment == 'false' }} env: PR_NUMBER: ${{ matrix.prInfo.number }} uses: peter-evans/create-or-update-comment@v1 @@ -210,7 +268,7 @@ jobs: issue-number: ${{ env.PR_NUMBER }} body: ${{ steps.compute-comment-body.outputs.comment_body }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: ${{ env.FULL_BUILD_SUMMARY_FILE_NAME }} path: ${{ env.FULL_BUILD_SUMMARY_FILE_PATH }} diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivity.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivity.kt index 2cda2286d02..64beda78f53 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivity.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivity.kt @@ -3,6 +3,7 @@ package org.oppia.android.app.administratorcontrols import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.OnBackPressedCallback import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity @@ -83,6 +84,15 @@ class AdministratorControlsActivity : isProfileDeletionDialogVisible ) title = resourceHandler.getStringInLocale(R.string.administrator_controls) + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(/* enabled = */ true) { + override fun handleOnBackPressed() { + this@AdministratorControlsActivity.handleBackPress() + } + } + ) } override fun routeToAppVersion() { @@ -121,7 +131,7 @@ class AdministratorControlsActivity : } } - override fun onBackPressed() { + private fun handleBackPress() { val fragment = supportFragmentManager.findFragmentById( R.id.administrator_controls_fragment_multipane_placeholder @@ -134,8 +144,7 @@ class AdministratorControlsActivity : if (fragment is ProfileEditFragment) { administratorControlsActivityPresenter.handleOnBackPressed() } else { - @Suppress("DEPRECATION") // TODO(#5404): Migrate to a back pressed dispatcher. - super.onBackPressed() + finish() } } diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/appversion/AppVersionActivity.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/appversion/AppVersionActivity.kt index e41600bb929..f78d770df1d 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/appversion/AppVersionActivity.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/appversion/AppVersionActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.activity.OnBackPressedCallback import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.model.ScreenName.APP_VERSION_ACTIVITY @@ -19,12 +20,20 @@ class AppVersionActivity : InjectableAutoLocalizedAppCompatActivity() { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) appVersionActivityPresenter.handleOnCreate() + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(/* enabled = */ true) { + override fun handleOnBackPressed() { + finish() + } + } + ) } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - @Suppress("DEPRECATION") // TODO(#5404): Migrate to a back pressed dispatcher. - onBackPressed() + onBackPressedDispatcher.onBackPressed() } return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileAndDeviceIdActivity.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileAndDeviceIdActivity.kt index cf6fb025db6..45a27fac5b8 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileAndDeviceIdActivity.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileAndDeviceIdActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.activity.OnBackPressedCallback import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.model.ScreenName.PROFILE_AND_DEVICE_ID_ACTIVITY @@ -18,18 +19,27 @@ import javax.inject.Inject * a particular user or group. */ class ProfileAndDeviceIdActivity : InjectableAutoLocalizedAppCompatActivity() { - @Inject lateinit var profileAndDeviceIdActivityPresenter: ProfileAndDeviceIdActivityPresenter + @Inject + lateinit var profileAndDeviceIdActivityPresenter: ProfileAndDeviceIdActivityPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) profileAndDeviceIdActivityPresenter.handleOnCreate() + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(/* enabled = */ true) { + override fun handleOnBackPressed() { + finish() + } + } + ) } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - @Suppress("DEPRECATION") // TODO(#5404): Migrate to a back pressed dispatcher. - onBackPressed() + onBackPressedDispatcher.onBackPressed() } return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/org/oppia/android/app/databinding/TextViewBindingAdapters.java b/app/src/main/java/org/oppia/android/app/databinding/TextViewBindingAdapters.java index e4584d7cad1..284e1332b2d 100644 --- a/app/src/main/java/org/oppia/android/app/databinding/TextViewBindingAdapters.java +++ b/app/src/main/java/org/oppia/android/app/databinding/TextViewBindingAdapters.java @@ -68,15 +68,14 @@ public static void setDrawableEndCompat( } private static String getTimeAgo(View view, long lastVisitedTimestamp) { - long timeStampMillis = ensureTimestampIsInMilliseconds(lastVisitedTimestamp); long currentTimeMillis = getOppiaClock(view).getCurrentTimeMs(); AppLanguageResourceHandler resourceHandler = getResourceHandler(view); - if (timeStampMillis > currentTimeMillis || timeStampMillis <= 0) { + if (lastVisitedTimestamp > currentTimeMillis || lastVisitedTimestamp <= 0) { return resourceHandler.getStringInLocale(R.string.last_logged_in_recently); } - long timeDifferenceMillis = currentTimeMillis - timeStampMillis; + long timeDifferenceMillis = currentTimeMillis - lastVisitedTimestamp; if (timeDifferenceMillis < (int) TimeUnit.MINUTES.toMillis(1)) { return resourceHandler.getStringInLocale(R.string.just_now); @@ -112,15 +111,6 @@ private static String getPluralString( ); } - private static long ensureTimestampIsInMilliseconds(long lastVisitedTimestamp) { - // TODO(#3842): Investigate & remove this check. - if (lastVisitedTimestamp < 1000000000000L) { - // If timestamp is given in seconds, convert that to milliseconds. - return TimeUnit.SECONDS.toMillis(lastVisitedTimestamp); - } - return lastVisitedTimestamp; - } - private static AppLanguageResourceHandler getResourceHandler(View view) { AppLanguageActivityInjectorProvider provider = (AppLanguageActivityInjectorProvider) getAttachedActivity(view); 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 ec8979dad41..cc1a7897150 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 @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.activity.OnBackPressedCallback import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity @@ -37,12 +38,20 @@ class MarkChaptersCompletedActivity : InjectableAutoLocalizedAppCompatActivity() val showConfirmationNotice = args?.showConfirmationNotice ?: false markChaptersCompletedActivityPresenter.handleOnCreate(internalProfileId, showConfirmationNotice) title = resourceHandler.getStringInLocale(R.string.mark_chapters_completed_activity_title) + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(/* enabled = */ true) { + override fun handleOnBackPressed() { + finish() + } + } + ) } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - @Suppress("DEPRECATION") // TODO(#5404): Migrate to a back pressed dispatcher. - onBackPressed() + onBackPressedDispatcher.onBackPressed() } return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedActivity.kt index e3373fc7df6..86bb96b2f8d 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.activity.OnBackPressedCallback import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity @@ -34,12 +35,20 @@ class MarkStoriesCompletedActivity : InjectableAutoLocalizedAppCompatActivity() internalProfileId = profileId?.internalId ?: -1 markStoriesCompletedActivityPresenter.handleOnCreate(internalProfileId) title = resourceHandler.getStringInLocale(R.string.mark_stories_completed_activity_title) + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(/* enabled = */ true) { + override fun handleOnBackPressed() { + finish() + } + } + ) } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - @Suppress("DEPRECATION") // TODO(#5404): Migrate to a back pressed dispatcher. - onBackPressed() + onBackPressedDispatcher.onBackPressed() } return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedActivity.kt index a891de3a059..606c4f70bd0 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.activity.OnBackPressedCallback import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity @@ -34,12 +35,20 @@ class MarkTopicsCompletedActivity : InjectableAutoLocalizedAppCompatActivity() { internalProfileId = profileId?.internalId ?: -1 markTopicsCompletedActivityPresenter.handleOnCreate(internalProfileId) title = resourceHandler.getStringInLocale(R.string.mark_topics_completed_activity_title) + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(/* enabled = */ true) { + override fun handleOnBackPressed() { + finish() + } + } + ) } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - @Suppress("DEPRECATION") // TODO(#5404): Migrate to a back pressed dispatcher. - onBackPressed() + onBackPressedDispatcher.onBackPressed() } return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivity.kt b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivity.kt index e88525841b5..054a4d8e54b 100644 --- a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivity.kt +++ b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivity.kt @@ -3,6 +3,7 @@ package org.oppia.android.app.options import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.OnBackPressedCallback import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.model.ReadingTextSize @@ -34,6 +35,22 @@ class ReadingTextSizeActivity : InjectableAutoLocalizedAppCompatActivity() { savedInstanceState?.retrieveStateBundle()?.selectedReadingTextSize ?: retrieveActivityParams().readingTextSize readingTextSizeActivityPresenter.handleOnCreate(readingTextSize) + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(/* enabled = */ true) { + override fun handleOnBackPressed() { + val resultBundle = ReadingTextSizeActivityResultBundle.newBuilder().apply { + selectedReadingTextSize = readingTextSizeActivityPresenter.getSelectedReadingTextSize() + }.build() + val intent = Intent().apply { + putProtoExtra(MESSAGE_READING_TEXT_SIZE_RESULTS_KEY, resultBundle) + } + setResult(RESULT_OK, intent) + finish() + } + } + ) } companion object { @@ -60,17 +77,6 @@ class ReadingTextSizeActivity : InjectableAutoLocalizedAppCompatActivity() { outState.putProto(ACTIVITY_SAVED_STATE_KEY, stateBundle) } - override fun onBackPressed() { - val resultBundle = ReadingTextSizeActivityResultBundle.newBuilder().apply { - selectedReadingTextSize = readingTextSizeActivityPresenter.getSelectedReadingTextSize() - }.build() - val intent = Intent().apply { - putProtoExtra(MESSAGE_READING_TEXT_SIZE_RESULTS_KEY, resultBundle) - } - setResult(RESULT_OK, intent) - finish() - } - private fun retrieveActivityParams() = intent.getProtoExtra(ACTIVITY_PARAMS_KEY, ReadingTextSizeActivityParams.getDefaultInstance()) diff --git a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivityPresenter.kt index 85eedd9d379..491ad80fb11 100644 --- a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivityPresenter.kt @@ -28,8 +28,7 @@ class ReadingTextSizeActivityPresenter @Inject constructor( private fun setToolbar() { val readingTextSizeToolbar: Toolbar = activity.findViewById(R.id.reading_text_size_toolbar) readingTextSizeToolbar.setNavigationOnClickListener { - @Suppress("DEPRECATION") // TODO(#5404): Migrate to a back pressed dispatcher. - activity.onBackPressed() + activity.onBackPressedDispatcher.onBackPressed() } } diff --git a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragment.kt b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragment.kt index 1cdae8c8578..02c9a456e74 100644 --- a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragment.kt +++ b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragment.kt @@ -63,7 +63,8 @@ class ReadingTextSizeFragment : InjectableFragment(), TextSizeRadioButtonListene readingTextSizeFragmentPresenter.onTextSizeSelected(selectedTextSize) } - private fun retrieveFragmentArguments(): ReadingTextSizeFragmentArguments { + /** Returns the [ReadingTextSizeFragmentArguments] stored in the fragment's arguments. */ + fun retrieveFragmentArguments(): ReadingTextSizeFragmentArguments { return checkNotNull(arguments) { "Expected arguments to be passed to ReadingTextSizeFragment" }.getProto(FRAGMENT_ARGUMENTS_KEY, ReadingTextSizeFragmentArguments.getDefaultInstance()) 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 6912d639ed9..d9519c01e2a 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 @@ -3,6 +3,7 @@ package org.oppia.android.app.player.exploration import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.OnBackPressedCallback import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.hintsandsolution.HintsAndSolutionDialogFragment @@ -48,7 +49,8 @@ class ExplorationActivity : BottomSheetOptionsMenuItemClickListener, RequestVoiceOverIconSpotlightListener { - @Inject lateinit var explorationActivityPresenter: ExplorationActivityPresenter + @Inject + lateinit var explorationActivityPresenter: ExplorationActivityPresenter private lateinit var state: State private lateinit var writtenTranslationContext: WrittenTranslationContext @@ -67,6 +69,14 @@ class ExplorationActivity : params.parentScreen, params.isCheckpointingEnabled ) + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(/* enabled = */ true) { + override fun handleOnBackPressed() { + explorationActivityPresenter.backButtonPressed() + } + } + ) } // TODO(#1655): Re-restrict access to fields in tests post-Gradle. @@ -114,10 +124,6 @@ class ExplorationActivity : getProtoExtra(PARAMS_KEY, ExplorationActivityParams.getDefaultInstance()) } - override fun onBackPressed() { - explorationActivityPresenter.backButtonPressed() - } - override fun deleteCurrentProgressAndStopSession(isCompletion: Boolean) { explorationActivityPresenter.deleteCurrentProgressAndStopExploration(isCompletion) } diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt index 1c493c19bfa..4049f5dc4a2 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt @@ -113,8 +113,7 @@ class ExplorationActivityPresenter @Inject constructor( } binding.explorationToolbar.setNavigationOnClickListener { - @Suppress("DEPRECATION") // TODO(#5404): Migrate to a back pressed dispatcher. - activity.onBackPressed() + activity.onBackPressedDispatcher.onBackPressed() } binding.actionAudioPlayer.setOnClickListener { diff --git a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonActivity.kt b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonActivity.kt index fc63898ac40..4e268b486c2 100644 --- a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonActivity.kt +++ b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonActivity.kt @@ -3,6 +3,7 @@ package org.oppia.android.app.resumelesson import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.OnBackPressedCallback import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.home.RouteToExplorationListener @@ -41,6 +42,15 @@ class ResumeLessonActivity : params.parentScreen, params.checkpoint ) + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(/* enabled = */ true) { + override fun handleOnBackPressed() { + resumeLessonActivityPresenter.setReadingTextSizeNormal() + finish() + } + } + ) } // TODO(#1655): Re-restrict access to fields in tests post-Gradle. @@ -113,9 +123,4 @@ class ResumeLessonActivity : override fun onDefaultFontSizeLoaded(readingTextSize: ReadingTextSize) { resumeLessonActivityPresenter.loadResumeLessonFragment(readingTextSize) } - - override fun onBackPressed() { - resumeLessonActivityPresenter.setReadingTextSizeNormal() - finish() - } } diff --git a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonActivityPresenter.kt index 210d49c887c..b09e77fdfce 100644 --- a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonActivityPresenter.kt @@ -73,8 +73,7 @@ class ResumeLessonActivityPresenter @Inject constructor( context = activity, ReadingTextSize.MEDIUM_TEXT_SIZE ) - @Suppress("DEPRECATION") // TODO(#5404): Migrate to a back pressed dispatcher. - activity.onBackPressed() + activity.onBackPressedDispatcher.onBackPressed() } } diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index 049ebac057a..17d2fd54cec 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -152,7 +152,7 @@ Tafadhali anza jibu lako kwa nambari (k.m.,”0” katika 0.5). Tafadhali weka nambari halali. Jibu linaweza kuwa na tarakimu zisizozidi 15 (0–9) au alama (. au -). - Tafadhali andika uwiano unaojumuisha tarakimu zilizotenganishwa na koloni(k.m. 1:2 au 1:2:3). + Tafadhali andika uwiano unaojumuisha tarakimu zilizotenganishwa na koloni (k.m. 1:2 au 1:2:3). Tafadhali weka uwiano sahihi (k.m. 1:2 au 1:2:3). Jibu lako lina koloni mbili (:) karibu na kila moja. Idadi ya masharti si sawa na masharti yanayohitajika. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1395b1d24a3..319d70ff93a 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -175,12 +175,12 @@ Please enter a valid number. The answer can contain at most 15 digits (0–9) or symbols (. or -). Enter a number to continue. - Please write a ratio that consists of digits separated by colons (e.g. 1:2 or 1:2:3). - Please enter a valid ratio (e.g. 1:2 or 1:2:3). - Your answer has two colons (:) next to each other. - Number of terms is not equal to the required terms. - Ratios cannot have 0 as an element. - Enter a ratio to continue. + Please write a ratio that consists of digits separated by colons (e.g. 1:2 or 1:2:3). + Please enter a valid ratio (e.g. 1:2 or 1:2:3). + Your answer has two colons (:) next to each other. + Number of terms is not equal to the required terms. + Ratios cannot have 0 as an element. + Enter a ratio to continue. Enter text to continue. Choose an answer to continue. Unknown size @@ -549,11 +549,11 @@ Policy Page Privacy Policy - this page for the latest version of this privacy policy.]]> + Please visit <a href="https://www.oppia.org/privacy-policy">this page</a> for the latest version of this privacy policy. Terms of Service - Terms of Service and Privacy Policy.]]> - this page for the latest version of these terms.]]> + By using %s, you agree to our <br> <oppia-noninteractive-policy link="tos">Terms of Service</oppia-noninteractive-policy> and <oppia-noninteractive-policy link="privacy">Privacy Policy</oppia-noninteractive-policy>. + Please visit <a href="https://www.oppia.org/terms">this page</a> for the latest version of these terms. What is %s? Who is an Administrator? @@ -569,19 +569,19 @@ How do I update my Android OS? I can\'t find my question here. What now? - %1$s "O-pee-yah" (Finnish) - "to learn"


%1$s\'s mission is to help anyone learn anything they want in an effective and enjoyable way.


By creating a set of free, high-quality, demonstrably effective lessons with the help of educators from around the world, %1$s aims to provide students with quality education — regardless of where they are or what traditional resources they have access to.


As a student, you can begin your learning adventure by browsing the topics listed on the Home Page!

]]>
- An Administrator is the main user that manages profiles and settings for every profile on their account. They are most likely your parent, teacher, or guardian that created this profile for you.


Administrators have the ability to manage profiles, assign PINs, and change other settings under their account. Depending on your profile, Administrator permissions may be required for certain features such as changing your PIN, and more.


To see who your Administrator is, go to the Profile Chooser. The first profile listed and has "Administrator" written under their name is the Administrator.

]]>
- If it is your first time creating a profile and you do not have a PIN:
  1. From the Profile Chooser, tap on Set up Multiple Profiles.
  2. Create a PIN and Save.
  3. Fill in all fields for the profile.
    1. (Optional) Upload a photo.
    2. Enter a name.
    3. (Optional) Assign a 3-digit PIN.
  4. Tap Create. This profile is added to your Profile Chooser!

If you have created a profile before and have a PIN:

  1. From the Profile Chooser, tap on Add Profile.
  2. Enter your PIN and tap Submit.
  3. Fill in all fields for the profile.
    1. (Optional) Upload a photo.
    2. Enter a name.
    3. (Optional) Assign a 3-digit PIN.
  4. Tap Create. This profile is added to your Profile Chooser!


Note: Only the Administrator is able to manage profiles.

]]>
- The %s app currently supports English, Brazilian Portuguese, Arabic, Swahili and Nigerian Pidgin. Choose one of these languages in the menu, under Options. To request the app in your language, please contact us at admin@oppia.org.

]]>
-
  1. From your %s app home screen, tap the menu in the top left corner.
  2. Tap Share feedback.
  3. Follow the instructions to report the bug or share feedback.
  4. ]]> - %1$s’s mission is to help learners gain necessary life skills. Math is an essential skill in everyday life. %1$s will be offering new lessons on science and other subjects soon!

    ]]>
    - Yes, %s will be offering new lessons on science and other subjects soon. Please check back for updates!

    ]]>
    - If the Exploration Player is not loading


    Check to see if the app is up to date:

    • Go to the Play Store and make sure the app is updated to its latest version


    Check your internet connection:

    • If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network.

    Ask the Administrator to check their device and internet connection:

    • Get the Administrator to troubleshoot using the steps above

    Let us know if you still have issues with loading:

    • Report a problem by contacting us at admin@oppia.org.
    ]]>
    - If your audio is not playing


    Check to see if the app is up to date:

    • Go to the Play Store and make sure the app is updated to its latest version


    Check your internet connection:

    • If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network. Slow internet may cause the audio to load irregularly, making it difficult to play.


    Ask the Administrator to check their device and internet connection:

    • Get the Administrator to troubleshoot using the steps above


    Let us know if you still have issues with loading:

    • Report a problem by contacting us at admin@oppia.org.
    ]]>
    - Once a profile is deleted:

    1. The profile cannot be recovered.
    2. Profile information such as name, photos, and progress will be permanently deleted.

    To delete a profile (excluding the Administrator\'s):

    1. From the Administrator\'s Home Page, tap on the menu button on the top left.
    2. Tap on Administrator Controls.
    3. Tap on Edit Profiles.
    4. Tap on the Profile you would like to delete.
    5. At the bottom of the screen, tap Profile Deletion.
    6. Tap Delete to confirm deletion.


    Note: Only the Administrator is able to manage profiles.

    ]]>
    -
    1. Open the Google Play Store app.
    2. Search for the %s app.
    3. Tap Update.

      ]]> -
      1. Tap your phone\'s Settings app.
      2. Tap System updates.
      3. Tap System updates and follow the instructions to update your Android operating system.

        ]]> - If you cannot find your question or would like to report a bug, contact us at admin@oppia.org.

        ]]>
        + <p>%1$s <i>"O-pee-yah"</i> (Finnish) - "to learn"</p><p><br></p><p>%1$s\'s mission is to help anyone learn anything they want in an effective and enjoyable way.</p><p><br></p><p>By creating a set of free, high-quality, demonstrably effective lessons with the help of educators from around the world, %1$s aims to provide students with quality education — regardless of where they are or what traditional resources they have access to.</p><p><br></p><p>As a student, you can begin your learning adventure by browsing the topics listed on the Home Page!</p> + <p>An Administrator is the main user that manages profiles and settings for every profile on their account. They are most likely your parent, teacher, or guardian that created this profile for you.</p><p><br></p><p>Administrators have the ability to manage profiles, assign PINs, and change other settings under their account. Depending on your profile, Administrator permissions may be required for certain features such as changing your PIN, and more.</p><p><br></p><p>To see who your Administrator is, go to the Profile Chooser. The first profile listed and has "Administrator" written under their name is the Administrator.</p> + <p>If it is your first time creating a profile and you do not have a PIN:<ol><li>From the Profile Chooser, tap on <strong>Set up Multiple Profiles</strong>.</li><li>Create a PIN and <strong>Save</strong>.</li><li>Fill in all fields for the profile.<ol><li>(Optional) Upload a photo.</li><li>Enter a name.</li><li>(Optional) Assign a 3-digit PIN.</li></ol></li><li>Tap <strong>Create</strong>. This profile is added to your Profile Chooser!</li></ol></p><p>If you have created a profile before and have a PIN:<ol><li>From the Profile Chooser, tap on <strong>Add Profile</strong>.</li><li>Enter your PIN and tap <strong>Submit</strong>.</li><li>Fill in all fields for the profile.<ol><li>(Optional) Upload a photo.</li><li>Enter a name.</li><li>(Optional) Assign a 3-digit PIN.</li></ol></li><li>Tap <strong>Create</strong>. This profile is added to your Profile Chooser!</li></ol></p><p><br></p><p>Note: Only the <u>Administrator</u> is able to manage profiles.</p> + <p>The %s app currently supports English, Brazilian Portuguese, Arabic, Swahili, and Nigerian Pidgin. Choose one of these languages in the menu, under Options. To request the app in your language, please contact us at <strong>admin@oppia.org</strong>.</p> + <p><ol><li>From your %s app home screen, tap the menu in the top left corner.</li><li>Tap <strong>Share feedback</strong>.</li><li>Follow the instructions to report the bug or share feedback.</li></ol></p> + <p>%1$s\'s mission is to help learners gain necessary life skills. Math is an essential skill in everyday life. %1$s will be offering new lessons on science and other subjects soon!</p> + <p>Yes, %s will be offering new lessons on science and other subjects soon. Please check back for updates!</p> + <p>If the Exploration Player is not loading</p><p><br></p><p>Check to see if the app is up to date:</p><ul><li>Go to the Play Store and make sure the app is updated to its latest version</li></ul><p><br></p><p>Check your internet connection:</p><ul><li>If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network.</li></ul><p>Ask the Administrator to check their device and internet connection:</p><ul><li>Get the Administrator to troubleshoot using the steps above</li></ul><p><br></p><p>Let us know if you still have issues with loading:</p><ul><li>Report a problem by contacting us at admin@oppia.org.</li></ul> + <p>If your audio is not playing</p><p><br></p><p>Check to see if the app is up to date:</p><ul><li>Go to the Play Store and make sure the app is updated to its latest version</li></ul><p><br></p><p>Check your internet connection:</p><ul><li>If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network. Slow internet may cause the audio to load irregularly, making it difficult to play.</li></ul><p><br></p><p>Ask the Administrator to check their device and internet connection:</p><ul><li>Get the Administrator to troubleshoot using the steps above</li></ul><p><br></p><p>Let us know if you still have issues with loading:</p><ul><li>Report a problem by contacting us at admin@oppia.org.</li></ul> + <p>Once a profile is deleted:</p><ol><li>The profile cannot be recovered.</li><li>Profile information such as name, photos, and progress will be permanently deleted.</li></ol><p>To delete a profile (excluding the <u>Administrator</u>):</p><ol><li>From the Administrator\'s Home Page, tap on the menu button on the top left.</li><li>Tap on <strong>Administrator Controls</strong>.</li><li>Tap on <strong>Edit Profiles</strong>.</li><li>Tap on the Profile you would like to delete.</li><li>At the bottom of the screen, tap <strong>Profile Deletion</strong>.</li><li>Tap <strong>Delete</strong> to confirm deletion.</li></ol><p><br></p><p>Note: Only the <u>Administrator</u> is able to manage profiles.</p> + <p><ol><li>Open the Google Play Store app.</li><li>Search for the %s app.</li><li>Tap Update.</li></ol></p> + <p><ol><li>Tap your phone\'s Settings app.</li><li>Tap System updates.</li><li>Tap System updates and follow the instructions to update your Android operating system.</li></ol></p> + <p>If you cannot find your question or would like to report a bug, contact us at <strong>admin@oppia.org.</strong></p> Profile Edit Fragment Test Activity Administrator Controls Fragment Test Activity diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt index 5e4b14be4aa..1883d2459bf 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt @@ -16,6 +16,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import dagger.Component import org.hamcrest.Description import org.hamcrest.TypeSafeMatcher @@ -37,6 +38,7 @@ import org.oppia.android.app.application.testing.TestingBuildFlavorModule import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.ReadingTextSize.MEDIUM_TEXT_SIZE import org.oppia.android.app.model.ReadingTextSize.SMALL_TEXT_SIZE import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView @@ -194,6 +196,47 @@ class ReadingTextSizeFragmentTest { } } + @Test + fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() { + launch(createReadingTextSizeActivityIntent()).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val readingTextSizeFragment = activity.supportFragmentManager + .findFragmentById(R.id.reading_text_size_container) as ReadingTextSizeFragment + val receivedReadingTextSize = readingTextSizeFragment.retrieveFragmentArguments() + .readingTextSize + + assertThat(receivedReadingTextSize).isEqualTo(SMALL_TEXT_SIZE) + } + } + } + + @Test + fun testFragment_saveInstanceState_verifyCorrectStateRestored() { + launch(createReadingTextSizeActivityIntent()).use { scenario -> + testCoroutineDispatchers.runCurrent() + + scenario.onActivity { activity -> + val readingTextSizeFragment = activity.supportFragmentManager + .findFragmentById(R.id.reading_text_size_container) as ReadingTextSizeFragment + readingTextSizeFragment.readingTextSizeFragmentPresenter + .onTextSizeSelected(MEDIUM_TEXT_SIZE) + } + + scenario.recreate() + + scenario.onActivity { activity -> + val newReadingTextSizeFragment = activity.supportFragmentManager + .findFragmentById(R.id.reading_text_size_container) as ReadingTextSizeFragment + val restoredTopicIdList = + newReadingTextSizeFragment.readingTextSizeFragmentPresenter.getTextSizeSelected() + + assertThat(restoredTopicIdList).isEqualTo(MEDIUM_TEXT_SIZE) + } + } + } + private fun createReadingTextSizeActivityIntent() = ReadingTextSizeActivity.createReadingTextSizeActivityIntent(context, SMALL_TEXT_SIZE) diff --git a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt index 1a28e628594..33da1eeed36 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt @@ -41,6 +41,7 @@ import org.oppia.android.app.model.ExplorationActivityParams import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.ReadingTextSize +import org.oppia.android.app.model.ResumeLessonFragmentArguments import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule @@ -99,6 +100,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.EventLoggingConfigurationModule @@ -273,6 +275,41 @@ class ResumeLessonFragmentTest { } } + @Test + fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() { + launch(createResumeLessonActivityIntent()).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val resumeLessonFragment = activity.supportFragmentManager + .findFragmentById(R.id.resume_lesson_fragment_placeholder) as ResumeLessonFragment + val args = checkNotNull(resumeLessonFragment.arguments) { + "Expected arguments to be provided for fragment." + }.getProto( + ResumeLessonFragment.RESUME_LESSON_FRAGMENT_ARGUMENTS_KEY, + ResumeLessonFragmentArguments.getDefaultInstance() + ) + val receivedProfileId = args.profileId + val receivedClassroomId = args.classroomId + val receivedTopicId = args.topicId + val receivedStoryId = args.storyId + val receivedExplorationId = args.explorationId + val receivedParentScreen = args.parentScreen + val receivedCheckpoint = args.checkpoint + + assertThat(receivedProfileId) + .isEqualTo(ProfileId.newBuilder().apply { internalId = 1 }.build()) + assertThat(receivedClassroomId).isEqualTo(TEST_CLASSROOM_ID_1) + assertThat(receivedTopicId).isEqualTo(FRACTIONS_TOPIC_ID) + assertThat(receivedStoryId).isEqualTo(FRACTIONS_STORY_ID_0) + assertThat(receivedExplorationId).isEqualTo(FRACTIONS_EXPLORATION_ID_0) + assertThat(receivedParentScreen) + .isEqualTo(ExplorationActivityParams.ParentScreen.PARENT_SCREEN_UNSPECIFIED) + assertThat(receivedCheckpoint).isEqualTo(ExplorationCheckpoint.getDefaultInstance()) + } + } + } + private fun createResumeLessonActivityIntent(): Intent { return ResumeLessonActivity.createResumeLessonActivityIntent( context, diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt index 5b7bd78bb3e..742c20bd0f5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt @@ -18,6 +18,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import dagger.Component import org.hamcrest.Matchers.not import org.junit.After @@ -37,6 +38,7 @@ import org.oppia.android.app.application.ApplicationStartupListenerModule import org.oppia.android.app.application.testing.TestingBuildFlavorModule import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.model.ProfileListFragmentArguments import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPosition import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView @@ -86,6 +88,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.EventLoggingConfigurationModule @@ -367,6 +370,30 @@ class ProfileListFragmentTest { } } + @Test + fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() { + profileTestHelper.initializeProfiles() + launch(ProfileListActivity::class.java).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val profileListFragment = activity.supportFragmentManager + .findFragmentById(R.id.profile_list_container) as ProfileListFragment + + val arguments = checkNotNull(profileListFragment.arguments) { + "Expected variables to be passed to ProfileListFragment" + } + val args = arguments.getProto( + ProfileListFragment.PROFILE_LIST_FRAGMENT_ARGUMENTS_KEY, + ProfileListFragmentArguments.getDefaultInstance() + ) + val receivedIsMultipane = args.isMultipane + + assertThat(receivedIsMultipane).isEqualTo(false) + } + } + } + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. @Singleton @Component( diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt index fd4bf50712b..1fc5d9316d3 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt @@ -21,6 +21,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule +import com.google.common.truth.Truth.assertThat import dagger.Component import org.hamcrest.CoreMatchers.not import org.hamcrest.core.AllOf.allOf @@ -102,6 +103,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -441,6 +443,30 @@ class ProfileRenameFragmentTest { } } + @Test + fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() { + ActivityScenario.launch( + ProfileRenameActivity.createProfileRenameActivity( + context = context, + internalProfileId = 1 + ) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val profileRenameFragment = activity.supportFragmentManager + .findFragmentById(R.id.profile_rename_fragment_placeholder) as ProfileRenameFragment + val args = + checkNotNull(profileRenameFragment.arguments) { + "Expected arguments to be passed to ProfileRenameFragment" + } + val receivedProfileId = args.extractCurrentUserProfileId().internalId + + assertThat(receivedProfileId).isEqualTo(1) + } + } + } + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. @Singleton @Component( diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt index c306e9a6206..c0e1541d8a3 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt @@ -22,6 +22,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule +import com.google.common.truth.Truth.assertThat import dagger.Component import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not @@ -42,6 +43,7 @@ import org.oppia.android.app.application.ApplicationStartupListenerModule import org.oppia.android.app.application.testing.TestingBuildFlavorModule import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.model.ProfileResetPinFragmentArguments import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule @@ -92,6 +94,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.EventLoggingConfigurationModule @@ -1006,6 +1009,70 @@ class ProfileResetPinFragmentTest { } } + @Test + fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() { + ActivityScenario.launch( + ProfileResetPinActivity.createProfileResetPinActivity( + context = context, + profileId = 0, + isAdmin = true + ) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val profileResetPinFragment = activity.supportFragmentManager + .findFragmentById(R.id.profile_reset_pin_fragment_placeholder) as ProfileResetPinFragment + + val arguments = checkNotNull(profileResetPinFragment.arguments) { + "Expected arguments to be passed to ProfileResetPinFragment" + } + val args = + arguments.getProto( + ProfileResetPinFragment.PROFILE_RESET_PIN_FRAGMENT_ARGUMENTS_KEY, + ProfileResetPinFragmentArguments.getDefaultInstance() + ) + val receivedProfileResetPinProfileId = args.internalProfileId + val receivedProfileResetPinIsAdmin = args.isAdmin + + assertThat(receivedProfileResetPinProfileId).isEqualTo(0) + assertThat(receivedProfileResetPinIsAdmin).isEqualTo(true) + } + } + } + + @Test + fun testFragment_fragmentLoaded_whenIsAdminFalse_verifyCorrectArgumentsPassed() { + ActivityScenario.launch( + ProfileResetPinActivity.createProfileResetPinActivity( + context = context, + profileId = 0, + isAdmin = false + ) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val profileResetPinFragment = activity.supportFragmentManager + .findFragmentById(R.id.profile_reset_pin_fragment_placeholder) as ProfileResetPinFragment + + val arguments = checkNotNull(profileResetPinFragment.arguments) { + "Expected arguments to be passed to ProfileResetPinFragment" + } + val args = + arguments.getProto( + ProfileResetPinFragment.PROFILE_RESET_PIN_FRAGMENT_ARGUMENTS_KEY, + ProfileResetPinFragmentArguments.getDefaultInstance() + ) + val receivedProfileResetPinProfileId = args.internalProfileId + val receivedProfileResetPinIsAdmin = args.isAdmin + + assertThat(receivedProfileResetPinProfileId).isEqualTo(0) + assertThat(receivedProfileResetPinIsAdmin).isEqualTo(false) + } + } + } + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. @Singleton @Component( diff --git a/app/src/sharedTest/java/org/oppia/android/app/spotlight/SpotlightFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/spotlight/SpotlightFragmentTest.kt index 1e37adc5e68..9fb017111e3 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/spotlight/SpotlightFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/spotlight/SpotlightFragmentTest.kt @@ -14,6 +14,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import dagger.Component import org.junit.After import org.junit.Before @@ -93,6 +94,7 @@ import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule import org.oppia.android.util.parser.image.ImageParsingModule +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -339,6 +341,25 @@ class SpotlightFragmentTest { } } + @Test + fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() { + TestPlatformParameterModule.forceEnableSpotlightUi(true) + launch( + createSpotlightFragmentTestActivity(context) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + + scenario.onActivity { activity -> + val spotlightFragment = activity.supportFragmentManager + .findFragmentByTag(SpotlightManager.SPOTLIGHT_FRAGMENT_TAG) as SpotlightFragment + val receivedInternalProfileId = spotlightFragment + .arguments?.extractCurrentUserProfileId()?.internalId ?: -1 + + assertThat(receivedInternalProfileId).isEqualTo(0) + } + } + } + private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext().inject(this) } diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt index c93e0f8460a..c8013552937 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt @@ -67,6 +67,7 @@ import org.oppia.android.app.customview.LessonThumbnailImageView import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.StoryFragmentArguments import org.oppia.android.app.player.exploration.ExplorationActivity import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPosition @@ -132,6 +133,7 @@ import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.accessibility.FakeAccessibilityService import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.EventLoggingConfigurationModule @@ -144,6 +146,7 @@ import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule import org.oppia.android.util.parser.image.ImageLoader import org.oppia.android.util.parser.image.ImageParsingModule import org.oppia.android.util.parser.image.ImageTransformation +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -772,6 +775,45 @@ class StoryFragmentTest { } } + @Test + fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() { + launch(createFractionsStoryActivityIntent()).use { scenario -> + testCoroutineDispatchers.runCurrent() + + scenario.onActivity { activity -> + val storyFragment = activity.supportFragmentManager + .findFragmentById(R.id.story_fragment_placeholder) as StoryFragment + + val arguments = checkNotNull(storyFragment.arguments) { + "Expected arguments to be passed to StoryFragment." + } + val args = arguments.getProto( + StoryFragment.STORY_FRAGMENT_ARGUMENTS_KEY, + StoryFragmentArguments.getDefaultInstance() + ) + + val receivedInternalProfileId = arguments.extractCurrentUserProfileId().internalId + val receivedClassroomId = + checkNotNull(args.classroomId) { + "Expected classroomId to be passed to StoryFragment." + } + val receivedTopicId = + checkNotNull(args.topicId) { + "Expected topicId to be passed to StoryFragment." + } + val receivedStoryId = + checkNotNull(args.storyId) { + "Expected storyId to be passed to StoryFragment." + } + + assertThat(receivedInternalProfileId).isEqualTo(internalProfileId) + assertThat(receivedClassroomId).isEqualTo(TEST_CLASSROOM_ID_1) + assertThat(receivedTopicId).isEqualTo(FRACTIONS_TOPIC_ID) + assertThat(receivedStoryId).isEqualTo(FRACTIONS_STORY_ID_0) + } + } + } + @Config(qualifiers = "+sw600dp") @Test // TODO(#4212): Error -> No views in hierarchy found matching fun testStoryFragment_completedChapter_checkProgressDrawableIsCorrect() { diff --git a/app/src/sharedTest/java/org/oppia/android/app/survey/SurveyFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/survey/SurveyFragmentTest.kt index b600558c0da..332914c0c7c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/survey/SurveyFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/survey/SurveyFragmentTest.kt @@ -48,6 +48,7 @@ import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.ScreenName +import org.oppia.android.app.model.SurveyFragmentArguments import org.oppia.android.app.model.SurveyQuestionName import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView @@ -101,6 +102,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName @@ -113,6 +115,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -535,6 +538,32 @@ class SurveyFragmentTest { } } + @Test + fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() { + launch( + createSurveyActivityIntent() + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val surveyFragment = activity.supportFragmentManager + .findFragmentById(R.id.survey_fragment_placeholder) as SurveyFragment + val args = surveyFragment.arguments!!.getProto( + SurveyFragment.SURVEY_FRAGMENT_ARGUMENTS_KEY, + SurveyFragmentArguments.getDefaultInstance() + ) + val receivedInternalProfileId = surveyFragment.arguments!! + .extractCurrentUserProfileId().internalId + val receivedTopicId = args.topicId!! + val receivedExplorationId = args.explorationId!! + + assertThat(receivedInternalProfileId).isEqualTo(0) + assertThat(receivedTopicId).isEqualTo(TEST_TOPIC_ID_0) + assertThat(receivedExplorationId).isEqualTo(TEST_EXPLORATION_ID_2) + } + } + } + private fun selectNpsAnswerAndMoveToNextQuestion(npsScore: Int) { onView( allOf( diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt index 89403f4efe2..08c49683c59 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt @@ -18,6 +18,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import dagger.Component import org.hamcrest.Matchers.allOf import org.junit.After @@ -39,6 +40,8 @@ import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule import org.oppia.android.app.help.thirdparty.LicenseListActivity import org.oppia.android.app.help.thirdparty.ThirdPartyDependencyListActivity +import org.oppia.android.app.help.thirdparty.ThirdPartyDependencyListFragment +import org.oppia.android.app.model.ThirdPartyDependencyListFragmentArguments import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPosition import org.oppia.android.app.shim.ViewBindingShimModule @@ -86,6 +89,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.EventLoggingConfigurationModule @@ -447,6 +451,30 @@ class ThirdPartyDependencyListFragmentTest { } } + @Test + fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() { + launch(ThirdPartyDependencyListActivity::class.java).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val thirdPartyDependencyListFragment = activity.supportFragmentManager + .findFragmentById(R.id.third_party_dependency_list_fragment_placeholder) + as ThirdPartyDependencyListFragment + + val arguments = checkNotNull(thirdPartyDependencyListFragment.arguments) { + "Expected arguments to be passed to ThirdPartyDependencyListFragment" + } + val args = arguments.getProto( + "ThirdPartyDependencyListFragment.arguments", + ThirdPartyDependencyListFragmentArguments.getDefaultInstance() + ) + val receivedIsMultipane = args?.isMultipane ?: false + + assertThat(receivedIsMultipane).isEqualTo(false) + } + } + } + private fun retrieveDependencyName(id: Int): String { return ApplicationProvider.getApplicationContext() .resources.getString(id) diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt index 29dde65b725..8be20c6dc55 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt @@ -26,6 +26,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import dagger.Component import dagger.Module import dagger.Provides @@ -58,6 +59,7 @@ import org.oppia.android.app.model.OptionsActivityParams import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.model.RevisionCardActivityParams +import org.oppia.android.app.model.RevisionCardFragmentArguments import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.app.options.OptionsActivity import org.oppia.android.app.player.exploration.ExplorationActivity @@ -122,6 +124,7 @@ import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.LoadImagesFromAssets import org.oppia.android.util.caching.LoadLessonProtosFromAssets +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.EventLoggingConfigurationModule @@ -133,6 +136,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -810,6 +814,45 @@ class RevisionCardFragmentTest { } } + @Test + fun testFragment_fragmentLoaded_verifyCorrectArgumentsPassed() { + launch( + createRevisionCardActivityIntent( + context, + profileId.internalId, + FRACTIONS_TOPIC_ID, + subtopicId = 2, + FRACTIONS_SUBTOPIC_LIST_SIZE + ) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val revisionCardFragment = activity.supportFragmentManager + .findFragmentById(R.id.revision_card_fragment_placeholder) as RevisionCardFragment + val arguments = checkNotNull(revisionCardFragment.arguments) { + "Expected arguments to be passed to StoryFragment" + } + val args = arguments.getProto( + RevisionCardFragment.REVISION_CARD_FRAGMENT_ARGUMENTS_KEY, + RevisionCardFragmentArguments.getDefaultInstance() + ) + val receivedTopicId = + checkNotNull(args?.topicId) { + "Expected topicId to be passed to RevisionCardFragment" + } + val receivedSubtopicId = args?.subtopicId ?: -1 + val receivedProfileId = arguments.extractCurrentUserProfileId() + val receivedSubtopicListSize = args?.subtopicListSize ?: -1 + + assertThat(receivedTopicId).isEqualTo(FRACTIONS_TOPIC_ID) + assertThat(receivedSubtopicId).isEqualTo(2) + assertThat(receivedProfileId).isEqualTo(profileId) + assertThat(receivedSubtopicListSize).isEqualTo(FRACTIONS_SUBTOPIC_LIST_SIZE) + } + } + } + /** See the version in StateFragmentTest for documentation details. */ @Suppress("SameParameterValue") private fun openClickableSpan(text: String): ViewAction { diff --git a/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt b/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt index 20cc3ee832e..dc99f75c5ce 100644 --- a/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt +++ b/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt @@ -72,6 +72,32 @@ class ClassroomController @Inject constructor( ) } + /** + * Returns the list of [ClassroomRecord]s currently available in the app. + */ + fun getClassrooms(): List { + return if (loadLessonProtosFromAssets) { + assetRepository.loadProtoFromLocalAssets( + assetName = "classrooms", + baseMessage = ClassroomIdList.getDefaultInstance() + ).classroomIdsList.map { classroomId -> + getClassroomById(classroomId) + } + } else loadClassroomsFromJson() + } + + /** + * Returns the [ClassroomRecord] associated with the given [classroomId]. + */ + fun getClassroomById(classroomId: String): ClassroomRecord { + return if (loadLessonProtosFromAssets) { + assetRepository.tryLoadProtoFromLocalAssets( + assetName = classroomId, + defaultMessage = ClassroomRecord.getDefaultInstance() + ) ?: ClassroomRecord.getDefaultInstance() + } else loadClassroomByIdFromJson(classroomId) + } + /** * Returns the list of [TopicSummary]s currently tracked by the app, possibly up to * [EVICTION_TIME_MILLIS] old. @@ -90,7 +116,7 @@ class ClassroomController @Inject constructor( */ fun getClassroomIdByTopicId(topicId: String): String { var classroomId = "" - loadClassrooms().forEach { + getClassrooms().forEach { if (it.topicPrerequisitesMap.keys.contains(topicId)) { classroomId = it.id } @@ -333,17 +359,6 @@ class ClassroomController @Inject constructor( .build() } - private fun loadClassrooms(): List { - return if (loadLessonProtosFromAssets) { - assetRepository.loadProtoFromLocalAssets( - assetName = "classrooms", - baseMessage = ClassroomIdList.getDefaultInstance() - ).classroomIdsList.map { classroomId -> - loadClassroomById(classroomId) - } - } else loadClassroomsFromJson() - } - private fun loadClassroomsFromJson(): List { // Load the classrooms.json file. val classroomIdsObj = jsonAssetRetriever.loadJsonFromAsset("classrooms.json") @@ -359,27 +374,20 @@ class ClassroomController @Inject constructor( val classroomId = checkNotNull(classroomIds.optString(i)) { "Expected non-null classroom ID at index $i." } - val classroomRecord = loadClassroomById(classroomId) + val classroomRecord = getClassroomById(classroomId) classroomRecords.add(classroomRecord) } return classroomRecords } - private fun loadClassroomById(classroomId: String): ClassroomRecord { - return if (loadLessonProtosFromAssets) { - assetRepository.tryLoadProtoFromLocalAssets( - assetName = classroomId, - defaultMessage = ClassroomRecord.getDefaultInstance() - ) ?: ClassroomRecord.getDefaultInstance() - } else loadClassroomByIdFromJson(classroomId) - } - private fun loadClassroomByIdFromJson(classroomId: String): ClassroomRecord { // Load the classroom obj. val classroomObj = jsonAssetRetriever.loadJsonFromAsset("$classroomId.json") checkNotNull(classroomObj) { "Failed to load $classroomId.json." } + val classroomTitle = classroomObj.getJSONObject("classroom_title") + // Load the topic prerequisite map. val topicPrereqsObj = checkNotNull(classroomObj.optJSONObject("topic_prerequisites")) { "Expected classroom to have non-null topic_prerequisites." @@ -398,6 +406,10 @@ class ClassroomController @Inject constructor( id = checkNotNull(classroomObj.optString("classroom_id")) { "Expected classroom to have ID." } + translatableTitle = SubtitledHtml.newBuilder().apply { + contentId = classroomTitle.getStringFromObject("content_id") + html = classroomTitle.getStringFromObject("html") + }.build() putAllTopicPrerequisites( topicPrereqs.mapValues { (_, topicIds) -> ClassroomRecord.TopicIdList.newBuilder().apply { diff --git a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt index 4fa2ed9edef..b82f8f56f0f 100644 --- a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt @@ -5,9 +5,7 @@ import org.json.JSONObject import org.oppia.android.app.model.ChapterPlayState import org.oppia.android.app.model.ChapterProgress import org.oppia.android.app.model.ChapterSummary -import org.oppia.android.app.model.ClassroomIdList import org.oppia.android.app.model.ClassroomRecord -import org.oppia.android.app.model.ClassroomRecord.TopicIdList import org.oppia.android.app.model.ComingSoonTopicList import org.oppia.android.app.model.EphemeralTopicSummary import org.oppia.android.app.model.LessonThumbnail @@ -29,7 +27,7 @@ import org.oppia.android.app.model.TopicProgress import org.oppia.android.app.model.TopicRecord import org.oppia.android.app.model.TopicSummary import org.oppia.android.app.model.UpcomingTopic -import org.oppia.android.domain.classroom.TEST_CLASSROOM_ID_0 +import org.oppia.android.domain.classroom.ClassroomController import org.oppia.android.domain.translation.TranslationController import org.oppia.android.domain.util.JsonAssetRetriever import org.oppia.android.domain.util.getStringFromObject @@ -101,6 +99,7 @@ class TopicListController @Inject constructor( private val oppiaClock: OppiaClock, private val assetRepository: AssetRepository, private val translationController: TranslationController, + private val classroomController: ClassroomController, @LoadLessonProtosFromAssets private val loadLessonProtosFromAssets: Boolean ) { @@ -137,7 +136,7 @@ class TopicListController @Inject constructor( private fun createTopicList(contentLocale: OppiaLocale.ContentLocale): TopicList { return if (loadLessonProtosFromAssets) { - val topicIdList = loadCombinedClassroomsTopicIdList() + val topicIdList = loadCombinedTopicIdList() return TopicList.newBuilder().apply { // Only include topics currently playable in the topic list. addAllTopicSummary( @@ -152,7 +151,7 @@ class TopicListController @Inject constructor( } private fun loadTopicListFromJson(contentLocale: OppiaLocale.ContentLocale): TopicList { - val topicIdList = loadCombinedClassroomsTopicIdList() + val topicIdList = loadCombinedTopicIdList() val topicListBuilder = TopicList.newBuilder() for (topicId in topicIdList) { val ephemeralSummary = createEphemeralTopicSummary(topicId, contentLocale) @@ -166,7 +165,7 @@ class TopicListController @Inject constructor( } private fun computeComingSoonTopicList(): ComingSoonTopicList { - val topicIdList = loadCombinedClassroomsTopicIdList() + val topicIdList = loadCombinedTopicIdList() val comingSoonTopicListBuilder = ComingSoonTopicList.newBuilder() for (topicId in topicIdList) { val upcomingTopicSummary = createUpcomingTopicSummary(topicId) @@ -185,7 +184,7 @@ class TopicListController @Inject constructor( contentLocale: OppiaLocale.ContentLocale ): EphemeralTopicSummary { val topicSummary = createTopicSummary(topicId) - val classroomRecord = loadClassroomById(topicSummary.classroomId) + val classroomRecord = classroomController.getClassroomById(topicSummary.classroomId) return EphemeralTopicSummary.newBuilder().apply { this.topicSummary = topicSummary writtenTranslationContext = @@ -217,7 +216,7 @@ class TopicListController @Inject constructor( this.topicId = topicId putAllWrittenTranslations(topicRecord.writtenTranslationsMap) title = topicRecord.translatableTitle - classroomId = getClassroomIdByTopicId(topicId) + classroomId = classroomController.getClassroomIdByTopicId(topicId) totalChapterCount = storyRecords.map { it.chaptersList.size }.sum() topicThumbnail = topicRecord.topicThumbnail topicPlayAvailability = if (topicRecord.isPublished) { @@ -259,7 +258,7 @@ class TopicListController @Inject constructor( contentId = "title" html = jsonObject.getStringFromObject("topic_name") }.build() - val classroomId = getClassroomIdByTopicId(topicId) + val classroomId = classroomController.getClassroomIdByTopicId(topicId) // No written translations are included since none are retrieved from JSON. return TopicSummary.newBuilder() .setTopicId(topicId) @@ -296,7 +295,7 @@ class TopicListController @Inject constructor( html = jsonObject.getStringFromObject("topic_name") }.build() - val classroomId = getClassroomIdByTopicId(topicId) + val classroomId = classroomController.getClassroomIdByTopicId(topicId) val classroomJsonObject = jsonAssetRetriever.loadJsonFromAsset("$classroomId.json")!! val classroomTitle = classroomJsonObject.getJSONObject("classroom_title").let { @@ -369,8 +368,8 @@ class TopicListController @Inject constructor( sortedTopicProgressList.forEach { topicProgress -> val topic = topicController.retrieveTopic(topicProgress.topicId) val classroom = topic?.topicId?.let { topicId -> - val classroomId = getClassroomIdByTopicId(topicId) - loadClassroomById(classroomId) + val classroomId = classroomController.getClassroomIdByTopicId(topicId) + classroomController.getClassroomById(classroomId) } ?: ClassroomRecord.getDefaultInstance() // Ignore topics that are no longer on the device, or that have been unpublished. if (topic?.topicPlayAvailability?.availabilityCase == AVAILABLE_TO_PLAY_NOW) { @@ -556,7 +555,7 @@ class TopicListController @Inject constructor( * being suggested. */ private fun retrieveTopicDependencies(topicId: String): List { - val classrooms = loadClassrooms() + val classrooms = classroomController.getClassrooms() for (classroom in classrooms) { if (classroom.topicPrerequisitesMap.containsKey(topicId)) { return classroom.topicPrerequisitesMap.getValue(topicId).topicIdsList @@ -589,7 +588,7 @@ class TopicListController @Inject constructor( contentLocale: OppiaLocale.ContentLocale ): List { return if (loadLessonProtosFromAssets) { - val topicIdList = loadCombinedClassroomsTopicIdList() + val topicIdList = loadCombinedTopicIdList() return computeSuggestedStoriesForTopicIds(topicProgressList, topicIdList, contentLocale) } else computeSuggestedStoriesFromJson(topicProgressList, contentLocale) } @@ -599,7 +598,7 @@ class TopicListController @Inject constructor( contentLocale: OppiaLocale.ContentLocale ): List { // All topics that could potentially be recommended. - val topicIdList = loadCombinedClassroomsTopicIdList() + val topicIdList = loadCombinedTopicIdList() return computeSuggestedStoriesForTopicIds(topicProgressList, topicIdList, contentLocale) } @@ -713,7 +712,7 @@ class TopicListController @Inject constructor( ) val classroomRecord = assetRepository.loadProtoFromLocalAssets( - assetName = getClassroomIdByTopicId(topicId), + assetName = classroomController.getClassroomIdByTopicId(topicId), baseMessage = ClassroomRecord.getDefaultInstance() ) return PromotedStory.newBuilder().apply { @@ -783,7 +782,7 @@ class TopicListController @Inject constructor( }.build() } ?: SubtitledHtml.getDefaultInstance() - val classroomId = getClassroomIdByTopicId(topicId) + val classroomId = classroomController.getClassroomIdByTopicId(topicId) val classroomJson = jsonAssetRetriever.loadJsonFromAsset("$classroomId.json") if (classroomJson!!.optString("classroom_title").isNullOrEmpty()) return null @@ -867,104 +866,8 @@ class TopicListController @Inject constructor( .build() } - private fun getClassroomIdByTopicId(topicId: String): String { - var classroomId = TEST_CLASSROOM_ID_0 - loadClassrooms().forEach { - if (it.topicPrerequisitesMap.keys.contains(topicId)) { - classroomId = it.id - } - } - return classroomId - } - - // TODO(#5344): Remove this in favor of per-classroom data handling. - private fun loadClassrooms(): List { - return if (loadLessonProtosFromAssets) { - assetRepository.loadProtoFromLocalAssets( - assetName = "classrooms", - baseMessage = ClassroomIdList.getDefaultInstance() - ).classroomIdsList.map { classroomId -> - loadClassroomById(classroomId) - } - } else loadClassroomsFromJson() - } - - // TODO(#5344): Remove this in favor of per-classroom data handling. - private fun loadClassroomsFromJson(): List { - // Load the classrooms.json file. - val classroomIdsObj = jsonAssetRetriever.loadJsonFromAsset("classrooms.json") - checkNotNull(classroomIdsObj) { "Failed to load classrooms.json." } - val classroomIds = classroomIdsObj.optJSONArray("classroom_id_list") - checkNotNull(classroomIds) { "classrooms.json is missing classroom IDs." } - - // Initialize a list to store the [ClassroomRecord]s. - val classroomRecords = mutableListOf() - - // Iterate over all classroomIds and load each classroom's JSON. - for (i in 0 until classroomIds.length()) { - val classroomId = checkNotNull(classroomIds.optString(i)) { - "Expected non-null classroom ID at index $i." - } - val classroomRecord = loadClassroomById(classroomId) - classroomRecords.add(classroomRecord) - } - - return classroomRecords - } - - // TODO(#5344): Move this to classroom controller. - private fun loadClassroomById(classroomId: String): ClassroomRecord { - return if (loadLessonProtosFromAssets) { - assetRepository.tryLoadProtoFromLocalAssets( - assetName = classroomId, - defaultMessage = ClassroomRecord.getDefaultInstance() - ) ?: ClassroomRecord.getDefaultInstance() - } else loadClassroomByIdFromJson(classroomId) - } - - // TODO(#5344): Remove this in favor of per-classroom data handling. - private fun loadClassroomByIdFromJson(classroomId: String): ClassroomRecord { - // Load the classroom obj. - val classroomObj = jsonAssetRetriever.loadJsonFromAsset("$classroomId.json") - checkNotNull(classroomObj) { "Failed to load $classroomId.json." } - - val classroomTitle = classroomObj.getJSONObject("classroom_title") - - // Load the topic prerequisite map. - val topicPrereqsObj = checkNotNull(classroomObj.optJSONObject("topic_prerequisites")) { - "Expected classroom to have non-null topic_prerequisites." - } - val topicPrereqs = topicPrereqsObj.keys().asSequence().associateWith { topicId -> - val topicIdArray = checkNotNull(topicPrereqsObj.optJSONArray(topicId)) { - "Expected topic $topicId to have a non-null string list." - } - return@associateWith List(topicIdArray.length()) { index -> - checkNotNull(topicIdArray.optString(index)) { - "Expected topic $topicId to have non-null string at index $index." - } - } - } - return ClassroomRecord.newBuilder().apply { - id = checkNotNull(classroomObj.optString("classroom_id")) { - "Expected classroom to have ID." - } - translatableTitle = SubtitledHtml.newBuilder().apply { - contentId = classroomTitle.getStringFromObject("content_id") - html = classroomTitle.getStringFromObject("html") - }.build() - putAllTopicPrerequisites( - topicPrereqs.mapValues { (_, topicIds) -> - TopicIdList.newBuilder().apply { - addAllTopicIds(topicIds) - }.build() - } - ) - }.build() - } - - // TODO(#5344): Remove this in favor of per-classroom data handling. - private fun loadCombinedClassroomsTopicIdList(): List = - loadClassrooms().flatMap { it.topicPrerequisitesMap.keys.toList() } + private fun loadCombinedTopicIdList(): List = + classroomController.getClassrooms().flatMap { it.topicPrerequisitesMap.keys.toList() } } internal fun createTopicThumbnailFromJson(topicJsonObject: JSONObject): LessonThumbnail { diff --git a/domain/src/test/java/org/oppia/android/domain/classroom/ClassroomControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/classroom/ClassroomControllerTest.kt index 24129abc307..6c1cee650bf 100644 --- a/domain/src/test/java/org/oppia/android/domain/classroom/ClassroomControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classroom/ClassroomControllerTest.kt @@ -131,6 +131,23 @@ class ClassroomControllerTest { assertThat(classroomList.classroomSummaryList.size).isEqualTo(2) } + @Test + fun testGetClassrooms_returnsAllClassrooms() { + val classrooms = classroomController.getClassrooms() + + assertThat(classrooms[0].id).isEqualTo(TEST_CLASSROOM_ID_0) + assertThat(classrooms[1].id).isEqualTo(TEST_CLASSROOM_ID_1) + assertThat(classrooms[2].id).isEqualTo(TEST_CLASSROOM_ID_2) + } + + @Test + fun testGetClassroomById_hasCorrectClassroomInfo() { + val classroom = classroomController.getClassroomById(TEST_CLASSROOM_ID_0) + + assertThat(classroom.id).isEqualTo(TEST_CLASSROOM_ID_0) + assertThat(classroom.translatableTitle.html).isEqualTo("Science") + } + @Test fun testRetrieveTopicList_isSuccessful() { val topicListProvider = classroomController.getTopicList(profileId0, TEST_CLASSROOM_ID_0) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 9a9918f2f64..d4e3597a664 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -116,6 +116,11 @@ file_content_checks { failure_message: "All plurals outside strings.xml must be marked as not translatable, or moved to strings.xml." exempted_file_patterns: "app/src/main/res/values.*?/strings\\.xml" } +file_content_checks { + file_path_regex: "app/src/main/res/values/strings\\.xml" + prohibited_content_regex: "CDATA" + failure_message: "CDATA isn't handled by Translatewiki correctly. Use escaped HTML, instead." +} file_content_checks { file_path_regex: ".+?\\.kt" prohibited_content_regex: "android.text.BidiFormatter" diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh new file mode 100644 index 00000000000..26061ef5c09 --- /dev/null +++ b/scripts/pre-commit.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Pre-commit hook to check for binary files. + +# Find the common ancestor between develop and the current branch +base_commit=$(git merge-base 'origin/develop' HEAD) + +# Get the list of staged changes (files ready to be committed) +staged_files=$(git diff --cached --name-only) + +# Get the list of changed files compared to the base commit +changed_files=$(git diff --name-only "$base_commit" HEAD) + +# Combine both lists of files, ensuring no duplicates +all_files=$(echo -e "$staged_files\n$changed_files" | sort -u) + +function checkForBinaries() { + binaryFilesCount=0 + + # Iterate over all files (both staged and changed) + for file in $all_files; do + if [ -f "$file" ] && file --mime "$file" | grep -q 'binary'; then + ((binaryFilesCount++)) + printf "\n\033[33m%s\033[0m" "$file" + fi + done + + if [[ "${binaryFilesCount}" -gt 0 ]]; then + printf "\n\nPlease remove the %d detected binary file(s)." "$binaryFilesCount" + printf "\nBINARY FILES CHECK FAILED" + exit 1 + fi +} + +checkForBinaries diff --git a/scripts/setup.sh b/scripts/setup.sh index 8c3ef595e1d..9f822095f32 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -13,6 +13,9 @@ # Move file from script folder to .git/hooks folder cp scripts/pre-push.sh .git/hooks/pre-push +# Copy the pre-commit hook from script to .git/hooks folder +cp scripts/pre-commit.sh .git/hooks/pre-commit + # Create a folder where all the set up files will be downloaded mkdir -p ../oppia-android-tools cd ../oppia-android-tools diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt index 4e96123650e..5f47d4b81e1 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt @@ -222,6 +222,8 @@ class RegexPatternValidationCheckTest { private val referenceComputeIfAbsent = "computeIfAbsent won't desugar and requires Java 8 support (SDK 24+). Suggest using an atomic" + " Kotlin-specific solution, instead." + private val cdataShouldNotBeUsed = + "CDATA isn't handled by Translatewiki correctly. Use escaped HTML, instead." private val wikiReferenceNote = "Refer to https://github.com/oppia/oppia-android/wiki/Static-Analysis-Checks" + "#regexpatternvalidation-check for more details on how to fix this." @@ -2752,6 +2754,28 @@ class RegexPatternValidationCheckTest { ) } + @Test + fun testFileContent_includesCdataContentInStringsXml_fileContentIsNotCorrect() { + val prohibitedContent = + """ + Some nested HTML.

        ]]>
        + """.trimIndent() + tempFolder.newFolder("testfiles", "app", "src", "main", "res", "values") + val stringFilePath = "app/src/main/res/values/strings.xml" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows() { runScript() } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $cdataShouldNotBeUsed + $wikiReferenceNote + """.trimIndent() + ) + } + /** Runs the regex_pattern_validation_check. */ private fun runScript() { main(File(tempFolder.root, "testfiles").absolutePath)