diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bec340490e4..ed28c574bd5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -50,9 +50,6 @@ gradlew.bat @BenHenning # Devbots configurations. /.devbots/ @BenHenning -# All tests. -*Test.kt @anandwana001 - # All resource files. /app/src/main/res/**/*.xml @rt4914 /utility/src/main/res/**/*.xml @rt4914 @@ -72,7 +69,7 @@ gradlew.bat @BenHenning *Manifest.xml @BenHenning # Linter configuration. -buf.yaml @anandwana001 +buf.yaml @BenHenning # Third-party dependencies. /third_party/ @BenHenning @@ -108,8 +105,8 @@ config/kitkat_main_dex_class_list.txt @BenHenning ##################################################################################### # Global app module code ownership. -/app/**/*.kt @rt4914 -/app/**/*.java @rt4914 +/app/**/*.kt @rt4914 @BenHenning +/app/**/*.java @rt4914 @BenHenning # State players. /app/src/*/java/org/oppia/android/app/player/ @BenHenning @@ -128,8 +125,8 @@ config/kitkat_main_dex_class_list.txt @BenHenning # Databinding adapters. /app/src/main/java/org/oppia/android/app/databinding/ @BenHenning -# App deprecation functionality. -/app/src/*/java/org/oppia/android/app/deprecation/ @BenHenning +# App notices functionality (such as for deprecations). +/app/src/*/java/org/oppia/android/app/notice/ @BenHenning # Parsing functionality needed for interactions. /app/src/*/java/org/oppia/android/app/parser/ @BenHenning @@ -144,7 +141,7 @@ config/kitkat_main_dex_class_list.txt @BenHenning /app/src/*/java/org/oppia/android/app/viewmodel/ @BenHenning # App testing infrastructure. -/app/src/*/java/org/oppia/android/app/testing/ @anandwana001 +/app/src/*/java/org/oppia/android/app/testing/ @BenHenning ##################################################################################### # domain module # @@ -153,9 +150,6 @@ config/kitkat_main_dex_class_list.txt @BenHenning # Global domain module code ownership. /domain/**/*.kt @BenHenning -# Domain test resources. -/domain/src/test/res/values/strings.xml @BenHenning - # Questions support. /domain/src/*/java/org/oppia/android/domain/question/ @BenHenning @@ -167,7 +161,7 @@ config/kitkat_main_dex_class_list.txt @BenHenning ##################################################################################### # Global testing module code ownership. -/testing/**/*.kt @anandwana001 @BenHenning +/testing/**/*.kt @BenHenning ##################################################################################### # data module # @@ -184,14 +178,17 @@ config/kitkat_main_dex_class_list.txt @BenHenning # Global utility module code ownership. /utility/**/*.kt @BenHenning +# Utility test resources. +/utility/src/test/res/values/strings.xml @BenHenning + # Accessibility utilities. -/utility/src/*/java/org/oppia/android/util/accessibility/ @rt4914 +/utility/src/*/java/org/oppia/android/util/accessibility/ @rt4914 @BenHenning # Core logging infrastructure. /utility/src/*/java/org/oppia/android/util/logging/ @BenHenning # Miscellaneous statusbar UI utilities. -/utility/src/*/java/org/oppia/android/util/statusbar/ @rt4914 +/utility/src/*/java/org/oppia/android/util/statusbar/ @rt4914 @BenHenning ##################################################################################### # scripts # @@ -200,9 +197,6 @@ config/kitkat_main_dex_class_list.txt @BenHenning # Global scripts code ownership. /scripts/ @BenHenning -# Shell file ownership. -/scripts/**/*.sh @anandwana001 @BenHenning - # Script proto ownership. /scripts/**/*.proto @BenHenning @@ -225,7 +219,7 @@ config/kitkat_main_dex_class_list.txt @BenHenning ##################################################################################### # End-to-end test utilities and modules. -/instrumentation/src/java/**/*.kt @anandwana001 @BenHenning +/instrumentation/**/*.kt @BenHenning ##################################################################################### # global overrides # diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml index 4fa60d192e0..fdaebd4af7b 100644 --- a/.github/workflows/build_tests.yml +++ b/.github/workflows/build_tests.yml @@ -443,10 +443,24 @@ jobs: run: | bazel build --compilation_mode=opt -- //:oppia_alpha_kitkat + # Note that caching only works on non-forks. + - name: Build Oppia alpha Kenya-specific AAB (with caching, non-fork only) + if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }} + env: + BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + run: | + bazel build --compilation_mode=opt --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia_alpha_kenya + + - name: Build Oppia alpha Kenya-specific AAB (without caching, or on a fork) + if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }} + run: | + bazel build --compilation_mode=opt -- //:oppia_alpha_kenya + - name: Copy Oppia alpha AABs for uploading run: | cp $GITHUB_WORKSPACE/bazel-bin/oppia_alpha.aab /home/runner/work/oppia-android/oppia-android/ cp $GITHUB_WORKSPACE/bazel-bin/oppia_alpha_kitkat.aab /home/runner/work/oppia-android/oppia-android/ + cp $GITHUB_WORKSPACE/bazel-bin/oppia_alpha_kenya.aab /home/runner/work/oppia-android/oppia-android/ - uses: actions/upload-artifact@v2 with: @@ -457,3 +471,268 @@ jobs: with: name: oppia_alpha_kitkat.aab path: /home/runner/work/oppia-android/oppia-android/oppia_alpha_kitkat.aab + + - uses: actions/upload-artifact@v2 + with: + name: oppia_alpha_kenya.aab + path: /home/runner/work/oppia-android/oppia-android/oppia_alpha_kenya.aab + + build_oppia_beta_aab: + name: Build Oppia AAB (beta flavor) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04] + env: + ENABLE_CACHING: false + CACHE_DIRECTORY: ~/.bazel_cache + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up JDK 9 + uses: actions/setup-java@v1 + with: + java-version: 9 + + - name: Set up Bazel + uses: abhinavsingh/setup-bazel@v3 + with: + version: 4.0.0 + + - name: Set up build environment + uses: ./.github/actions/set-up-android-bazel-build-environment + + # For reference on this & the later cache actions, see: + # https://github.com/actions/cache/issues/239#issuecomment-606950711 & + # https://github.com/actions/cache/issues/109#issuecomment-558771281. Note that these work + # with Bazel since Bazel can share the most recent cache from an unrelated build and still + # benefit from incremental build performance (assuming that actions/cache aggressively removes + # older caches due to the 5GB cache limit size & Bazel's large cache size). + - uses: actions/cache@v2 + id: cache + with: + path: ${{ env.CACHE_DIRECTORY }} + key: ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-binary-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-binary- + ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-tests- + ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel- + + # This check is needed to ensure that Bazel's unbounded cache growth doesn't result in a + # situation where the cache never updates (e.g. due to exceeding GitHub's cache size limit) + # thereby only ever using the last successful cache version. This solution will result in a + # few slower CI actions around the time cache is detected to be too large, but it should + # incrementally improve thereafter. + - name: Ensure cache size + env: + BAZEL_CACHE_DIR: ${{ env.CACHE_DIRECTORY }} + run: | + # See https://stackoverflow.com/a/27485157 for reference. + EXPANDED_BAZEL_CACHE_PATH="${BAZEL_CACHE_DIR/#\~/$HOME}" + CACHE_SIZE_MB=$(du -smc $EXPANDED_BAZEL_CACHE_PATH | grep total | cut -f1) + echo "Total size of Bazel cache (rounded up to MBs): $CACHE_SIZE_MB" + # Use a 4.5GB threshold since actions/cache compresses the results, and Bazel caches seem + # to only increase by a few hundred megabytes across changes for unrelated branches. This + # is also a reasonable upper-bound (local tests as of 2021-03-31 suggest that a full build + # of the codebase (e.g. //...) from scratch only requires a ~2.1GB uncompressed/~900MB + # compressed cache). + if [[ "$CACHE_SIZE_MB" -gt 4500 ]]; then + echo "Cache exceeds cut-off; resetting it (will result in a slow build)" + rm -rf $EXPANDED_BAZEL_CACHE_PATH + fi + + - name: Configure Bazel to use a local cache + env: + BAZEL_CACHE_DIR: ${{ env.CACHE_DIRECTORY }} + run: | + EXPANDED_BAZEL_CACHE_PATH="${BAZEL_CACHE_DIR/#\~/$HOME}" + echo "Using $EXPANDED_BAZEL_CACHE_PATH as Bazel's cache path" + echo "build --disk_cache=$EXPANDED_BAZEL_CACHE_PATH" >> $HOME/.bazelrc + shell: bash + + - name: Check Bazel environment + run: bazel info + + # See https://git-secret.io/installation for details on installing git-secret. Note that the + # apt-get method isn't used since it's much slower to update & upgrade apt before installation + # versus just directly cloning & installing the project. Further, the specific version + # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. + # This also uses a different directory to install git-secret to avoid requiring root access + # when running the git secret command. + - name: Install git-secret (non-fork only) + if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }} + shell: bash + run: | + cd $HOME + mkdir -p $HOME/gitsecret + git clone https://github.com/sobolevn/git-secret.git git-secret + cd git-secret && make build + PREFIX="$HOME/gitsecret" make install + echo "$HOME/gitsecret" >> $GITHUB_PATH + echo "$HOME/gitsecret/bin" >> $GITHUB_PATH + + - name: Decrypt secrets (non-fork only) + if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }} + env: + GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} + run: | + cd $HOME + # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! + echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg + gpg --import ./git_secret_private_key.gpg + cd $GITHUB_WORKSPACE + git secret reveal + + # Note that caching only works on non-forks. + - name: Build Oppia beta AAB (with caching, non-fork only) + if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }} + env: + BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + run: | + bazel build --compilation_mode=opt --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia_beta + + - name: Build Oppia beta AAB (without caching, or on a fork) + if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }} + run: | + bazel build --compilation_mode=opt -- //:oppia_beta + + - name: Copy Oppia beta AAB for uploading + run: | + cp $GITHUB_WORKSPACE/bazel-bin/oppia_beta.aab /home/runner/work/oppia-android/oppia-android/ + + - uses: actions/upload-artifact@v2 + with: + name: oppia_beta.aab + path: /home/runner/work/oppia-android/oppia-android/oppia_beta.aab + + build_oppia_ga_aab: + name: Build Oppia AAB (GA flavor) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04] + env: + ENABLE_CACHING: false + CACHE_DIRECTORY: ~/.bazel_cache + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up JDK 9 + uses: actions/setup-java@v1 + with: + java-version: 9 + + - name: Set up Bazel + uses: abhinavsingh/setup-bazel@v3 + with: + version: 4.0.0 + + - name: Set up build environment + uses: ./.github/actions/set-up-android-bazel-build-environment + + # For reference on this & the later cache actions, see: + # https://github.com/actions/cache/issues/239#issuecomment-606950711 & + # https://github.com/actions/cache/issues/109#issuecomment-558771281. Note that these work + # with Bazel since Bazel can share the most recent cache from an unrelated build and still + # benefit from incremental build performance (assuming that actions/cache aggressively removes + # older caches due to the 5GB cache limit size & Bazel's large cache size). + - uses: actions/cache@v2 + id: cache + with: + path: ${{ env.CACHE_DIRECTORY }} + key: ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-binary-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-binary- + ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-tests- + ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel- + + # This check is needed to ensure that Bazel's unbounded cache growth doesn't result in a + # situation where the cache never updates (e.g. due to exceeding GitHub's cache size limit) + # thereby only ever using the last successful cache version. This solution will result in a + # few slower CI actions around the time cache is detected to be too large, but it should + # incrementally improve thereafter. + - name: Ensure cache size + env: + BAZEL_CACHE_DIR: ${{ env.CACHE_DIRECTORY }} + run: | + # See https://stackoverflow.com/a/27485157 for reference. + EXPANDED_BAZEL_CACHE_PATH="${BAZEL_CACHE_DIR/#\~/$HOME}" + CACHE_SIZE_MB=$(du -smc $EXPANDED_BAZEL_CACHE_PATH | grep total | cut -f1) + echo "Total size of Bazel cache (rounded up to MBs): $CACHE_SIZE_MB" + # Use a 4.5GB threshold since actions/cache compresses the results, and Bazel caches seem + # to only increase by a few hundred megabytes across changes for unrelated branches. This + # is also a reasonable upper-bound (local tests as of 2021-03-31 suggest that a full build + # of the codebase (e.g. //...) from scratch only requires a ~2.1GB uncompressed/~900MB + # compressed cache). + if [[ "$CACHE_SIZE_MB" -gt 4500 ]]; then + echo "Cache exceeds cut-off; resetting it (will result in a slow build)" + rm -rf $EXPANDED_BAZEL_CACHE_PATH + fi + + - name: Configure Bazel to use a local cache + env: + BAZEL_CACHE_DIR: ${{ env.CACHE_DIRECTORY }} + run: | + EXPANDED_BAZEL_CACHE_PATH="${BAZEL_CACHE_DIR/#\~/$HOME}" + echo "Using $EXPANDED_BAZEL_CACHE_PATH as Bazel's cache path" + echo "build --disk_cache=$EXPANDED_BAZEL_CACHE_PATH" >> $HOME/.bazelrc + shell: bash + + - name: Check Bazel environment + run: bazel info + + # See https://git-secret.io/installation for details on installing git-secret. Note that the + # apt-get method isn't used since it's much slower to update & upgrade apt before installation + # versus just directly cloning & installing the project. Further, the specific version + # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. + # This also uses a different directory to install git-secret to avoid requiring root access + # when running the git secret command. + - name: Install git-secret (non-fork only) + if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }} + shell: bash + run: | + cd $HOME + mkdir -p $HOME/gitsecret + git clone https://github.com/sobolevn/git-secret.git git-secret + cd git-secret && make build + PREFIX="$HOME/gitsecret" make install + echo "$HOME/gitsecret" >> $GITHUB_PATH + echo "$HOME/gitsecret/bin" >> $GITHUB_PATH + + - name: Decrypt secrets (non-fork only) + if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }} + env: + GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} + run: | + cd $HOME + # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! + echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg + gpg --import ./git_secret_private_key.gpg + cd $GITHUB_WORKSPACE + git secret reveal + + # Note that caching only works on non-forks. + - name: Build Oppia GA AAB (with caching, non-fork only) + if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }} + env: + BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + run: | + bazel build --compilation_mode=opt --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia_ga + + - name: Build Oppia GA AAB (without caching, or on a fork) + if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }} + run: | + bazel build --compilation_mode=opt -- //:oppia_ga + + - name: Copy Oppia GA AAB for uploading + run: | + cp $GITHUB_WORKSPACE/bazel-bin/oppia_ga.aab /home/runner/work/oppia-android/oppia-android/ + + - uses: actions/upload-artifact@v2 + with: + name: oppia_ga.aab + path: /home/runner/work/oppia-android/oppia-android/oppia_ga.aab diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml index ff531ffbebb..f9dfbaa86fb 100644 --- a/.github/workflows/static_checks.yml +++ b/.github/workflows/static_checks.yml @@ -180,6 +180,11 @@ jobs: gh issue list --limit 2000 --repo oppia/oppia-android --json number > $(pwd)/open_issues.json bazel run //scripts:todo_open_check -- $(pwd) scripts/assets/todo_open_exemptions.pb open_issues.json + - name: String Resource Validation Check + if: always() + run: | + bazel run //scripts:string_resource_validation_check -- $(pwd) + # 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/BUILD.bazel b/BUILD.bazel index 4dccfd87153..81250077b03 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -75,6 +75,7 @@ package_group( [ transform_android_manifest( name = "oppia_apk_%s_transformed_manifest" % apk_flavor_metadata["flavor"], + application_relative_qualified_class = ".app.application.dev.DeveloperOppiaApplication", build_flavor = apk_flavor_metadata["flavor"], input_file = "//app:src/main/AndroidManifest.xml", major_version = MAJOR_VERSION, @@ -108,7 +109,7 @@ package_group( }, multidex = apk_flavor_metadata["multidex"], deps = [ - "//app", + "//app/src/main/java/org/oppia/android/app/application/dev:developer_application", ], ) for apk_flavor_metadata in [ diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 7c9941d90a8..1f8fc6eeacb 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -110,6 +110,7 @@ LISTENERS = [ "src/main/java/org/oppia/android/app/help/LoadFaqListFragmentListener.kt", "src/main/java/org/oppia/android/app/help/LoadLicenseListFragmentListener.kt", "src/main/java/org/oppia/android/app/help/LoadLicenseTextViewerFragmentListener.kt", + "src/main/java/org/oppia/android/app/help/LoadPoliciesFragmentListener.kt", "src/main/java/org/oppia/android/app/help/LoadThirdPartyDependencyListFragmentListener.kt", "src/main/java/org/oppia/android/app/help/RouteToFAQListListener.kt", "src/main/java/org/oppia/android/app/help/RouteToThirdPartyDependencyListListener.kt", @@ -123,7 +124,8 @@ LISTENERS = [ "src/main/java/org/oppia/android/app/home/topiclist/TopicSummaryClickListener.kt", "src/main/java/org/oppia/android/app/onboarding/OnboardingNavigationListener.kt", "src/main/java/org/oppia/android/app/onboarding/RouteToProfileListListener.kt", - "src/main/java/org/oppia/android/app/options/LanguageRadioButtonListener.kt", + "src/main/java/org/oppia/android/app/options/AppLanguageRadioButtonListener.kt", + "src/main/java/org/oppia/android/app/options/AudioLanguageRadioButtonListener.kt", "src/main/java/org/oppia/android/app/options/LoadAppLanguageListListener.kt", "src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt", "src/main/java/org/oppia/android/app/options/LoadReadingTextSizeListener.kt", @@ -143,6 +145,7 @@ LISTENERS = [ "src/main/java/org/oppia/android/app/player/state/listener/RouteToHintsAndSolutionListener.kt", "src/main/java/org/oppia/android/app/player/state/listener/StateKeyboardButtonListener.kt", "src/main/java/org/oppia/android/app/player/state/listener/SubmitNavigationButtonListener.kt", + "src/main/java/org/oppia/android/app/policies/RouteToPoliciesListener.kt", "src/main/java/org/oppia/android/app/profile/RouteToAdminPinListener.kt", "src/main/java/org/oppia/android/app/profileprogress/ProfilePictureClickListener.kt", "src/main/java/org/oppia/android/app/profileprogress/RouteToCompletedStoryListListener.kt", @@ -207,10 +210,12 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/onboarding/OnboadingSlideViewModel.kt", "src/main/java/org/oppia/android/app/onboarding/OnboardingViewModel.kt", "src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt", + "src/main/java/org/oppia/android/app/options/OptionsReadingTextSizeViewModel.kt", "src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt", "src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt", "src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt", "src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt", + "src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragDropInteractionContentViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt", @@ -218,6 +223,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/player/state/itemviewmodel/PreviousResponsesHeaderViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/SubmittedAnswerViewModel.kt", + "src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt", "src/main/java/org/oppia/android/app/profile/AddProfileViewModel.kt", "src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt", "src/main/java/org/oppia/android/app/profile/ResetPinViewModel.kt", @@ -276,7 +282,7 @@ VIEW_MODELS = [ "src/main/java/org/oppia/android/app/help/thirdparty/LicenseItemViewModel.kt", "src/main/java/org/oppia/android/app/help/thirdparty/ThirdPartyDependencyItemViewModel.kt", "src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionItemViewModel.kt", - "src/main/java/org/oppia/android/app/hintsandsolution/HintsDividerViewModel.kt", + "src/main/java/org/oppia/android/app/hintsandsolution/ReturnToLessonViewModel.kt", "src/main/java/org/oppia/android/app/hintsandsolution/SolutionViewModel.kt", "src/main/java/org/oppia/android/app/home/HomeItemViewModel.kt", "src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicListViewModel.kt", @@ -288,15 +294,15 @@ VIEW_MODELS = [ "src/main/java/org/oppia/android/app/onboarding/OnboardingViewPagerViewModel.kt", "src/main/java/org/oppia/android/app/onboarding/ViewPagerSlide.kt", "src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt", - "src/main/java/org/oppia/android/app/options/LanguageItemViewModel.kt", - "src/main/java/org/oppia/android/app/options/LanguageSelectionViewModel.kt", + "src/main/java/org/oppia/android/app/options/AppLanguageItemViewModel.kt", + "src/main/java/org/oppia/android/app/options/AppLanguageSelectionViewModel.kt", + "src/main/java/org/oppia/android/app/options/AudioLanguageItemViewModel.kt", + "src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt", "src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt", "src/main/java/org/oppia/android/app/options/OptionsAppLanguageViewModel.kt", "src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt", "src/main/java/org/oppia/android/app/options/OptionsItemViewModel.kt", - "src/main/java/org/oppia/android/app/options/OptionsReadingTextSizeViewModel.kt", "src/main/java/org/oppia/android/app/options/ReadingTextSizeSelectionViewModel.kt", - "src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt", "src/main/java/org/oppia/android/app/player/exploration/ExplorationViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContentViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt", @@ -312,7 +318,6 @@ VIEW_MODELS = [ "src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/SubmitButtonViewModel.kt", - "src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt", "src/main/java/org/oppia/android/app/player/state/StateViewModel.kt", "src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestViewModel.kt", "src/main/java/org/oppia/android/app/profile/AdminAuthViewModel.kt", @@ -573,6 +578,7 @@ android_library( "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", "//model/src/main/proto:interaction_object_java_proto_lite", "//model/src/main/proto:thumbnail_java_proto_lite", + "//model/src/main/proto:version_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_core_core", @@ -603,6 +609,7 @@ kt_android_library( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/audio:cellular_audio_dialog_controller", + "//model/src/main/proto:arguments_java_proto_lite", "//model/src/main/proto:question_java_proto_lite", "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_recyclerview_recyclerview", @@ -630,9 +637,6 @@ kt_android_library( "//third_party:androidx_databinding_databinding-runtime", "//third_party:circularimageview_circular_image_view", "//utility/src/main/java/org/oppia/android/util/accessibility", - "//utility/src/main/java/org/oppia/android/util/caching:caching_prod_module", - "//utility/src/main/java/org/oppia/android/util/logging:prod_module", - "//utility/src/main/java/org/oppia/android/util/logging/firebase:prod_module", "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser", "//utility/src/main/java/org/oppia/android/util/parser/image:image_loader", "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_annonations", @@ -668,27 +672,25 @@ kt_android_library( ":listeners", ":resources", "//app/src/main/java/org/oppia/android/app/shim:intent_factory_shim", + "//app/src/main/java/org/oppia/android/app/utility/datetime:date_time_util", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", "//app/src/main/java/org/oppia/android/app/viewmodel:observable_array_list", "//app/src/main/java/org/oppia/android/app/viewmodel:observable_view_model", "//app/src/main/java/org/oppia/android/app/viewmodel:view_model_provider", - "//app/src/main/java/org/oppia/android/app/utility/datetime:date_time_util", - "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", "//domain", "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", "//domain/src/main/java/org/oppia/android/domain/clipboard:clipboard_controller", "//domain/src/main/java/org/oppia/android/domain/onboarding:state_controller", "//domain/src/main/java/org/oppia/android/domain/profile:profile_management_controller", + "//model/src/main/proto:arguments_java_proto_lite", "//third_party:androidx_core_core", "//third_party:androidx_databinding_databinding-common", "//third_party:androidx_databinding_databinding-runtime", "//utility", "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_event_logger", - "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_module", "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", - # TODO(#59): Remove 'debug_util_module' once we completely migrate to Bazel from Gradle as - # we can then directly exclude debug files from the build and thus won't be requiring this module. - "//utility/src/main/java/org/oppia/android/util/networking:debug_util_module", + "//utility/src/main/java/org/oppia/android/util/networking:network_connection_debug_util", "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser", ], ) @@ -757,34 +759,11 @@ kt_android_library( "//app/src/main/java/org/oppia/android/app/fragment:injectable_dialog_fragment", "//app/src/main/java/org/oppia/android/app/fragment:injectable_fragment", "//app/src/main/java/org/oppia/android/app/shim:prod_modules", - "//app/src/main/java/org/oppia/android/app/translation:prod_module", - "//data/src/main/java/org/oppia/android/data/backends/gae:network_config_prod_module", - "//data/src/main/java/org/oppia/android/data/backends/gae:prod_module", - "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", - "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", - "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector", - "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector_provider", - "//domain/src/main/java/org/oppia/android/domain/onboarding:retriever_prod_module", + "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//domain/src/main/java/org/oppia/android/domain/onboarding:state_controller", "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:logger_module", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", - "//model/src/main/proto:arguments_java_proto_lite", "//domain/src/main/java/org/oppia/android/domain/profile:profile_management_controller", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:prod_module", - "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", + "//model/src/main/proto:arguments_java_proto_lite", "//third_party:androidx_databinding_databinding-adapters", "//third_party:androidx_databinding_databinding-common", "//third_party:androidx_databinding_databinding-runtime", @@ -794,26 +773,14 @@ kt_android_library( "//third_party:androidx_multidex_multidex", "//third_party:androidx_viewpager2_viewpager2", "//third_party:androidx_viewpager_viewpager", - "//third_party:androidx_work_work-runtime-ktx", "//third_party:com_caverock_androidsvg", "//third_party:com_google_android_flexbox_flexbox", "//third_party:javax_annotation_javax_annotation-api_jar", "//utility", - "//utility/src/main/java/org/oppia/android/util/accessibility:prod_module", - "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", "//utility/src/main/java/org/oppia/android/util/extensions:bundle_extensions", - "//utility/src/main/java/org/oppia/android/util/locale:prod_module", - "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser_entity_type_module", "//utility/src/main/java/org/oppia/android/util/parser/image:image_loader", - "//utility/src/main/java/org/oppia/android/util/parser/image:glide_image_loader_module", - "//utility/src/main/java/org/oppia/android/util/parser/image:repository_glide_module", "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_annonations", - "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_module", - # TODO(#2432): Replace debug_module with prod_module when building the app in prod mode. - "//utility/src/main/java/org/oppia/android/util/networking:debug_module", - "//utility/src/main/java/org/oppia/android/util/logging:console_logger_injector_provider", "//utility/src/main/java/org/oppia/android/util/statusbar:status_bar_color", - "//utility/src/main/java/org/oppia/android/util/threading:dispatcher_injector_provider", ], ) @@ -858,6 +825,11 @@ TEST_DEPS = [ ":dagger", ":resources", ":test_deps", + "//app/src/main/java/org/oppia/android/app/application:application_component", + "//app/src/main/java/org/oppia/android/app/application:application_injector", + "//app/src/main/java/org/oppia/android/app/application:application_injector_provider", + "//app/src/main/java/org/oppia/android/app/application:common_application_modules", + "//app/src/main/java/org/oppia/android/app/application/testing:testing_build_flavor_module", "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", @@ -891,6 +863,7 @@ TEST_DEPS = [ "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", + "//testing/src/main/java/org/oppia/android/testing/platformparameter:test_module", "//testing/src/main/java/org/oppia/android/testing/robolectric:is_on_robolectric", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:coroutine_executor_service", @@ -923,15 +896,20 @@ TEST_DEPS = [ "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/logging:standard_event_logging_configuration_module", + "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_module", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", - "//utility/src/main/java/org/oppia/android/util/parser/html:custom_bullet_span", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser", "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser_entity_type_module", + "//utility/src/main/java/org/oppia/android/util/parser/html:list_item_leading_margin_span", + "//utility/src/main/java/org/oppia/android/util/parser/html:policy_type", "//utility/src/main/java/org/oppia/android/util/parser/image:glide_image_loader", "//utility/src/main/java/org/oppia/android/util/parser/image:glide_image_loader_module", "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_module", "//utility/src/main/java/org/oppia/android/util/parser/image:image_transformation", "//utility/src/main/java/org/oppia/android/util/parser/image:test_glide_image_loader", + "//utility/src/main/java/org/oppia/android/util/profile:profile_name_validator", ] # App module tests. Note that all tests are assumed to be tests with resources (even though not all diff --git a/app/build.gradle b/app/build.gradle index af0c490cec8..4709cf0f06f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -91,13 +91,15 @@ android { } } -// See notice for the excluded files in domain/build.gradle for an explanation. +// See notice for the excluded files in domain/build.gradle for an explanation on the +// language-related exclusions. The additional application component is excluded because Gradle +// isn't able to compile multiple Dagger graphs in the same build configuration. def filesToExclude = [ '**/*AppLanguageLocaleHandlerTest*.kt', '**/*AppLanguageResourceHandlerTest*.kt', '**/*AppLanguageWatcherMixinTest*.kt', ] -_excludeTestFiles(filesToExclude) +_excludeSourceFiles(filesToExclude) // Check if sharding is being attempted and, if so, exclude all tests that aren't in the specified // shard. The approach to using arguments was inspired by @@ -113,7 +115,7 @@ shardIndexes.each { shardIndex -> if (project.gradle.startParameter?.taskRequests?.args[0]?.remove("--shard$shardIndex".toString())) { def filesToExcludeForShard = _computeTestsCorrespondingToShard(appModuleShardCount, shardIndex, true) println("Excluding ${filesToExcludeForShard.size} tests for shard $shardIndex") - _excludeTestFiles(filesToExcludeForShard.collect { + _excludeSourceFiles(filesToExcludeForShard.collect { // Ensure the test paths are properly relative to the app module so that their exclusion // filters match (but keep the unique relative paths to avoid accidentally skipping tests that // share names). Note that the 'org/oppia/android/app' part is specifically included to ensure @@ -137,7 +139,7 @@ if (project.gradle.startParameter?.taskRequests?.args[0]?.remove("--list-shards" println() } // This is a hacky way to make an 'information' command. - _excludeTestFiles("**/*Test.kt") + _excludeSourceFiles("**/*Test.kt") } dependencies { @@ -291,7 +293,7 @@ def _computeTestsCorrespondingToShard(shardCount, shardIndex, exclude) { }.sort() } -def _excludeTestFiles(filesToExclude) { +def _excludeSourceFiles(filesToExclude) { tasks.withType(SourceTask.class).configureEach { it.exclude(filesToExclude) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6515cf4329a..6156e5d0764 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,11 +9,10 @@ @@ -85,6 +84,14 @@ android:label="@string/my_downloads_activity_title" android:screenOrientation="portrait" android:theme="@style/OppiaThemeWithoutActionBar" /> + + + + @@ -276,6 +285,9 @@ + , @LearnerStudyAnalytics private val learnerStudyAnalytics: PlatformParameterValue ) { private val routeToProfileListListener = activity as RouteToProfileListListener @@ -71,9 +74,13 @@ class AdministratorControlsViewModel @Inject constructor( private fun processAdministratorControlsList( deviceSettings: DeviceSettings ): List { - val itemViewModelList: MutableList = mutableListOf( - AdministratorControlsGeneralViewModel() - ) + + val itemViewModelList = mutableListOf() + + if (enableEditAccountsOptionsUi.value) { + itemViewModelList.add(AdministratorControlsGeneralViewModel()) + } + itemViewModelList.add( AdministratorControlsProfileViewModel( routeToProfileListListener, @@ -84,6 +91,7 @@ class AdministratorControlsViewModel @Inject constructor( if (learnerStudyAnalytics.value) { itemViewModelList.add(AdministratorControlsProfileAndDeviceIdViewModel(activity)) } + itemViewModelList.add( AdministratorControlsDownloadPermissionsViewModel( fragment, @@ -93,6 +101,7 @@ class AdministratorControlsViewModel @Inject constructor( deviceSettings ) ) + itemViewModelList.add(AdministratorControlsAppInformationViewModel(activity)) itemViewModelList.add( AdministratorControlsAccountActionsViewModel( 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 0bc857e5c9f..2dd9c5f37f4 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 @@ -6,6 +6,8 @@ import android.os.Bundle import android.view.MenuItem import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.APP_VERSION_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for App Version. */ @@ -28,7 +30,9 @@ class AppVersionActivity : InjectableAppCompatActivity() { companion object { fun createAppVersionActivityIntent(context: Context): Intent { - return Intent(context, AppVersionActivity::class.java) + return Intent(context, AppVersionActivity::class.java).apply { + decorateWithScreenName(APP_VERSION_ACTIVITY) + } } } } 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 c6e164ef8fb..6bcd82687a6 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 @@ -6,6 +6,8 @@ import android.os.Bundle import android.view.MenuItem import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.PROFILE_AND_DEVICE_ID_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** @@ -34,7 +36,9 @@ class ProfileAndDeviceIdActivity : InjectableAppCompatActivity() { companion object { /** Returns an [Intent] to launch [ProfileAndDeviceIdActivity]. */ fun createIntent(context: Context): Intent { - return Intent(context, ProfileAndDeviceIdActivity::class.java) + return Intent(context, ProfileAndDeviceIdActivity::class.java).apply { + decorateWithScreenName(PROFILE_AND_DEVICE_ID_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/application/OppiaApplication.kt b/app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt similarity index 89% rename from app/src/main/java/org/oppia/android/app/application/OppiaApplication.kt rename to app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt index 5eafca3585c..996403f261e 100644 --- a/app/src/main/java/org/oppia/android/app/application/OppiaApplication.kt +++ b/app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt @@ -13,17 +13,17 @@ import org.oppia.android.app.activity.ActivityComponent import org.oppia.android.app.activity.ActivityComponentFactory import org.oppia.android.domain.oppialogger.ApplicationStartupListener -/** The root [Application] of the Oppia app. */ -class OppiaApplication : - MultiDexApplication(), +/** The root base [Application] of the Oppia app. */ +abstract class AbstractOppiaApplication( + createComponentBuilder: () -> ApplicationComponent.Builder +) : MultiDexApplication(), ActivityComponentFactory, ApplicationInjectorProvider, Configuration.Provider { + /** The root [ApplicationComponent]. */ private val component: ApplicationComponent by lazy { - DaggerApplicationComponent.builder() - .setApplication(this) - .build() + createComponentBuilder().setApplication(this).build() } override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt index f08f475b263..1c92ad5c6c6 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt @@ -3,116 +3,22 @@ package org.oppia.android.app.application import android.app.Application import androidx.work.Configuration import dagger.BindsInstance -import dagger.Component import org.oppia.android.app.activity.ActivityComponentImpl -import org.oppia.android.app.devoptions.DeveloperOptionsModule -import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule -import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule -import org.oppia.android.app.shim.IntentFactoryShimModule -import org.oppia.android.app.shim.ViewBindingShimModule -import org.oppia.android.app.topic.PracticeTabModule -import org.oppia.android.app.translation.ActivityRecreatorProdModule -import org.oppia.android.data.backends.gae.NetworkConfigProdModule -import org.oppia.android.data.backends.gae.NetworkModule -import org.oppia.android.domain.classify.InteractionsModule -import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule -import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule -import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule -import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule -import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule -import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule -import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule -import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule -import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule -import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule -import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule -import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule -import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule -import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule -import org.oppia.android.domain.hintsandsolution.HintsAndSolutionDebugModule -import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.android.domain.oppialogger.ApplicationStartupListener -import org.oppia.android.domain.oppialogger.LogStorageModule -import org.oppia.android.domain.oppialogger.LoggingIdentifierModule -import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule -import org.oppia.android.domain.oppialogger.exceptions.UncaughtExceptionLoggerModule -import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorkerModule -import org.oppia.android.domain.platformparameter.PlatformParameterModule -import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule -import org.oppia.android.domain.platformparameter.syncup.PlatformParameterSyncUpWorkerModule -import org.oppia.android.domain.question.QuestionModule -import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule -import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule -import org.oppia.android.util.accessibility.AccessibilityProdModule -import org.oppia.android.util.caching.AssetModule -import org.oppia.android.util.caching.CachingModule -import org.oppia.android.util.gcsresource.GcsResourceModule -import org.oppia.android.util.locale.LocaleProdModule -import org.oppia.android.util.logging.LoggerModule -import org.oppia.android.util.logging.SyncStatusModule -import org.oppia.android.util.logging.firebase.DebugLogReportingModule -import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule -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.GlideImageLoaderModule -import org.oppia.android.util.parser.image.ImageParsingModule -import org.oppia.android.util.system.OppiaClockModule -import org.oppia.android.util.threading.DispatcherModule import javax.inject.Provider -import javax.inject.Singleton /** * Root Dagger component for the application. All application-scoped modules should be included in * this component. * - * At the time of building the app in prod mode - - * Remove: [DeveloperOptionsStarterModule], [DebugLogReportingModule], - * [NetworkConnectionUtilDebugModule], [HintsAndSolutionDebugModule] - * Add: [LogReportingModule], [NetworkConnectionUtilProdModule], [HintsAndSolutionProdModule] - * - * When building with Bazel, please also refer to instructions in app/BUILD.bazel. + * This component will be subclasses for specific contexts (such as test builds, or specific build + * flavors of the app). */ -@Singleton -@Component( - modules = [ - ApplicationModule::class, DispatcherModule::class, - LoggerModule::class, OppiaClockModule::class, - ContinueModule::class, FractionInputModule::class, - ItemSelectionInputModule::class, MultipleChoiceInputModule::class, - NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, - TextInputRuleModule::class, DragDropSortInputModule::class, - InteractionsModule::class, GcsResourceModule::class, - GlideImageLoaderModule::class, ImageParsingModule::class, - HtmlParserEntityTypeModule::class, CachingModule::class, - QuestionModule::class, DebugLogReportingModule::class, - AccessibilityProdModule::class, ImageClickInputModule::class, - LogStorageModule::class, IntentFactoryShimModule::class, - ViewBindingShimModule::class, PrimeTopicAssetsControllerModule::class, - ExpirationMetaDataRetrieverModule::class, RatioInputModule::class, - UncaughtExceptionLoggerModule::class, ApplicationStartupListenerModule::class, - LogUploadWorkerModule::class, WorkManagerConfigurationModule::class, - HintsAndSolutionConfigModule::class, HintsAndSolutionDebugModule::class, - FirebaseLogUploaderModule::class, NetworkModule::class, PracticeTabModule::class, - PlatformParameterModule::class, PlatformParameterSingletonModule::class, - ExplorationStorageModule::class, DeveloperOptionsStarterModule::class, - DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, - NetworkConnectionUtilDebugModule::class, NetworkConfigProdModule::class, AssetModule::class, - LocaleProdModule::class, ActivityRecreatorProdModule::class, - NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, - MathEquationInputModule::class, SplitScreenInteractionModule::class, - LoggingIdentifierModule::class, ApplicationLifecycleModule::class, - // TODO(#59): Remove this module once we completely migrate to Bazel from Gradle as we can then - // directly exclude debug files from the build and thus won't be requiring this module. - NetworkConnectionDebugUtilModule::class, LoggingIdentifierModule::class, SyncStatusModule::class - ] -) interface ApplicationComponent : ApplicationInjector { - @Component.Builder interface Builder { @BindsInstance fun setApplication(application: Application): Builder + fun build(): ApplicationComponent } diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationContext.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationContext.kt deleted file mode 100644 index d4aa0324272..00000000000 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationContext.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.oppia.android.app.application - -import javax.inject.Qualifier - -/** Qualifier for injecting the application context. */ -@Qualifier -annotation class ApplicationContext diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationModule.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationModule.kt index 00954abdc7d..9cd0698c05b 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationModule.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationModule.kt @@ -2,25 +2,13 @@ package org.oppia.android.app.application import android.app.Application import android.content.Context +import dagger.Binds import dagger.Module -import dagger.Provides import org.oppia.android.app.activity.ActivityComponentImpl -import javax.inject.Singleton /** Provides core infrastructure needed to support all other dependencies in the app. */ @Module(subcomponents = [ActivityComponentImpl::class]) -class ApplicationModule { - @Provides - @Singleton - @ApplicationContext - fun provideApplicationContext(application: Application): Context { - return application - } - - // TODO(#59): Remove this provider once all modules have access to the @ApplicationContext qualifier. - @Provides - @Singleton - fun provideContext(@ApplicationContext context: Context): Context { - return context - } +interface ApplicationModule { + @Binds + fun provideContext(application: Application): Context } diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationStartupListenerModule.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationStartupListenerModule.kt index 239e3b0e9f8..77287f080d3 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationStartupListenerModule.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationStartupListenerModule.kt @@ -7,7 +7,6 @@ import org.oppia.android.domain.oppialogger.ApplicationStartupListener /** Binds multiple dependencies that implement [ApplicationStartupListener] into a set. */ @Module interface ApplicationStartupListenerModule { - @Multibinds fun bindStartupListenerSet(): Set } diff --git a/app/src/main/java/org/oppia/android/app/application/BUILD.bazel b/app/src/main/java/org/oppia/android/app/application/BUILD.bazel new file mode 100644 index 00000000000..593be4f261a --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/BUILD.bazel @@ -0,0 +1,139 @@ +""" +This package contains all of the top-level infrastructure for the application entrypoint and root +Dagger graph. + +Specific application implementations can be found in subpackages. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "abstract_application", + srcs = [ + "AbstractOppiaApplication.kt", + ], + visibility = [ + "//app/src/main/java/org/oppia/android/app/application:__subpackages__", + "//instrumentation/src/java/org/oppia/android/instrumentation/application:__pkg__", + ], + deps = [ + ":application_component", + ":application_injector", + ":application_injector_provider", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", + "//third_party:androidx_work_work-runtime-ktx", + "//third_party:com_google_firebase_firebase-common", + ], +) + +kt_android_library( + name = "application_component", + srcs = [ + "ApplicationComponent.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + "//app/src/main/java/org/oppia/android/app/application:__subpackages__", + ], + deps = [ + ":application_injector", + "//app", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", + "//third_party:androidx_work_work-runtime-ktx", + ], +) + +kt_android_library( + name = "application_injector", + srcs = [ + "ApplicationInjector.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + "//app/src/main/java/org/oppia/android/app/translation:app_language_application_injector", + "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger_injector", + "//utility/src/main/java/org/oppia/android/util/system:oppia_clock_injector", + "//utility/src/main/java/org/oppia/android/util/threading:dispatcher_injector", + ], +) + +kt_android_library( + name = "application_injector_provider", + srcs = [ + "ApplicationInjectorProvider.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + ":application_injector", + "//app/src/main/java/org/oppia/android/app/translation:app_language_application_injector_provider", + "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector_provider", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger_injector_provider", + "//utility/src/main/java/org/oppia/android/util/system:oppia_clock_injector_provider", + "//utility/src/main/java/org/oppia/android/util/threading:dispatcher_injector_provider", + ], +) + +kt_android_library( + name = "modules", + srcs = [ + "ApplicationModule.kt", + "ApplicationStartupListenerModule.kt", + ], + deps = [ + ":dagger", + "//app", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", + ], +) + +# TODO(#1720): Remove this once the list can be automatically determined from the build graph. +android_library( + name = "common_application_modules", + visibility = [ + "//:oppia_testing_visibility", + "//app/src/main/java/org/oppia/android/app/application:__subpackages__", + ], + exports = [ + ":modules", + "//app/src/main/java/org/oppia/android/app/translation:prod_module", + "//data/src/main/java/org/oppia/android/data/backends/gae:network_config_prod_module", + "//data/src/main/java/org/oppia/android/data/backends/gae:prod_module", + "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", + "//domain/src/main/java/org/oppia/android/domain/onboarding:retriever_prod_module", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:performance_metrics_logger_module", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:prod_module", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:logger_module", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler:metric_log_scheduler_module", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", + "//utility/src/main/java/org/oppia/android/util/accessibility:prod_module", + "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", + "//utility/src/main/java/org/oppia/android/util/caching:caching_prod_module", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging/firebase:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging/performancemetrics:performance_metrics_assessor_module", + "//utility/src/main/java/org/oppia/android/util/logging/performancemetrics:performance_metrics_configurations_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_util_module", + "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser_entity_type_module", + "//utility/src/main/java/org/oppia/android/util/parser/image:glide_image_loader_module", + "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_module", + "//utility/src/main/java/org/oppia/android/util/parser/image:repository_glide_module", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/application/alpha/AlphaApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/alpha/AlphaApplicationComponent.kt new file mode 100644 index 00000000000..526a82a92fd --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/alpha/AlphaApplicationComponent.kt @@ -0,0 +1,112 @@ +package org.oppia.android.app.application.alpha + +import dagger.Component +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.IntentFactoryShimModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.app.translation.ActivityRecreatorProdModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +import org.oppia.android.domain.oppialogger.analytics.ActivityLifecycleObserverModule +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.exceptions.UncaughtExceptionLoggerModule +import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule +import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.platformparameter.syncup.PlatformParameterSyncUpWorkerModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.util.accessibility.AccessibilityProdModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.CachingModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.logging.firebase.LogReportingModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilProdModule +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.system.OppiaClockModule +import org.oppia.android.util.threading.DispatcherModule +import javax.inject.Singleton + +/** + * Root Dagger component for alpha versions of the application. + * + * All application-scoped modules should be included in this component. + */ +@Singleton +@Component( + modules = [ + ApplicationModule::class, DispatcherModule::class, LoggerModule::class, OppiaClockModule::class, + ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, + MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, + NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, + InteractionsModule::class, GcsResourceModule::class, GlideImageLoaderModule::class, + ImageParsingModule::class, HtmlParserEntityTypeModule::class, CachingModule::class, + QuestionModule::class, AccessibilityProdModule::class, ImageClickInputModule::class, + LogStorageModule::class, IntentFactoryShimModule::class, ViewBindingShimModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + RatioInputModule::class, UncaughtExceptionLoggerModule::class, + ApplicationStartupListenerModule::class, LogReportWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class, NetworkModule::class, PracticeTabModule::class, + PlatformParameterModule::class, PlatformParameterSingletonModule::class, + ExplorationStorageModule::class, DeveloperOptionsModule::class, + PlatformParameterSyncUpWorkerModule::class, NetworkConfigProdModule::class, AssetModule::class, + LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, SplitScreenInteractionModule::class, + LoggingIdentifierModule::class, ApplicationLifecycleModule::class, + NetworkConnectionDebugUtilModule::class, LoggingIdentifierModule::class, + SyncStatusModule::class, LogReportingModule::class, NetworkConnectionUtilProdModule::class, + HintsAndSolutionProdModule::class, MetricLogSchedulerModule::class, + ActivityLifecycleObserverModule::class, PerformanceMetricsAssessorModule::class, + PerformanceMetricsConfigurationsModule::class, AlphaBuildFlavorModule::class, + EventLoggingConfigurationModule::class + ] +) +interface AlphaApplicationComponent : ApplicationComponent { + /** + * The [ApplicationComponent.Builder] for this component. Dagger will generate an implementation + * of this builder for use. + */ + @Component.Builder + interface Builder : ApplicationComponent.Builder { + override fun build(): AlphaApplicationComponent + } +} diff --git a/app/src/main/java/org/oppia/android/app/application/alpha/AlphaBuildFlavorModule.kt b/app/src/main/java/org/oppia/android/app/application/alpha/AlphaBuildFlavorModule.kt new file mode 100644 index 00000000000..1830cc54291 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/alpha/AlphaBuildFlavorModule.kt @@ -0,0 +1,12 @@ +package org.oppia.android.app.application.alpha + +import dagger.Module +import dagger.Provides +import org.oppia.android.app.model.BuildFlavor + +/** Module for providing the compile-time [BuildFlavor] of alpha builds of the app. */ +@Module +class AlphaBuildFlavorModule { + @Provides + fun provideAlphaBuildFlavor(): BuildFlavor = BuildFlavor.ALPHA +} diff --git a/app/src/main/java/org/oppia/android/app/application/alpha/AlphaOppiaApplication.kt b/app/src/main/java/org/oppia/android/app/application/alpha/AlphaOppiaApplication.kt new file mode 100644 index 00000000000..c37a271e8e1 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/alpha/AlphaOppiaApplication.kt @@ -0,0 +1,6 @@ +package org.oppia.android.app.application.alpha + +import org.oppia.android.app.application.AbstractOppiaApplication + +/** The root [AbstractOppiaApplication] for alpha builds of the Oppia app. */ +class AlphaOppiaApplication : AbstractOppiaApplication(DaggerAlphaApplicationComponent::builder) diff --git a/app/src/main/java/org/oppia/android/app/application/alpha/BUILD.bazel b/app/src/main/java/org/oppia/android/app/application/alpha/BUILD.bazel new file mode 100644 index 00000000000..9c410695450 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/alpha/BUILD.bazel @@ -0,0 +1,43 @@ +""" +This package contains the root application definitions for alpha builds of the app. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "alpha_application", + srcs = [ + "AlphaApplicationComponent.kt", + "AlphaOppiaApplication.kt", + ], + visibility = ["//:oppia_binary_visibility"], + deps = [ + ":alpha_build_flavor_module", + ":dagger", + "//app", + "//app/src/main/java/org/oppia/android/app/application:abstract_application", + "//app/src/main/java/org/oppia/android/app/application:application_component", + "//app/src/main/java/org/oppia/android/app/application:common_application_modules", + "//utility/src/main/java/org/oppia/android/util/logging:standard_event_logging_configuration_module", + "//utility/src/main/java/org/oppia/android/util/logging/firebase:prod_module", + "//utility/src/main/java/org/oppia/android/util/networking:prod_module", + ], +) + +kt_android_library( + name = "alpha_build_flavor_module", + srcs = [ + "AlphaBuildFlavorModule.kt", + ], + visibility = [ + "//app/src/main/java/org/oppia/android/app/application/alphakenya:__pkg__", + "//app/src/test/java/org/oppia/android/app/application/alpha:__pkg__", + ], + deps = [ + ":dagger", + "//model/src/main/proto:version_java_proto_lite", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/application/alphakenya/AlphaKenyaApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/alphakenya/AlphaKenyaApplicationComponent.kt new file mode 100644 index 00000000000..1afb482c812 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/alphakenya/AlphaKenyaApplicationComponent.kt @@ -0,0 +1,113 @@ +package org.oppia.android.app.application.alphakenya + +import dagger.Component +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.application.alpha.AlphaBuildFlavorModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.IntentFactoryShimModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.app.translation.ActivityRecreatorProdModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigAlphaKenyaModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +import org.oppia.android.domain.oppialogger.analytics.ActivityLifecycleObserverModule +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.exceptions.UncaughtExceptionLoggerModule +import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule +import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterAlphaKenyaModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.platformparameter.syncup.PlatformParameterSyncUpWorkerModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.util.accessibility.AccessibilityProdModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.CachingModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.KenyaAlphaEventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.logging.firebase.LogReportingModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilProdModule +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.system.OppiaClockModule +import org.oppia.android.util.threading.DispatcherModule +import javax.inject.Singleton + +/** + * Root Dagger component for the alpha version of the application specific to a user study in Kenya. + * + * All application-scoped modules should be included in this component. + */ +@Singleton +@Component( + modules = [ + ApplicationModule::class, DispatcherModule::class, LoggerModule::class, OppiaClockModule::class, + ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, + MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, + NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, + InteractionsModule::class, GcsResourceModule::class, GlideImageLoaderModule::class, + ImageParsingModule::class, HtmlParserEntityTypeModule::class, CachingModule::class, + QuestionModule::class, AccessibilityProdModule::class, ImageClickInputModule::class, + LogStorageModule::class, IntentFactoryShimModule::class, ViewBindingShimModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + RatioInputModule::class, UncaughtExceptionLoggerModule::class, + ApplicationStartupListenerModule::class, LogReportWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigAlphaKenyaModule::class, + FirebaseLogUploaderModule::class, NetworkModule::class, PracticeTabModule::class, + PlatformParameterAlphaKenyaModule::class, PlatformParameterSingletonModule::class, + ExplorationStorageModule::class, DeveloperOptionsModule::class, + PlatformParameterSyncUpWorkerModule::class, NetworkConfigProdModule::class, AssetModule::class, + LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, SplitScreenInteractionModule::class, + LoggingIdentifierModule::class, ApplicationLifecycleModule::class, + NetworkConnectionDebugUtilModule::class, LoggingIdentifierModule::class, + SyncStatusModule::class, LogReportingModule::class, NetworkConnectionUtilProdModule::class, + HintsAndSolutionProdModule::class, MetricLogSchedulerModule::class, + ActivityLifecycleObserverModule::class, PerformanceMetricsAssessorModule::class, + PerformanceMetricsConfigurationsModule::class, AlphaBuildFlavorModule::class, + KenyaAlphaEventLoggingConfigurationModule::class + ] +) +interface AlphaKenyaApplicationComponent : ApplicationComponent { + /** + * The [ApplicationComponent.Builder] for this component. Dagger will generate an implementation + * of this builder for use. + */ + @Component.Builder + interface Builder : ApplicationComponent.Builder { + override fun build(): AlphaKenyaApplicationComponent + } +} diff --git a/app/src/main/java/org/oppia/android/app/application/alphakenya/AlphaKenyaOppiaApplication.kt b/app/src/main/java/org/oppia/android/app/application/alphakenya/AlphaKenyaOppiaApplication.kt new file mode 100644 index 00000000000..00566b9b6f6 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/alphakenya/AlphaKenyaOppiaApplication.kt @@ -0,0 +1,9 @@ +package org.oppia.android.app.application.alphakenya + +import org.oppia.android.app.application.AbstractOppiaApplication + +// TODO(#4419): Remove this application class & broader Kenya-specific alpha package. +/** The root [AbstractOppiaApplication] for the Kenya-specific alpha build of the Oppia app. */ +class AlphaKenyaOppiaApplication : AbstractOppiaApplication( + DaggerAlphaKenyaApplicationComponent::builder +) diff --git a/app/src/main/java/org/oppia/android/app/application/alphakenya/BUILD.bazel b/app/src/main/java/org/oppia/android/app/application/alphakenya/BUILD.bazel new file mode 100644 index 00000000000..538d47308dc --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/alphakenya/BUILD.bazel @@ -0,0 +1,29 @@ +""" +This package contains the root application definitions for a Kenya user study specific alpha build +of the app. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "alpha_kenya_application", + srcs = [ + "AlphaKenyaApplicationComponent.kt", + "AlphaKenyaOppiaApplication.kt", + ], + visibility = ["//:oppia_binary_visibility"], + deps = [ + ":dagger", + "//app", + "//app/src/main/java/org/oppia/android/app/application:abstract_application", + "//app/src/main/java/org/oppia/android/app/application:application_component", + "//app/src/main/java/org/oppia/android/app/application:common_application_modules", + "//app/src/main/java/org/oppia/android/app/application/alpha:alpha_build_flavor_module", + "//utility/src/main/java/org/oppia/android/util/logging:kenya_alpha_event_logging_configuration_module", + "//utility/src/main/java/org/oppia/android/util/logging/firebase:prod_module", + "//utility/src/main/java/org/oppia/android/util/networking:prod_module", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/application/beta/BUILD.bazel b/app/src/main/java/org/oppia/android/app/application/beta/BUILD.bazel new file mode 100644 index 00000000000..db532533d41 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/beta/BUILD.bazel @@ -0,0 +1,31 @@ +""" +This package contains the root application definitions for beta builds of the app. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "beta_application", + srcs = [ + "BetaApplicationComponent.kt", + "BetaBuildFlavorModule.kt", + "BetaOppiaApplication.kt", + ], + visibility = [ + "//:oppia_binary_visibility", + "//app/src/test/java/org/oppia/android/app/application/beta:__pkg__", + ], + deps = [ + ":dagger", + "//app", + "//app/src/main/java/org/oppia/android/app/application:abstract_application", + "//app/src/main/java/org/oppia/android/app/application:application_component", + "//app/src/main/java/org/oppia/android/app/application:common_application_modules", + "//utility/src/main/java/org/oppia/android/util/logging:standard_event_logging_configuration_module", + "//utility/src/main/java/org/oppia/android/util/logging/firebase:prod_module", + "//utility/src/main/java/org/oppia/android/util/networking:prod_module", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/application/beta/BetaApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/beta/BetaApplicationComponent.kt new file mode 100644 index 00000000000..d629bb697f7 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/beta/BetaApplicationComponent.kt @@ -0,0 +1,112 @@ +package org.oppia.android.app.application.beta + +import dagger.Component +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.IntentFactoryShimModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.app.translation.ActivityRecreatorProdModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +import org.oppia.android.domain.oppialogger.analytics.ActivityLifecycleObserverModule +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.exceptions.UncaughtExceptionLoggerModule +import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule +import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.platformparameter.syncup.PlatformParameterSyncUpWorkerModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.util.accessibility.AccessibilityProdModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.CachingModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.logging.firebase.LogReportingModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilProdModule +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.system.OppiaClockModule +import org.oppia.android.util.threading.DispatcherModule +import javax.inject.Singleton + +/** + * Root Dagger component for beta versions of the application. + * + * All application-scoped modules should be included in this component. + */ +@Singleton +@Component( + modules = [ + ApplicationModule::class, DispatcherModule::class, LoggerModule::class, OppiaClockModule::class, + ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, + MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, + NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, + InteractionsModule::class, GcsResourceModule::class, GlideImageLoaderModule::class, + ImageParsingModule::class, HtmlParserEntityTypeModule::class, CachingModule::class, + QuestionModule::class, AccessibilityProdModule::class, ImageClickInputModule::class, + LogStorageModule::class, IntentFactoryShimModule::class, ViewBindingShimModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + RatioInputModule::class, UncaughtExceptionLoggerModule::class, + ApplicationStartupListenerModule::class, LogReportWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class, NetworkModule::class, PracticeTabModule::class, + PlatformParameterModule::class, PlatformParameterSingletonModule::class, + ExplorationStorageModule::class, DeveloperOptionsModule::class, + PlatformParameterSyncUpWorkerModule::class, NetworkConfigProdModule::class, AssetModule::class, + LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, SplitScreenInteractionModule::class, + LoggingIdentifierModule::class, ApplicationLifecycleModule::class, + NetworkConnectionDebugUtilModule::class, LoggingIdentifierModule::class, + SyncStatusModule::class, LogReportingModule::class, NetworkConnectionUtilProdModule::class, + HintsAndSolutionProdModule::class, MetricLogSchedulerModule::class, + ActivityLifecycleObserverModule::class, PerformanceMetricsAssessorModule::class, + PerformanceMetricsConfigurationsModule::class, BetaBuildFlavorModule::class, + EventLoggingConfigurationModule::class + ] +) +interface BetaApplicationComponent : ApplicationComponent { + /** + * The [ApplicationComponent.Builder] for this component. Dagger will generate an implementation + * of this builder for use. + */ + @Component.Builder + interface Builder : ApplicationComponent.Builder { + override fun build(): BetaApplicationComponent + } +} diff --git a/app/src/main/java/org/oppia/android/app/application/beta/BetaBuildFlavorModule.kt b/app/src/main/java/org/oppia/android/app/application/beta/BetaBuildFlavorModule.kt new file mode 100644 index 00000000000..46e04d47ec7 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/beta/BetaBuildFlavorModule.kt @@ -0,0 +1,12 @@ +package org.oppia.android.app.application.beta + +import dagger.Module +import dagger.Provides +import org.oppia.android.app.model.BuildFlavor + +/** Module for providing the compile-time [BuildFlavor] of beta builds of the app. */ +@Module +class BetaBuildFlavorModule { + @Provides + fun provideBetaBuildFlavor(): BuildFlavor = BuildFlavor.BETA +} diff --git a/app/src/main/java/org/oppia/android/app/application/beta/BetaOppiaApplication.kt b/app/src/main/java/org/oppia/android/app/application/beta/BetaOppiaApplication.kt new file mode 100644 index 00000000000..39d33b818a2 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/beta/BetaOppiaApplication.kt @@ -0,0 +1,6 @@ +package org.oppia.android.app.application.beta + +import org.oppia.android.app.application.AbstractOppiaApplication + +/** The root [AbstractOppiaApplication] for beta builds of the Oppia app. */ +class BetaOppiaApplication : AbstractOppiaApplication(DaggerBetaApplicationComponent::builder) diff --git a/app/src/main/java/org/oppia/android/app/application/dev/BUILD.bazel b/app/src/main/java/org/oppia/android/app/application/dev/BUILD.bazel new file mode 100644 index 00000000000..53841df1c37 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/dev/BUILD.bazel @@ -0,0 +1,35 @@ +""" +This package contains the root application definitions for developer builds of the app. + +Note that this will be the application configuration used for Gradle builds of the app. For Bazel, +there are specially defined top-level build flavors which will select their corresponding +application configuration. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "developer_application", + srcs = [ + "DeveloperApplicationComponent.kt", + "DeveloperBuildFlavorModule.kt", + "DeveloperOppiaApplication.kt", + ], + visibility = [ + "//:oppia_binary_visibility", + "//app/src/test/java/org/oppia/android/app/application/dev:__pkg__", + ], + deps = [ + ":dagger", + "//app", + "//app/src/main/java/org/oppia/android/app/application:abstract_application", + "//app/src/main/java/org/oppia/android/app/application:application_component", + "//app/src/main/java/org/oppia/android/app/application:common_application_modules", + "//utility/src/main/java/org/oppia/android/util/logging:standard_event_logging_configuration_module", + "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/application/dev/DeveloperApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/dev/DeveloperApplicationComponent.kt new file mode 100644 index 00000000000..fa33e88d4e2 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/dev/DeveloperApplicationComponent.kt @@ -0,0 +1,114 @@ +package org.oppia.android.app.application.dev + +import dagger.Component +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.IntentFactoryShimModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.app.translation.ActivityRecreatorProdModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionDebugModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +import org.oppia.android.domain.oppialogger.analytics.ActivityLifecycleObserverModule +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.exceptions.UncaughtExceptionLoggerModule +import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule +import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.platformparameter.syncup.PlatformParameterSyncUpWorkerModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.util.accessibility.AccessibilityProdModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.CachingModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +import org.oppia.android.util.logging.firebase.DebugLogReportingModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule +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.GlideImageLoaderModule +import org.oppia.android.util.parser.image.ImageParsingModule +import org.oppia.android.util.system.OppiaClockModule +import org.oppia.android.util.threading.DispatcherModule +import javax.inject.Singleton + +/** + * Root Dagger component for developer versions of the application. + * + * All application-scoped modules should be included in this component. + */ +@Singleton +@Component( + modules = [ + ApplicationModule::class, DispatcherModule::class, LoggerModule::class, OppiaClockModule::class, + ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, + MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, + NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, + InteractionsModule::class, GcsResourceModule::class, GlideImageLoaderModule::class, + ImageParsingModule::class, HtmlParserEntityTypeModule::class, CachingModule::class, + QuestionModule::class, DebugLogReportingModule::class, AccessibilityProdModule::class, + ImageClickInputModule::class, LogStorageModule::class, IntentFactoryShimModule::class, + ViewBindingShimModule::class, PrimeTopicAssetsControllerModule::class, + ExpirationMetaDataRetrieverModule::class, RatioInputModule::class, + UncaughtExceptionLoggerModule::class, ApplicationStartupListenerModule::class, + LogReportWorkerModule::class, WorkManagerConfigurationModule::class, + HintsAndSolutionConfigModule::class, HintsAndSolutionDebugModule::class, + FirebaseLogUploaderModule::class, NetworkModule::class, PracticeTabModule::class, + PlatformParameterModule::class, PlatformParameterSingletonModule::class, + ExplorationStorageModule::class, DeveloperOptionsStarterModule::class, + DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, + NetworkConnectionUtilDebugModule::class, NetworkConfigProdModule::class, AssetModule::class, + LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, SplitScreenInteractionModule::class, + LoggingIdentifierModule::class, ApplicationLifecycleModule::class, + NetworkConnectionDebugUtilModule::class, LoggingIdentifierModule::class, + SyncStatusModule::class, MetricLogSchedulerModule::class, + PerformanceMetricsAssessorModule::class, PerformanceMetricsConfigurationsModule::class, + DeveloperBuildFlavorModule::class, EventLoggingConfigurationModule::class, + ActivityLifecycleObserverModule::class, + ] +) +interface DeveloperApplicationComponent : ApplicationComponent { + /** + * The [ApplicationComponent.Builder] for this component. Dagger will generate an implementation + * of this builder for use. + */ + @Component.Builder + interface Builder : ApplicationComponent.Builder { + override fun build(): DeveloperApplicationComponent + } +} diff --git a/app/src/main/java/org/oppia/android/app/application/dev/DeveloperBuildFlavorModule.kt b/app/src/main/java/org/oppia/android/app/application/dev/DeveloperBuildFlavorModule.kt new file mode 100644 index 00000000000..20975a930db --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/dev/DeveloperBuildFlavorModule.kt @@ -0,0 +1,12 @@ +package org.oppia.android.app.application.dev + +import dagger.Module +import dagger.Provides +import org.oppia.android.app.model.BuildFlavor + +/** Module for providing the compile-time [BuildFlavor] of developer-only builds of the app. */ +@Module +class DeveloperBuildFlavorModule { + @Provides + fun provideDeveloperBuildFlavor(): BuildFlavor = BuildFlavor.DEVELOPER +} diff --git a/app/src/main/java/org/oppia/android/app/application/dev/DeveloperOppiaApplication.kt b/app/src/main/java/org/oppia/android/app/application/dev/DeveloperOppiaApplication.kt new file mode 100644 index 00000000000..495a21d7a3e --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/dev/DeveloperOppiaApplication.kt @@ -0,0 +1,8 @@ +package org.oppia.android.app.application.dev + +import org.oppia.android.app.application.AbstractOppiaApplication + +/** The root [AbstractOppiaApplication] for developer builds of the Oppia app. */ +class DeveloperOppiaApplication : AbstractOppiaApplication( + DaggerDeveloperApplicationComponent::builder +) diff --git a/app/src/main/java/org/oppia/android/app/application/ga/BUILD.bazel b/app/src/main/java/org/oppia/android/app/application/ga/BUILD.bazel new file mode 100644 index 00000000000..5421f374f20 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/ga/BUILD.bazel @@ -0,0 +1,31 @@ +""" +This package contains the root application definitions for general availability builds of the app. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "general_availability_application", + srcs = [ + "GaApplicationComponent.kt", + "GaBuildFlavorModule.kt", + "GaOppiaApplication.kt", + ], + visibility = [ + "//:oppia_binary_visibility", + "//app/src/test/java/org/oppia/android/app/application/ga:__pkg__", + ], + deps = [ + ":dagger", + "//app", + "//app/src/main/java/org/oppia/android/app/application:abstract_application", + "//app/src/main/java/org/oppia/android/app/application:application_component", + "//app/src/main/java/org/oppia/android/app/application:common_application_modules", + "//utility/src/main/java/org/oppia/android/util/logging:standard_event_logging_configuration_module", + "//utility/src/main/java/org/oppia/android/util/logging/firebase:prod_module", + "//utility/src/main/java/org/oppia/android/util/networking:prod_module", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/application/ga/GaApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/ga/GaApplicationComponent.kt new file mode 100644 index 00000000000..b6ce2e2b2ca --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/ga/GaApplicationComponent.kt @@ -0,0 +1,112 @@ +package org.oppia.android.app.application.ga + +import dagger.Component +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.IntentFactoryShimModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.app.translation.ActivityRecreatorProdModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +import org.oppia.android.domain.oppialogger.analytics.ActivityLifecycleObserverModule +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.exceptions.UncaughtExceptionLoggerModule +import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule +import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.platformparameter.syncup.PlatformParameterSyncUpWorkerModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.util.accessibility.AccessibilityProdModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.CachingModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.logging.firebase.LogReportingModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilProdModule +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.system.OppiaClockModule +import org.oppia.android.util.threading.DispatcherModule +import javax.inject.Singleton + +/** + * Root Dagger component for general availability versions of the application. + * + * All application-scoped modules should be included in this component. + */ +@Singleton +@Component( + modules = [ + ApplicationModule::class, DispatcherModule::class, LoggerModule::class, OppiaClockModule::class, + ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, + MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, + NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, + InteractionsModule::class, GcsResourceModule::class, GlideImageLoaderModule::class, + ImageParsingModule::class, HtmlParserEntityTypeModule::class, CachingModule::class, + QuestionModule::class, AccessibilityProdModule::class, ImageClickInputModule::class, + LogStorageModule::class, IntentFactoryShimModule::class, ViewBindingShimModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + RatioInputModule::class, UncaughtExceptionLoggerModule::class, + ApplicationStartupListenerModule::class, LogReportWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class, NetworkModule::class, PracticeTabModule::class, + PlatformParameterModule::class, PlatformParameterSingletonModule::class, + ExplorationStorageModule::class, DeveloperOptionsModule::class, + PlatformParameterSyncUpWorkerModule::class, NetworkConfigProdModule::class, AssetModule::class, + LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, SplitScreenInteractionModule::class, + LoggingIdentifierModule::class, ApplicationLifecycleModule::class, + NetworkConnectionDebugUtilModule::class, LoggingIdentifierModule::class, + SyncStatusModule::class, LogReportingModule::class, NetworkConnectionUtilProdModule::class, + HintsAndSolutionProdModule::class, MetricLogSchedulerModule::class, + ActivityLifecycleObserverModule::class, PerformanceMetricsAssessorModule::class, + PerformanceMetricsConfigurationsModule::class, GaBuildFlavorModule::class, + EventLoggingConfigurationModule::class + ] +) +interface GaApplicationComponent : ApplicationComponent { + /** + * The [ApplicationComponent.Builder] for this component. Dagger will generate an implementation + * of this builder for use. + */ + @Component.Builder + interface Builder : ApplicationComponent.Builder { + override fun build(): GaApplicationComponent + } +} diff --git a/app/src/main/java/org/oppia/android/app/application/ga/GaBuildFlavorModule.kt b/app/src/main/java/org/oppia/android/app/application/ga/GaBuildFlavorModule.kt new file mode 100644 index 00000000000..816b4df088d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/ga/GaBuildFlavorModule.kt @@ -0,0 +1,12 @@ +package org.oppia.android.app.application.ga + +import dagger.Module +import dagger.Provides +import org.oppia.android.app.model.BuildFlavor + +/** Module for providing the compile-time [BuildFlavor] of generally available builds of the app. */ +@Module +class GaBuildFlavorModule { + @Provides + fun provideGaBuildFlavor(): BuildFlavor = BuildFlavor.GENERAL_AVAILABILITY +} diff --git a/app/src/main/java/org/oppia/android/app/application/ga/GaOppiaApplication.kt b/app/src/main/java/org/oppia/android/app/application/ga/GaOppiaApplication.kt new file mode 100644 index 00000000000..001f10b032a --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/ga/GaOppiaApplication.kt @@ -0,0 +1,6 @@ +package org.oppia.android.app.application.ga + +import org.oppia.android.app.application.AbstractOppiaApplication + +/** The root [AbstractOppiaApplication] for general availability builds of the Oppia app. */ +class GaOppiaApplication : AbstractOppiaApplication(DaggerGaApplicationComponent::builder) diff --git a/app/src/main/java/org/oppia/android/app/application/testing/BUILD.bazel b/app/src/main/java/org/oppia/android/app/application/testing/BUILD.bazel new file mode 100644 index 00000000000..f55e5d15b86 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/testing/BUILD.bazel @@ -0,0 +1,22 @@ +""" +This package contains testing utilities that may be needed to set up the root application for +testing environments. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "testing_build_flavor_module", + testonly = True, + srcs = [ + "TestingBuildFlavorModule.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + ":dagger", + "//model/src/main/proto:version_java_proto_lite", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/application/testing/TestingBuildFlavorModule.kt b/app/src/main/java/org/oppia/android/app/application/testing/TestingBuildFlavorModule.kt new file mode 100644 index 00000000000..e23384d275f --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/application/testing/TestingBuildFlavorModule.kt @@ -0,0 +1,15 @@ +package org.oppia.android.app.application.testing + +import dagger.Module +import dagger.Provides +import org.oppia.android.app.model.BuildFlavor + +/** + * Module for providing the compile-time [BuildFlavor] of test environment exclusive builds of the + * app. + */ +@Module +class TestingBuildFlavorModule { + @Provides + fun provideTestingBuildFlavor(): BuildFlavor = BuildFlavor.TESTING +} diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryItemViewModel.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryItemViewModel.kt index ce634b15ccc..fff1d388067 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryItemViewModel.kt @@ -5,6 +5,7 @@ import org.oppia.android.app.home.RouteToTopicPlayStoryListener import org.oppia.android.app.model.CompletedStory import org.oppia.android.app.shim.IntentFactoryShim import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.domain.translation.TranslationController /** Completed story view model for the recycler view in [CompletedStoryListFragment]. */ class CompletedStoryItemViewModel( @@ -12,8 +13,19 @@ class CompletedStoryItemViewModel( private val internalProfileId: Int, val completedStory: CompletedStory, val entityType: String, - private val intentFactoryShim: IntentFactoryShim + private val intentFactoryShim: IntentFactoryShim, + translationController: TranslationController ) : ObservableViewModel(), RouteToTopicPlayStoryListener { + val completedStoryName by lazy { + translationController.extractString( + completedStory.storyTitle, completedStory.storyWrittenTranslationContext + ) + } + val topicName by lazy { + translationController.extractString( + completedStory.topicTitle, completedStory.topicWrittenTranslationContext + ) + } fun onCompletedStoryItemClicked() { routeToTopicPlayStory(internalProfileId, completedStory.topicId, completedStory.storyId) diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivity.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivity.kt index 1a0dcce859b..0d1aa27fc3d 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivity.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.COMPLETED_STORY_LIST_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for completed stories. */ @@ -29,6 +31,7 @@ class CompletedStoryListActivity : InjectableAppCompatActivity() { fun createCompletedStoryListActivityIntent(context: Context, internalProfileId: Int): Intent { val intent = Intent(context, CompletedStoryListActivity::class.java) intent.putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId) + intent.decorateWithScreenName(COMPLETED_STORY_LIST_ACTIVITY) return intent } } diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt index 4086552343e..5499e461e66 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt @@ -10,6 +10,7 @@ import org.oppia.android.app.shim.IntentFactoryShim import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.parser.html.StoryHtmlParserEntityType @@ -22,6 +23,7 @@ class CompletedStoryListViewModel @Inject constructor( private val intentFactoryShim: IntentFactoryShim, private val topicController: TopicController, private val oppiaLogger: OppiaLogger, + private val translationController: TranslationController, @StoryHtmlParserEntityType private val entityType: String ) : ObservableViewModel() { /** [internalProfileId] needs to be set before any of the live data members can be accessed. */ @@ -73,7 +75,8 @@ class CompletedStoryListViewModel @Inject constructor( internalProfileId, completedStory, entityType, - intentFactoryShim + intentFactoryShim, + translationController ) } ) diff --git a/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt b/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt index 49972bda253..1fb50be3f5b 100644 --- a/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt @@ -28,28 +28,31 @@ class FractionInputInteractionView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyle: Int = android.R.attr.editTextStyle ) : EditText(context, attrs, defStyle), View.OnFocusChangeListener { - private val hintText: CharSequence + private var hintText: CharSequence = "" private val stateKeyboardButtonListener: StateKeyboardButtonListener init { onFocusChangeListener = this - hintText = (hint ?: "") + // Assume multi-line for the purpose of properly showing long hints. + setSingleLine(hint != null) stateKeyboardButtonListener = context as StateKeyboardButtonListener } + // TODO(#4574): Add tests to verify that the placeholder correctly shows/doesn’t show when expected override fun onFocusChange(v: View, hasFocus: Boolean) = if (hasFocus) { - hint = "" - typeface = Typeface.DEFAULT + hintText = hint + hideHint() showSoftKeyboard(v, context) } else { - hint = hintText - if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + restoreHint() hideSoftKeyboard(v, context) } override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean { - if (event.keyCode == KEYCODE_BACK && event.action == ACTION_UP) - this.clearFocus() + if (event.keyCode == KEYCODE_BACK && event.action == ACTION_UP) { + clearFocus() + restoreHint() + } return super.onKeyPreIme(keyCode, event) } @@ -59,4 +62,16 @@ class FractionInputInteractionView @JvmOverloads constructor( } super.onEditorAction(actionCode) } + + private fun hideHint() { + hint = "" + typeface = Typeface.DEFAULT + setSingleLine(true) + } + + private fun restoreHint() { + hint = hintText + if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + setSingleLine(false) + } } diff --git a/app/src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt b/app/src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt index 26b606237c0..00de2602d42 100644 --- a/app/src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt +++ b/app/src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt @@ -29,28 +29,30 @@ class MathExpressionInteractionsView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyle: Int = android.R.attr.editTextStyle ) : EditText(context, attrs, defStyle), View.OnFocusChangeListener { - private var hintText: CharSequence + private var hintText: CharSequence = "" private val stateKeyboardButtonListener: StateKeyboardButtonListener init { onFocusChangeListener = this - hintText = (hint ?: "") + // Assume multi-line for the purpose of properly showing long hints. + setSingleLine(hint != null) stateKeyboardButtonListener = context as StateKeyboardButtonListener } + // TODO(#4574): Add tests to verify that the placeholder correctly shows/doesn’t show when expected override fun onFocusChange(v: View, hasFocus: Boolean) = if (hasFocus) { - hint = "" - typeface = Typeface.DEFAULT + hintText = hint + hideHint() showSoftKeyboard(v, context) } else { - hint = hintText - if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + restoreHint() hideSoftKeyboard(v, context) } override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean { if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { clearFocus() + restoreHint() } return super.onKeyPreIme(keyCode, event) } @@ -73,4 +75,16 @@ class MathExpressionInteractionsView @JvmOverloads constructor( hint = placeholderText } } + + private fun hideHint() { + hint = "" + typeface = Typeface.DEFAULT + setSingleLine(true) + } + + private fun restoreHint() { + hint = hintText + if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + setSingleLine(false) + } } diff --git a/app/src/main/java/org/oppia/android/app/customview/interaction/NumericInputInteractionView.kt b/app/src/main/java/org/oppia/android/app/customview/interaction/NumericInputInteractionView.kt index 093795a1cb2..b6745cad7d9 100644 --- a/app/src/main/java/org/oppia/android/app/customview/interaction/NumericInputInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/customview/interaction/NumericInputInteractionView.kt @@ -27,27 +27,30 @@ class NumericInputInteractionView @JvmOverloads constructor( defStyle: Int = android.R.attr.editTextStyle ) : EditText(context, attrs, defStyle), View.OnFocusChangeListener { private val stateKeyboardButtonListener: StateKeyboardButtonListener - private val hintText: CharSequence + private var hintText: CharSequence = "" init { onFocusChangeListener = this - hintText = (hint ?: "") + // Assume multi-line for the purpose of properly showing long hints. + setSingleLine(hint != null) stateKeyboardButtonListener = context as StateKeyboardButtonListener } + // TODO(#4574): Add tests to verify that the placeholder correctly shows/doesn’t show when expected override fun onFocusChange(v: View, hasFocus: Boolean) = if (hasFocus) { - hint = "" - typeface = Typeface.DEFAULT + hintText = hint + hideHint() showSoftKeyboard(v, context) } else { - hint = hintText - if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + restoreHint() hideSoftKeyboard(v, context) } override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean { - if (event.keyCode == KEYCODE_BACK && event.action == ACTION_UP) - this.clearFocus() + if (event.keyCode == KEYCODE_BACK && event.action == ACTION_UP) { + clearFocus() + restoreHint() + } return super.onKeyPreIme(keyCode, event) } @@ -57,4 +60,16 @@ class NumericInputInteractionView @JvmOverloads constructor( } super.onEditorAction(actionCode) } + + private fun hideHint() { + hint = "" + typeface = Typeface.DEFAULT + setSingleLine(true) + } + + private fun restoreHint() { + hint = hintText + if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + setSingleLine(false) + } } diff --git a/app/src/main/java/org/oppia/android/app/customview/interaction/RatioInputInteractionView.kt b/app/src/main/java/org/oppia/android/app/customview/interaction/RatioInputInteractionView.kt index c3eb784514d..fd29579a1fd 100644 --- a/app/src/main/java/org/oppia/android/app/customview/interaction/RatioInputInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/customview/interaction/RatioInputInteractionView.kt @@ -16,28 +16,31 @@ class RatioInputInteractionView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyle: Int = android.R.attr.editTextStyle ) : EditText(context, attrs, defStyle), View.OnFocusChangeListener { - private val hintText: CharSequence + private var hintText: CharSequence = "" private val stateKeyboardButtonListener: StateKeyboardButtonListener init { onFocusChangeListener = this - hintText = (hint ?: "") + // Assume multi-line for the purpose of properly showing long hints. + setSingleLine(hint != null) stateKeyboardButtonListener = context as StateKeyboardButtonListener } + // TODO(#4574): Add tests to verify that the placeholder correctly shows/doesn’t show when expected override fun onFocusChange(v: View, hasFocus: Boolean) = if (hasFocus) { - hint = "" - typeface = Typeface.DEFAULT + hintText = hint + hideHint() KeyboardHelper.showSoftKeyboard(v, context) } else { - hint = hintText - if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + restoreHint() KeyboardHelper.hideSoftKeyboard(v, context) } override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean { - if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) - this.clearFocus() + if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { + clearFocus() + restoreHint() + } return super.onKeyPreIme(keyCode, event) } @@ -47,4 +50,16 @@ class RatioInputInteractionView @JvmOverloads constructor( } super.onEditorAction(actionCode) } + + private fun hideHint() { + hint = "" + typeface = Typeface.DEFAULT + setSingleLine(true) + } + + private fun restoreHint() { + hint = hintText + if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + setSingleLine(false) + } } diff --git a/app/src/main/java/org/oppia/android/app/customview/interaction/TextInputInteractionView.kt b/app/src/main/java/org/oppia/android/app/customview/interaction/TextInputInteractionView.kt index d3400a10e51..10001acd258 100644 --- a/app/src/main/java/org/oppia/android/app/customview/interaction/TextInputInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/customview/interaction/TextInputInteractionView.kt @@ -23,28 +23,31 @@ class TextInputInteractionView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyle: Int = android.R.attr.editTextStyle ) : EditText(context, attrs, defStyle), View.OnFocusChangeListener { - private val hintText: CharSequence + private var hintText: CharSequence = "" private val stateKeyboardButtonListener: StateKeyboardButtonListener init { onFocusChangeListener = this - hintText = (hint ?: "") + // Assume multi-line for the purpose of properly showing long hints. + setSingleLine(hint != null) stateKeyboardButtonListener = context as StateKeyboardButtonListener } + // TODO(#4574): Add tests to verify that the placeholder correctly shows/doesn’t show when expected override fun onFocusChange(v: View, hasFocus: Boolean) = if (hasFocus) { - hint = "" - typeface = Typeface.DEFAULT + hintText = hint + hideHint() showSoftKeyboard(v, context) } else { - hint = hintText - if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + restoreHint() hideSoftKeyboard(v, context) } override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean { - if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) - this.clearFocus() + if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { + clearFocus() + restoreHint() + } return super.onKeyPreIme(keyCode, event) } @@ -54,4 +57,16 @@ class TextInputInteractionView @JvmOverloads constructor( } super.onEditorAction(actionCode) } + + private fun hideHint() { + hint = "" + typeface = Typeface.DEFAULT + setSingleLine(true) + } + + private fun restoreHint() { + hint = hintText + if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + setSingleLine(false) + } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt index 6d7b2a5bc4e..2d6b82eb17b 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt @@ -13,7 +13,9 @@ import org.oppia.android.app.devoptions.marktopicscompleted.MarkTopicsCompletedA import org.oppia.android.app.devoptions.mathexpressionparser.MathExpressionParserActivity import org.oppia.android.app.devoptions.vieweventlogs.ViewEventLogsActivity import org.oppia.android.app.drawer.NAVIGATION_PROFILE_ID_ARGUMENT_KEY +import org.oppia.android.app.model.ScreenName.DEVELOPER_OPTIONS_ACTIVITY import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for Developer Options. */ @@ -79,9 +81,10 @@ class DeveloperOptionsActivity : companion object { /** Function to create intent for DeveloperOptionsActivity */ fun createDeveloperOptionsActivityIntent(context: Context, internalProfileId: Int): Intent { - val intent = Intent(context, DeveloperOptionsActivity::class.java) - intent.putExtra(NAVIGATION_PROFILE_ID_ARGUMENT_KEY, internalProfileId) - return intent + return Intent(context, DeveloperOptionsActivity::class.java).apply { + putExtra(NAVIGATION_PROFILE_ID_ARGUMENT_KEY, internalProfileId) + decorateWithScreenName(DEVELOPER_OPTIONS_ACTIVITY) + } } fun getIntentKey(): String { diff --git a/app/src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivity.kt index b7eb390c537..ff8049516bb 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivity.kt @@ -6,7 +6,9 @@ import android.os.Bundle import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.FORCE_NETWORK_TYPE_ACTIVITY import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for forcing the network mode for the app. */ @@ -27,7 +29,9 @@ class ForceNetworkTypeActivity : InjectableAppCompatActivity() { companion object { /** Returns [Intent] for [ForceNetworkTypeActivity]. */ fun createForceNetworkTypeActivityIntent(context: Context): Intent { - return Intent(context, ForceNetworkTypeActivity::class.java) + return Intent(context, ForceNetworkTypeActivity::class.java).apply { + decorateWithScreenName(FORCE_NETWORK_TYPE_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/ChapterSummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/ChapterSummaryViewModel.kt index 27ac347f097..6b9dd7a112e 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/ChapterSummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/ChapterSummaryViewModel.kt @@ -1,16 +1,28 @@ package org.oppia.android.app.devoptions.markchapterscompleted import org.oppia.android.app.model.ChapterPlayState -import org.oppia.android.app.model.ChapterSummary +import org.oppia.android.app.model.EphemeralChapterSummary +import org.oppia.android.domain.translation.TranslationController /** [MarkChaptersCompletedItemViewModel] for displaying a chapter summary. */ class ChapterSummaryViewModel( val chapterIndex: Int, - val chapterSummary: ChapterSummary, + ephemeralChapterSummary: EphemeralChapterSummary, val nextStoryIndex: Int, val storyId: String, - val topicId: String + val topicId: String, + translationController: TranslationController ) : MarkChaptersCompletedItemViewModel() { + /** The summary of the chapter being displayed. */ + val chapterSummary = ephemeralChapterSummary.chapterSummary + + /** The localized title of the chapter being displayed. */ + val chapterTitle by lazy { + translationController.extractString( + chapterSummary.title, ephemeralChapterSummary.writtenTranslationContext + ) + } + /** Returns whether the chapter represented by the current view model is completed. */ fun checkIfChapterIsCompleted(): Boolean = chapterSummary.chapterPlayState == ChapterPlayState.COMPLETED 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 a867ad8af51..27997cc5bb0 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 @@ -7,7 +7,9 @@ import android.view.MenuItem import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.MARK_CHAPTERS_COMPLETED_ACTIVITY import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for Mark Chapters Completed. */ @@ -40,9 +42,10 @@ class MarkChaptersCompletedActivity : InjectableAppCompatActivity() { const val PROFILE_ID_EXTRA_KEY = "MarkChaptersCompletedActivity.profile_id" fun createMarkChaptersCompletedIntent(context: Context, internalProfileId: Int): Intent { - val intent = Intent(context, MarkChaptersCompletedActivity::class.java) - intent.putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId) - return intent + return Intent(context, MarkChaptersCompletedActivity::class.java).apply { + putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId) + decorateWithScreenName(MARK_CHAPTERS_COMPLETED_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt index 1dcf18564d5..65fb174dc5c 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt @@ -10,7 +10,6 @@ import androidx.recyclerview.widget.RecyclerView import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId import org.oppia.android.app.recyclerview.BindableAdapter -import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.MarkChaptersCompletedChapterSummaryViewBinding import org.oppia.android.databinding.MarkChaptersCompletedFragmentBinding import org.oppia.android.databinding.MarkChaptersCompletedStorySummaryViewBinding @@ -22,7 +21,7 @@ import javax.inject.Inject class MarkChaptersCompletedFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider, + private val viewModel: MarkChaptersCompletedViewModel, private val modifyLessonProgressController: ModifyLessonProgressController ) : ChapterSelector { private lateinit var binding: MarkChaptersCompletedFragmentBinding @@ -49,13 +48,13 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( binding.apply { this.lifecycleOwner = fragment - this.viewModel = getMarkChaptersCompletedViewModel() + this.viewModel = this@MarkChaptersCompletedFragmentPresenter.viewModel } this.selectedExplorationIdList = selectedExplorationIdList profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() - getMarkChaptersCompletedViewModel().setProfileId(profileId) + viewModel.setProfileId(profileId) linearLayoutManager = LinearLayoutManager(activity.applicationContext) @@ -68,7 +67,7 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( binding.markChaptersCompletedAllCheckBoxContainer.setOnClickListener { if (binding.isAllChecked == null || binding.isAllChecked == false) { binding.isAllChecked = true - getMarkChaptersCompletedViewModel().getItemList().forEach { viewModel -> + viewModel.getItemList().forEach { viewModel -> if (viewModel is ChapterSummaryViewModel) { if (!viewModel.checkIfChapterIsCompleted()) chapterSelected( @@ -93,8 +92,7 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( binding.markChaptersCompletedMarkCompletedTextView.setOnClickListener { modifyLessonProgressController.markMultipleChaptersCompleted( profileId = profileId, - chapterMap = getMarkChaptersCompletedViewModel().getChapterMap() - .filterKeys { selectedExplorationIdList.contains(it) } + chapterMap = viewModel.getChapterMap().filterKeys { selectedExplorationIdList.contains(it) } ) activity.finish() } @@ -131,7 +129,7 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( model: ChapterSummaryViewModel ) { binding.viewModel = model - val notCompletedChapterCount = getMarkChaptersCompletedViewModel().getItemList().count { + val notCompletedChapterCount = viewModel.getItemList().count { it is ChapterSummaryViewModel && !it.checkIfChapterIsCompleted() } if (notCompletedChapterCount == 0) { @@ -164,16 +162,12 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( } } - private fun getMarkChaptersCompletedViewModel(): MarkChaptersCompletedViewModel { - return viewModelProvider.getForFragment(fragment, MarkChaptersCompletedViewModel::class.java) - } - override fun chapterSelected(chapterIndex: Int, nextStoryIndex: Int, explorationId: String) { if (!selectedExplorationIdList.contains(explorationId)) { selectedExplorationIdList.add(explorationId) } if (selectedExplorationIdList.size == - getMarkChaptersCompletedViewModel().getItemList().count { + viewModel.getItemList().count { it is ChapterSummaryViewModel && !it.checkIfChapterIsCompleted() } ) { @@ -191,14 +185,13 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( override fun chapterUnselected(chapterIndex: Int, nextStoryIndex: Int) { for (index in chapterIndex until nextStoryIndex) { val explorationId = - (getMarkChaptersCompletedViewModel().getItemList()[index] as ChapterSummaryViewModel) - .chapterSummary.explorationId + (viewModel.getItemList()[index] as ChapterSummaryViewModel).chapterSummary.explorationId if (selectedExplorationIdList.contains(explorationId)) { selectedExplorationIdList.remove(explorationId) } } if (selectedExplorationIdList.size != - getMarkChaptersCompletedViewModel().getItemList().count { + viewModel.getItemList().count { it is ChapterSummaryViewModel && !it.checkIfChapterIsCompleted() } ) { diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt index 24d1b7fc4e7..3c7fe863f5e 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt @@ -3,11 +3,12 @@ package org.oppia.android.app.devoptions.markchapterscompleted import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralStorySummary import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.StorySummary import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.devoptions.ModifyLessonProgressController import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -19,7 +20,8 @@ import javax.inject.Inject @FragmentScope class MarkChaptersCompletedViewModel @Inject constructor( private val oppiaLogger: OppiaLogger, - private val modifyLessonProgressController: ModifyLessonProgressController + private val modifyLessonProgressController: ModifyLessonProgressController, + private val translationController: TranslationController ) : ObservableViewModel() { private lateinit var profileId: ProfileId @@ -34,20 +36,22 @@ class MarkChaptersCompletedViewModel @Inject constructor( Transformations.map(storyMapLiveData, ::processStoryMap) } - private val storyMapLiveData: LiveData>> by lazy { getStoryMap() } + private val storyMapLiveData: LiveData>> by lazy { + getStoryMap() + } - private fun getStoryMap(): LiveData>> { + private fun getStoryMap(): LiveData>> { return Transformations.map(storyMapResultLiveData, ::processStoryMapResult) } private val storyMapResultLiveData: - LiveData>>> by lazy { + LiveData>>> by lazy { modifyLessonProgressController.getStoryMapWithProgress(profileId).toLiveData() } private fun processStoryMapResult( - storyMap: AsyncResult>> - ): Map> { + storyMap: AsyncResult>> + ): Map> { return when (storyMap) { is AsyncResult.Failure -> { oppiaLogger.e( @@ -61,24 +65,30 @@ class MarkChaptersCompletedViewModel @Inject constructor( } private fun processStoryMap( - storyMap: Map> + storyMap: Map> ): List { itemList.clear() var nextStoryIndex: Int var chapterIndex = 0 storyMap.forEach { storyMapItem -> - storyMapItem.value.forEach { storySummary -> - itemList.add(StorySummaryViewModel(storyName = storySummary.storyName)) + storyMapItem.value.forEach { ephemeralStorySummary -> + val storySummary = ephemeralStorySummary.storySummary + val storyTitle = + translationController.extractString( + storySummary.storyTitle, ephemeralStorySummary.writtenTranslationContext + ) + itemList.add(StorySummaryViewModel(storyTitle)) chapterIndex++ nextStoryIndex = chapterIndex + storySummary.chapterCount - storySummary.chapterList.forEach { chapterSummary -> + ephemeralStorySummary.chaptersList.forEach { ephemeralChapterSummary -> itemList.add( ChapterSummaryViewModel( chapterIndex = chapterIndex, - chapterSummary = chapterSummary, + ephemeralChapterSummary = ephemeralChapterSummary, nextStoryIndex = nextStoryIndex, storyId = storySummary.storyId, - topicId = storyMapItem.key + topicId = storyMapItem.key, + translationController ) ) chapterIndex++ diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/StorySummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/StorySummaryViewModel.kt index cd322688a90..9e9bb2ce670 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/StorySummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/StorySummaryViewModel.kt @@ -1,4 +1,4 @@ package org.oppia.android.app.devoptions.markchapterscompleted /** [MarkChaptersCompletedItemViewModel] for displaying a story. */ -class StorySummaryViewModel(val storyName: String) : MarkChaptersCompletedItemViewModel() +class StorySummaryViewModel(val storyTitle: String) : MarkChaptersCompletedItemViewModel() 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 453f4a117e1..140fee54421 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 @@ -7,7 +7,9 @@ import android.view.MenuItem import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.MARK_STORIES_COMPLETED_ACTIVITY import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for Mark Stories Completed. */ @@ -40,9 +42,10 @@ class MarkStoriesCompletedActivity : InjectableAppCompatActivity() { const val PROFILE_ID_EXTRA_KEY = "MarkStoriesCompletedActivity.profile_id" fun createMarkStoriesCompletedIntent(context: Context, internalProfileId: Int): Intent { - val intent = Intent(context, MarkStoriesCompletedActivity::class.java) - intent.putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId) - return intent + return Intent(context, MarkStoriesCompletedActivity::class.java).apply { + putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId) + decorateWithScreenName(MARK_STORIES_COMPLETED_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedFragmentPresenter.kt index b58b07faaa6..91c61fde9b6 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedFragmentPresenter.kt @@ -9,7 +9,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId import org.oppia.android.app.recyclerview.BindableAdapter -import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.MarkStoriesCompletedFragmentBinding import org.oppia.android.databinding.MarkStoriesCompletedStorySummaryViewBinding import org.oppia.android.domain.devoptions.ModifyLessonProgressController @@ -20,7 +19,7 @@ import javax.inject.Inject class MarkStoriesCompletedFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider, + private val viewModel: MarkStoriesCompletedViewModel, private val modifyLessonProgressController: ModifyLessonProgressController ) : StorySelector { private lateinit var binding: MarkStoriesCompletedFragmentBinding @@ -47,13 +46,13 @@ class MarkStoriesCompletedFragmentPresenter @Inject constructor( binding.apply { this.lifecycleOwner = fragment - this.viewModel = getMarkStoriesCompletedViewModel() + this.viewModel = this@MarkStoriesCompletedFragmentPresenter.viewModel } this.selectedStoryIdList = selectedStoryIdList profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() - getMarkStoriesCompletedViewModel().setProfileId(profileId) + viewModel.setProfileId(profileId) linearLayoutManager = LinearLayoutManager(activity.applicationContext) @@ -73,7 +72,7 @@ class MarkStoriesCompletedFragmentPresenter @Inject constructor( binding.markStoriesCompletedAllCheckBox.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - getMarkStoriesCompletedViewModel().getStorySummaryMap().values.forEach { viewModel -> + viewModel.getStorySummaryMap().values.forEach { viewModel -> if (!viewModel.isCompleted) storySelected(viewModel.storySummary.storyId) } @@ -88,8 +87,9 @@ class MarkStoriesCompletedFragmentPresenter @Inject constructor( binding.markStoriesCompletedMarkCompletedTextView.setOnClickListener { modifyLessonProgressController.markMultipleStoriesCompleted( profileId, - getMarkStoriesCompletedViewModel().getStorySummaryMap() - .filterKeys { selectedStoryIdList.contains(it) }.mapValues { it.value.topicId } + viewModel.getStorySummaryMap().filterKeys { + selectedStoryIdList.contains(it) + }.mapValues { it.value.topicId } ) activity.finish() } @@ -112,9 +112,7 @@ class MarkStoriesCompletedFragmentPresenter @Inject constructor( model: StorySummaryViewModel ) { binding.viewModel = model - if (getMarkStoriesCompletedViewModel().getStorySummaryMap().values - .count { !it.isCompleted } == 0 - ) { + if (viewModel.getStorySummaryMap().values.count { !it.isCompleted } == 0) { this.binding.isAllChecked = true } if (model.isCompleted) { @@ -132,17 +130,13 @@ class MarkStoriesCompletedFragmentPresenter @Inject constructor( } } - private fun getMarkStoriesCompletedViewModel(): MarkStoriesCompletedViewModel { - return viewModelProvider.getForFragment(fragment, MarkStoriesCompletedViewModel::class.java) - } - override fun storySelected(storyId: String) { if (!selectedStoryIdList.contains(storyId)) { selectedStoryIdList.add(storyId) } if (selectedStoryIdList.size == - getMarkStoriesCompletedViewModel().getStorySummaryMap().values.count { !it.isCompleted } + viewModel.getStorySummaryMap().values.count { !it.isCompleted } ) { binding.isAllChecked = true } @@ -154,7 +148,7 @@ class MarkStoriesCompletedFragmentPresenter @Inject constructor( } if (selectedStoryIdList.size != - getMarkStoriesCompletedViewModel().getStorySummaryMap().values.count { !it.isCompleted } + viewModel.getStorySummaryMap().values.count { !it.isCompleted } ) { binding.isAllChecked = false } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt index 8ec76b5d69e..d728135bfbb 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt @@ -3,11 +3,12 @@ package org.oppia.android.app.devoptions.markstoriescompleted import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralStorySummary import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.StorySummary import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.devoptions.ModifyLessonProgressController import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -19,7 +20,8 @@ import javax.inject.Inject @FragmentScope class MarkStoriesCompletedViewModel @Inject constructor( private val oppiaLogger: OppiaLogger, - private val modifyLessonProgressController: ModifyLessonProgressController + private val modifyLessonProgressController: ModifyLessonProgressController, + private val translationController: TranslationController ) : ObservableViewModel() { private lateinit var profileId: ProfileId @@ -34,20 +36,22 @@ class MarkStoriesCompletedViewModel @Inject constructor( Transformations.map(storyMapLiveData, ::processStoryMap) } - private val storyMapLiveData: LiveData>> by lazy { getStoryMap() } + private val storyMapLiveData: LiveData>> by lazy { + getStoryMap() + } private val storyMapResultLiveData: - LiveData>>> by lazy { + LiveData>>> by lazy { modifyLessonProgressController.getStoryMapWithProgress(profileId).toLiveData() } - private fun getStoryMap(): LiveData>> { + private fun getStoryMap(): LiveData>> { return Transformations.map(storyMapResultLiveData, ::processStoryMapResult) } private fun processStoryMapResult( - storyMap: AsyncResult>> - ): Map> { + storyMap: AsyncResult>> + ): Map> { return when (storyMap) { is AsyncResult.Failure -> { oppiaLogger.e( @@ -61,14 +65,17 @@ class MarkStoriesCompletedViewModel @Inject constructor( } private fun processStoryMap( - storyMap: Map> + storyMap: Map> ): List { itemList.clear() storyMap.forEach { - it.value.forEach { storySummary -> - val isCompleted = modifyLessonProgressController.checkIfStoryIsCompleted(storySummary) - itemList[storySummary.storyId] = - StorySummaryViewModel(storySummary, isCompleted, topicId = it.key) + it.value.forEach { ephemeralStorySummary -> + val isCompleted = + modifyLessonProgressController.checkIfStoryIsCompleted(ephemeralStorySummary) + itemList[ephemeralStorySummary.storySummary.storyId] = + StorySummaryViewModel( + ephemeralStorySummary, isCompleted, topicId = it.key, translationController + ) } } return itemList.values.toList() diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/StorySummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/StorySummaryViewModel.kt index f464fd16ed5..eca0df1aa04 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/StorySummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/StorySummaryViewModel.kt @@ -1,12 +1,24 @@ package org.oppia.android.app.devoptions.markstoriescompleted import androidx.lifecycle.ViewModel -import org.oppia.android.app.model.StorySummary +import org.oppia.android.app.model.EphemeralStorySummary import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.domain.translation.TranslationController /** [ViewModel] for displaying a story summary for the recyclerView in [MarkStoriesCompletedFragment]. */ class StorySummaryViewModel( - val storySummary: StorySummary, + ephemeralStorySummary: EphemeralStorySummary, val isCompleted: Boolean, - val topicId: String -) : ObservableViewModel() + val topicId: String, + translationController: TranslationController +) : ObservableViewModel() { + /** The summary of the story being displayed. */ + val storySummary = ephemeralStorySummary.storySummary + + /** The localized title of the story being displayed. */ + val storyTitle by lazy { + translationController.extractString( + storySummary.storyTitle, ephemeralStorySummary.writtenTranslationContext + ) + } +} 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 1eda9a71284..9533215579f 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 @@ -7,7 +7,9 @@ import android.view.MenuItem import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.MARK_TOPICS_COMPLETED_ACTIVITY import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for Mark Topics Completed. */ @@ -40,9 +42,10 @@ class MarkTopicsCompletedActivity : InjectableAppCompatActivity() { const val PROFILE_ID_EXTRA_KEY = "MarkTopicsCompletedActivity.profile_id" fun createMarkTopicsCompletedIntent(context: Context, internalProfileId: Int): Intent { - val intent = Intent(context, MarkTopicsCompletedActivity::class.java) - intent.putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId) - return intent + return Intent(context, MarkTopicsCompletedActivity::class.java).apply { + putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId) + decorateWithScreenName(MARK_TOPICS_COMPLETED_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedFragmentPresenter.kt index 0b62ca2caed..4fdb5d0d5c7 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedFragmentPresenter.kt @@ -9,7 +9,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId import org.oppia.android.app.recyclerview.BindableAdapter -import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.MarkTopicsCompletedFragmentBinding import org.oppia.android.databinding.MarkTopicsCompletedTopicViewBinding import org.oppia.android.domain.devoptions.ModifyLessonProgressController @@ -20,7 +19,7 @@ import javax.inject.Inject class MarkTopicsCompletedFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider, + private val viewModel: MarkTopicsCompletedViewModel, private val modifyLessonProgressController: ModifyLessonProgressController ) : TopicSelector { private lateinit var binding: MarkTopicsCompletedFragmentBinding @@ -47,13 +46,13 @@ class MarkTopicsCompletedFragmentPresenter @Inject constructor( binding.apply { this.lifecycleOwner = fragment - this.viewModel = getMarkTopicsCompletedViewModel() + this.viewModel = this@MarkTopicsCompletedFragmentPresenter.viewModel } this.selectedTopicIdList = selectedTopicIdList this.profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() - getMarkTopicsCompletedViewModel().setProfileId(profileId) + viewModel.setProfileId(profileId) linearLayoutManager = LinearLayoutManager(activity.applicationContext) @@ -69,7 +68,7 @@ class MarkTopicsCompletedFragmentPresenter @Inject constructor( binding.markTopicsCompletedAllCheckBox.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - getMarkTopicsCompletedViewModel().getTopicList().forEach { viewModel -> + viewModel.getTopicList().forEach { viewModel -> if (!viewModel.isCompleted) topicSelected(viewModel.topic.topicId) } } else { @@ -106,7 +105,7 @@ class MarkTopicsCompletedFragmentPresenter @Inject constructor( model: TopicViewModel ) { binding.viewModel = model - if (getMarkTopicsCompletedViewModel().getTopicList().count { !it.isCompleted } == 0) { + if (viewModel.getTopicList().count { !it.isCompleted } == 0) { this.binding.isAllChecked = true } if (model.isCompleted) { @@ -124,18 +123,12 @@ class MarkTopicsCompletedFragmentPresenter @Inject constructor( } } - private fun getMarkTopicsCompletedViewModel(): MarkTopicsCompletedViewModel { - return viewModelProvider.getForFragment(fragment, MarkTopicsCompletedViewModel::class.java) - } - override fun topicSelected(topicId: String) { if (!selectedTopicIdList.contains(topicId)) { selectedTopicIdList.add(topicId) } - if (selectedTopicIdList.size == - getMarkTopicsCompletedViewModel().getTopicList().count { !it.isCompleted } - ) { + if (selectedTopicIdList.size == viewModel.getTopicList().count { !it.isCompleted }) { binding.isAllChecked = true } } @@ -145,9 +138,7 @@ class MarkTopicsCompletedFragmentPresenter @Inject constructor( selectedTopicIdList.remove(topicId) } - if (selectedTopicIdList.size != - getMarkTopicsCompletedViewModel().getTopicList().count { !it.isCompleted } - ) { + if (selectedTopicIdList.size != viewModel.getTopicList().count { !it.isCompleted }) { binding.isAllChecked = false } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt index fd2d06e094b..958733d57d5 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt @@ -3,11 +3,12 @@ package org.oppia.android.app.devoptions.marktopicscompleted import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralTopic import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.Topic import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.devoptions.ModifyLessonProgressController import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -20,6 +21,7 @@ import javax.inject.Inject class MarkTopicsCompletedViewModel @Inject constructor( private val oppiaLogger: OppiaLogger, private val modifyLessonProgressController: ModifyLessonProgressController, + private val translationController: TranslationController ) : ObservableViewModel() { private lateinit var profileId: ProfileId @@ -34,34 +36,36 @@ class MarkTopicsCompletedViewModel @Inject constructor( Transformations.map(allTopicsLiveData, ::processAllTopics) } - private val allTopicsLiveData: LiveData> by lazy { getAllTopics() } + private val allTopicsLiveData: LiveData> by lazy { getAllTopics() } - private val allTopicsResultLiveData: LiveData>> by lazy { + private val allTopicsResultLiveData: LiveData>> by lazy { modifyLessonProgressController.getAllTopicsWithProgress(profileId).toLiveData() } - private fun getAllTopics(): LiveData> { + private fun getAllTopics(): LiveData> { return Transformations.map(allTopicsResultLiveData, ::processAllTopicsResult) } - private fun processAllTopicsResult(allTopics: AsyncResult>): List { - return when (allTopics) { + private fun processAllTopicsResult( + ephemeralResult: AsyncResult> + ): List { + return when (ephemeralResult) { is AsyncResult.Failure -> { oppiaLogger.e( - "MarkTopicsCompletedFragment", "Failed to retrieve all topics", allTopics.error + "MarkTopicsCompletedFragment", "Failed to retrieve all topics", ephemeralResult.error ) mutableListOf() } is AsyncResult.Pending -> mutableListOf() - is AsyncResult.Success -> allTopics.value + is AsyncResult.Success -> ephemeralResult.value } } - private fun processAllTopics(allTopics: List): List { + private fun processAllTopics(allTopics: List): List { itemList.clear() - allTopics.forEach { topic -> - val isCompleted = modifyLessonProgressController.checkIfTopicIsCompleted(topic) - itemList.add(TopicViewModel(topic, isCompleted)) + allTopics.forEach { ephemeralTopic -> + val isCompleted = modifyLessonProgressController.checkIfTopicIsCompleted(ephemeralTopic) + itemList.add(TopicViewModel(ephemeralTopic, isCompleted, translationController)) } return itemList } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/TopicViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/TopicViewModel.kt index 1f2c8df1df7..9c3d174f1ab 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/TopicViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/TopicViewModel.kt @@ -1,8 +1,21 @@ package org.oppia.android.app.devoptions.marktopicscompleted import androidx.lifecycle.ViewModel -import org.oppia.android.app.model.Topic +import org.oppia.android.app.model.EphemeralTopic import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.domain.translation.TranslationController /** [ViewModel] for displaying a topic for the recyclerView in [MarkTopicsCompletedFragment]. */ -class TopicViewModel(val topic: Topic, val isCompleted: Boolean) : ObservableViewModel() +class TopicViewModel( + ephemeralTopic: EphemeralTopic, + val isCompleted: Boolean, + translationController: TranslationController +) : ObservableViewModel() { + /** The summary of the topic being displayed. */ + val topic = ephemeralTopic.topic + + /** The localized title of the topic being displayed. */ + val topicTitle by lazy { + translationController.extractString(topic.title, ephemeralTopic.writtenTranslationContext) + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt index 25349699375..69c5c31ace4 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt @@ -6,7 +6,9 @@ import android.os.Bundle import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.MATH_EXPRESSION_PARSER_ACTIVITY import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity to allow the user to test math expressions/equations. */ @@ -27,7 +29,9 @@ class MathExpressionParserActivity : InjectableAppCompatActivity() { companion object { /** Returns [Intent] for [MathExpressionParserActivity]. */ fun createIntent(context: Context): Intent { - return Intent(context, MathExpressionParserActivity::class.java) + return Intent(context, MathExpressionParserActivity::class.java).apply { + decorateWithScreenName(MATH_EXPRESSION_PARSER_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt index 741b17ca7aa..e808c6fdb68 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt @@ -36,7 +36,8 @@ class MathExpressionParserViewModel @Inject constructor( gcsResourceName = "", entityType = "", entityId = "", - imageCenterAlign = false + imageCenterAlign = false, + displayLocale = appLanguageResourceHandler.getDisplayLocale() ) } private lateinit var parseResultTextView: TextView @@ -89,7 +90,10 @@ class MathExpressionParserViewModel @Inject constructor( val newText = computeParseResult() // Only parse HTML if there is HTML to preserve formatting. parseResultTextView.text = if ("oppia-noninteractive-math" in newText) { - htmlParser.parseOppiaHtml(newText.replace("\n", "
"), parseResultTextView) + htmlParser.parseOppiaHtml( + newText.replace("\n", "
"), + parseResultTextView + ) } else newText } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/vieweventlogs/ViewEventLogsActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/vieweventlogs/ViewEventLogsActivity.kt index 38dc66d93fa..ec3dc787202 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/vieweventlogs/ViewEventLogsActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/vieweventlogs/ViewEventLogsActivity.kt @@ -6,7 +6,9 @@ import android.os.Bundle import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.VIEW_EVENT_LOGS_ACTIVITY import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for View Event Logs. */ @@ -26,7 +28,9 @@ class ViewEventLogsActivity : InjectableAppCompatActivity() { companion object { fun createViewEventLogsActivityIntent(context: Context): Intent { - return Intent(context, ViewEventLogsActivity::class.java) + return Intent(context, ViewEventLogsActivity::class.java).apply { + decorateWithScreenName(VIEW_EVENT_LOGS_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt b/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt index 5ec6f40e761..4ff8411e9d8 100644 --- a/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt +++ b/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt @@ -8,7 +8,6 @@ import org.oppia.android.app.administratorcontrols.LogoutDialogFragment import org.oppia.android.app.administratorcontrols.appversion.AppVersionFragment import org.oppia.android.app.administratorcontrols.learneranalytics.ProfileAndDeviceIdFragment import org.oppia.android.app.completedstorylist.CompletedStoryListFragment -import org.oppia.android.app.deprecation.AutomaticAppDeprecationNoticeDialogFragment import org.oppia.android.app.devoptions.DeveloperOptionsFragment import org.oppia.android.app.devoptions.forcenetworktype.ForceNetworkTypeFragment import org.oppia.android.app.devoptions.markchapterscompleted.MarkChaptersCompletedFragment @@ -30,6 +29,9 @@ import org.oppia.android.app.home.recentlyplayed.RecentlyPlayedFragment import org.oppia.android.app.mydownloads.DownloadsTabFragment import org.oppia.android.app.mydownloads.MyDownloadsFragment import org.oppia.android.app.mydownloads.UpdatesTabFragment +import org.oppia.android.app.notice.AutomaticAppDeprecationNoticeDialogFragment +import org.oppia.android.app.notice.BetaNoticeDialogFragment +import org.oppia.android.app.notice.GeneralAvailabilityUpgradeNoticeDialogFragment import org.oppia.android.app.onboarding.OnboardingFragment import org.oppia.android.app.ongoingtopiclist.OngoingTopicListFragment import org.oppia.android.app.options.AppLanguageFragment @@ -47,6 +49,7 @@ import org.oppia.android.app.player.state.itemviewmodel.InteractionViewModelModu import org.oppia.android.app.player.stopplaying.ProgressDatabaseFullDialogFragment import org.oppia.android.app.player.stopplaying.StopExplorationDialogFragment import org.oppia.android.app.player.stopplaying.UnsavedExplorationDialogFragment +import org.oppia.android.app.policies.PoliciesFragment import org.oppia.android.app.profile.AdminSettingsDialogFragment import org.oppia.android.app.profile.ProfileChooserFragment import org.oppia.android.app.profile.ResetPinDialogFragment @@ -105,6 +108,7 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto fun inject(audioFragment: AudioFragment) fun inject(audioLanguageFragment: AudioLanguageFragment) fun inject(autoAppDeprecationNoticeDialogFragment: AutomaticAppDeprecationNoticeDialogFragment) + fun inject(betaNoticeDialogFragment: BetaNoticeDialogFragment) fun inject(cellularAudioDialogFragment: CellularAudioDialogFragment) fun inject(completedStoryListFragment: CompletedStoryListFragment) fun inject(conceptCardFragment: ConceptCardFragment) @@ -116,6 +120,7 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto fun inject(explorationTestActivityTestFragment: ExplorationTestActivityPresenter.TestFragment) fun inject(faqListFragment: FAQListFragment) fun inject(forceNetworkTypeFragment: ForceNetworkTypeFragment) + fun inject(fragment: GeneralAvailabilityUpgradeNoticeDialogFragment) fun inject(helpFragment: HelpFragment) fun inject(hintsAndSolutionDialogFragment: HintsAndSolutionDialogFragment) fun inject(hintsAndSolutionExplorationManagerFragment: HintsAndSolutionExplorationManagerFragment) @@ -135,6 +140,7 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto fun inject(onboardingFragment: OnboardingFragment) fun inject(ongoingTopicListFragment: OngoingTopicListFragment) fun inject(optionFragment: OptionsFragment) + fun inject(policiesFragment: PoliciesFragment) fun inject(profileAndDeviceIdFragment: ProfileAndDeviceIdFragment) fun inject(profileChooserFragment: ProfileChooserFragment) fun inject(profileEditDeletionDialogFragment: ProfileEditDeletionDialogFragment) diff --git a/app/src/main/java/org/oppia/android/app/help/HelpActivity.kt b/app/src/main/java/org/oppia/android/app/help/HelpActivity.kt index 25d53ed8946..22d3b3f33b0 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpActivity.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpActivity.kt @@ -11,8 +11,15 @@ import org.oppia.android.app.help.faq.FAQListActivity import org.oppia.android.app.help.faq.RouteToFAQSingleListener import org.oppia.android.app.help.faq.faqsingle.FAQSingleActivity import org.oppia.android.app.help.thirdparty.ThirdPartyDependencyListActivity +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.model.ScreenName.HELP_ACTIVITY +import org.oppia.android.app.policies.PoliciesActivity +import org.oppia.android.app.policies.RouteToPoliciesListener import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.extensions.getStringFromBundle +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject const val HELP_OPTIONS_TITLE_SAVED_KEY = "HelpActivity.help_options_title" @@ -21,17 +28,21 @@ const val THIRD_PARTY_DEPENDENCY_INDEX_SAVED_KEY = "HelpActivity.third_party_dependency_index" const val LICENSE_INDEX_SAVED_KEY = "HelpActivity.license_index" const val FAQ_LIST_FRAGMENT_TAG = "FAQListFragment.tag" +const val POLICIES_ARGUMENT_PROTO = "PoliciesActivity.policy_page" +const val POLICIES_FRAGMENT_TAG = "PoliciesFragment.tag" const val THIRD_PARTY_DEPENDENCY_LIST_FRAGMENT_TAG = "ThirdPartyDependencyListFragment.tag" const val LICENSE_LIST_FRAGMENT_TAG = "LicenseListFragment.tag" const val LICENSE_TEXT_FRAGMENT_TAG = "LicenseTextFragment.tag" -/** The help page activity for FAQs and third-party dependencies. */ +/** The help page activity for FAQs, third-party dependencies and policies page. */ class HelpActivity : InjectableAppCompatActivity(), RouteToFAQListListener, RouteToFAQSingleListener, + RouteToPoliciesListener, RouteToThirdPartyDependencyListListener, LoadFaqListFragmentListener, + LoadPoliciesFragmentListener, LoadThirdPartyDependencyListFragmentListener, LoadLicenseListFragmentListener, LoadLicenseTextViewerFragmentListener { @@ -59,12 +70,17 @@ class HelpActivity : val selectedLicenseIndex = savedInstanceState?.getInt(LICENSE_INDEX_SAVED_KEY) ?: 0 selectedHelpOptionsTitle = savedInstanceState?.getStringFromBundle(HELP_OPTIONS_TITLE_SAVED_KEY) ?: resourceHandler.getStringInLocale(R.string.faq_activity_title) + val policiesActivityParams = savedInstanceState?.getProto( + POLICIES_ARGUMENT_PROTO, + PoliciesActivityParams.getDefaultInstance() + ) helpActivityPresenter.handleOnCreate( selectedHelpOptionsTitle, isFromNavigationDrawer, selectedFragment, selectedDependencyIndex, - selectedLicenseIndex + selectedLicenseIndex, + policiesActivityParams ) title = resourceHandler.getStringInLocale(R.string.menu_help) } @@ -82,6 +98,7 @@ class HelpActivity : val intent = Intent(context, HelpActivity::class.java) intent.putExtra(NAVIGATION_PROFILE_ID_ARGUMENT_KEY, profileId) intent.putExtra(BOOL_IS_FROM_NAVIGATION_DRAWER_EXTRA_KEY, isFromNavigationDrawer) + intent.decorateWithScreenName(HELP_ACTIVITY) return intent } } @@ -121,4 +138,12 @@ class HelpActivity : override fun onRouteToFAQSingle(question: String, answer: String) { startActivity(FAQSingleActivity.createFAQSingleActivityIntent(this, question, answer)) } + + override fun onRouteToPolicies(policyPage: PolicyPage) { + startActivity(PoliciesActivity.createPoliciesActivityIntent(this, policyPage)) + } + + override fun loadPoliciesFragment(policyPage: PolicyPage) { + helpActivityPresenter.handleLoadPoliciesFragment(policyPage) + } } diff --git a/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt index 7c2debc1189..29f2aa8123b 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt @@ -16,7 +16,12 @@ import org.oppia.android.app.help.faq.FAQListFragment import org.oppia.android.app.help.thirdparty.LicenseListFragment import org.oppia.android.app.help.thirdparty.LicenseTextViewerFragment import org.oppia.android.app.help.thirdparty.ThirdPartyDependencyListFragment +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PoliciesFragmentArguments +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.policies.PoliciesFragment import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.extensions.putProto import javax.inject.Inject /** The presenter for [HelpActivity]. */ @@ -32,18 +37,24 @@ class HelpActivityPresenter @Inject constructor( private lateinit var selectedHelpOptionTitle: String private var selectedDependencyIndex: Int? = null private var selectedLicenseIndex: Int? = null + private var internalPolicyPage: PolicyPage = PolicyPage.POLICY_PAGE_UNSPECIFIED fun handleOnCreate( helpOptionsTitle: String, isFromNavigationDrawer: Boolean, selectedFragment: String, dependencyIndex: Int, - licenseIndex: Int + licenseIndex: Int, + policiesActivityParams: PoliciesActivityParams? ) { selectedFragmentTag = selectedFragment selectedDependencyIndex = dependencyIndex selectedLicenseIndex = licenseIndex selectedHelpOptionTitle = helpOptionsTitle + if (policiesActivityParams != null) { + internalPolicyPage = policiesActivityParams.policyPage + } + if (isFromNavigationDrawer) { activity.setContentView(R.layout.help_activity) setUpToolbar() @@ -141,6 +152,12 @@ class HelpActivityPresenter @Inject constructor( outState.putString(SELECTED_FRAGMENT_SAVED_KEY, selectedFragmentTag) selectedDependencyIndex?.let { outState.putInt(THIRD_PARTY_DEPENDENCY_INDEX_SAVED_KEY, it) } selectedLicenseIndex?.let { outState.putInt(LICENSE_INDEX_SAVED_KEY, it) } + val policiesActivityParams = + PoliciesActivityParams + .newBuilder() + .setPolicyPage(internalPolicyPage) + .build() + outState.putProto(POLICIES_ARGUMENT_PROTO, policiesActivityParams) } private fun setUpToolbar() { @@ -194,6 +211,7 @@ class HelpActivityPresenter @Inject constructor( ) { when (selectedFragment) { FAQ_LIST_FRAGMENT_TAG -> handleLoadFAQListFragment() + POLICIES_FRAGMENT_TAG -> handleLoadPoliciesFragment(internalPolicyPage) THIRD_PARTY_DEPENDENCY_LIST_FRAGMENT_TAG -> handleLoadThirdPartyDependencyListFragment() LICENSE_LIST_FRAGMENT_TAG -> handleLoadLicenseListFragment(dependencyIndex) LICENSE_TEXT_FRAGMENT_TAG -> handleLoadLicenseTextViewerFragment( @@ -296,4 +314,38 @@ class HelpActivityPresenter @Inject constructor( private fun getMultipaneOptionsFragment(): Fragment? { return activity.supportFragmentManager.findFragmentById(R.id.multipane_options_container) } + + fun handleLoadPoliciesFragment(policyPage: PolicyPage) { + internalPolicyPage = policyPage + selectPoliciesFragment(policyPage) + + val policiesFragmentArguments = + PoliciesFragmentArguments + .newBuilder() + .setPolicyPage(policyPage) + .build() + val previousFragment = getMultipaneOptionsFragment() + if (previousFragment != null) { + activity.supportFragmentManager.beginTransaction().remove(previousFragment).commitNow() + } + activity.supportFragmentManager.beginTransaction().add( + R.id.multipane_options_container, + PoliciesFragment.newInstance(policiesFragmentArguments) + ).commitNow() + } + + private fun selectPoliciesFragment(policyPage: PolicyPage) { + when (policyPage) { + PolicyPage.PRIVACY_POLICY -> setMultipaneContainerTitle( + resourceHandler.getStringInLocale(R.string.privacy_policy_title) + ) + PolicyPage.TERMS_OF_SERVICE -> setMultipaneContainerTitle( + resourceHandler.getStringInLocale(R.string.terms_of_service_title) + ) + else -> { } + } + setMultipaneBackButtonVisibility(View.GONE) + selectedFragmentTag = POLICIES_FRAGMENT_TAG + selectedHelpOptionTitle = getMultipaneContainerTitle() + } } diff --git a/app/src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt b/app/src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt index 71b84a96068..a2472a4e3e1 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt @@ -2,6 +2,8 @@ package org.oppia.android.app.help import androidx.appcompat.app.AppCompatActivity import org.oppia.android.R +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.policies.RouteToPoliciesListener import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableViewModel @@ -34,6 +36,23 @@ class HelpItemViewModel( routeToThirdPartyDependencyListListener.onRouteToThirdPartyDependencyList() } } + resourceHandler.getStringInLocale(R.string.privacy_policy_title) -> { + loadPolicyPage(PolicyPage.PRIVACY_POLICY) + } + resourceHandler.getStringInLocale(R.string.terms_of_service_title) -> { + loadPolicyPage(PolicyPage.TERMS_OF_SERVICE) + } + } + } + + private fun loadPolicyPage(policyPage: PolicyPage) { + if (isMultipane) { + val loadPoliciesFragmentListener = activity as + LoadPoliciesFragmentListener + loadPoliciesFragmentListener.loadPoliciesFragment(policyPage) + } else { + val routeToPoliciesListener = activity as RouteToPoliciesListener + routeToPoliciesListener.onRouteToPolicies(policyPage) } } } diff --git a/app/src/main/java/org/oppia/android/app/help/HelpItems.kt b/app/src/main/java/org/oppia/android/app/help/HelpItems.kt index 3ad3827b5c3..a8f376f90ab 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpItems.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpItems.kt @@ -3,5 +3,9 @@ package org.oppia.android.app.help /** Enum class containing the items for the Recycler view of [HelpActivity]. */ enum class HelpItems { FAQ, - THIRD_PARTY; + THIRD_PARTY, + /** Corresponds to the Privacy Policy page. */ + PRIVACY_POLICY, + /** Corresponds to the Terms of Service page. */ + TERMS_OF_SERVICE } diff --git a/app/src/main/java/org/oppia/android/app/help/HelpListViewModel.kt b/app/src/main/java/org/oppia/android/app/help/HelpListViewModel.kt index 4612cee6e1b..3684a6b4e29 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpListViewModel.kt @@ -18,22 +18,24 @@ class HelpListViewModel @Inject constructor( private fun getRecyclerViewItemList(): ArrayList { for (item in HelpItems.values()) { - val category: String - val helpItemViewModel: HelpItemViewModel - when (item) { - HelpItems.FAQ -> { - category = resourceHandler.getStringInLocale(R.string.frequently_asked_questions_FAQ) - helpItemViewModel = - HelpItemViewModel(activity, category, isMultipane.get()!!, resourceHandler) - } - HelpItems.THIRD_PARTY -> { - category = - resourceHandler.getStringInLocale(R.string.third_party_dependency_list_activity_title) - helpItemViewModel = - HelpItemViewModel(activity, category, isMultipane.get()!!, resourceHandler) - } + val category = when (item) { + HelpItems.FAQ -> resourceHandler.getStringInLocale(R.string.frequently_asked_questions_FAQ) + HelpItems.THIRD_PARTY -> resourceHandler.getStringInLocale( + R.string.third_party_dependency_list_activity_title + ) + HelpItems.PRIVACY_POLICY -> resourceHandler.getStringInLocale( + R.string.privacy_policy_title + ) + HelpItems.TERMS_OF_SERVICE -> resourceHandler.getStringInLocale( + R.string.terms_of_service_title + ) } - arrayList.add(helpItemViewModel) + arrayList += HelpItemViewModel( + activity, + category, + isMultipane.get() ?: false, + resourceHandler + ) } return arrayList } diff --git a/app/src/main/java/org/oppia/android/app/help/LoadPoliciesFragmentListener.kt b/app/src/main/java/org/oppia/android/app/help/LoadPoliciesFragmentListener.kt new file mode 100644 index 00000000000..3062f60138d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/help/LoadPoliciesFragmentListener.kt @@ -0,0 +1,12 @@ +package org.oppia.android.app.help + +import org.oppia.android.app.model.PolicyPage + +/** + * Listener for when a selection should result in displaying a policy page (e.g. the Privacy Policy) + * on tablet. + */ +interface LoadPoliciesFragmentListener { + /** Called when the user wants to view an app policy. */ + fun loadPoliciesFragment(policyPage: PolicyPage) +} diff --git a/app/src/main/java/org/oppia/android/app/help/faq/FAQListActivity.kt b/app/src/main/java/org/oppia/android/app/help/faq/FAQListActivity.kt index f44b705a12a..fe19b6712d2 100644 --- a/app/src/main/java/org/oppia/android/app/help/faq/FAQListActivity.kt +++ b/app/src/main/java/org/oppia/android/app/help/faq/FAQListActivity.kt @@ -6,6 +6,8 @@ import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.help.faq.faqsingle.FAQSingleActivity +import org.oppia.android.app.model.ScreenName.FAQ_LIST_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** The FAQ page activity for placement of different FAQs. */ @@ -22,7 +24,9 @@ class FAQListActivity : InjectableAppCompatActivity(), RouteToFAQSingleListener companion object { fun createFAQListActivityIntent(context: Context): Intent { - return Intent(context, FAQListActivity::class.java) + return Intent(context, FAQListActivity::class.java).apply { + decorateWithScreenName(FAQ_LIST_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivity.kt b/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivity.kt index cd85b716416..ecf746cd132 100644 --- a/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivity.kt +++ b/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.FAQ_SINGLE_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** The FAQ page activity for placement of single FAQ. */ @@ -34,6 +36,7 @@ class FAQSingleActivity : InjectableAppCompatActivity() { val intent = Intent(context, FAQSingleActivity::class.java) intent.putExtra(FAQ_SINGLE_ACTIVITY_QUESTION, question) intent.putExtra(FAQ_SINGLE_ACTIVITY_ANSWER, answer) + intent.decorateWithScreenName(FAQ_SINGLE_ACTIVITY) return intent } } diff --git a/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivityPresenter.kt index 8663a9143b1..4802913476e 100644 --- a/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivityPresenter.kt @@ -51,7 +51,8 @@ class FAQSingleActivityPresenter @Inject constructor( resourceBucketName, entityType = "faq", entityId = "oppia", - imageCenterAlign = false + imageCenterAlign = false, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( answer, answerTextView diff --git a/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseListActivity.kt b/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseListActivity.kt index 282f929fdbb..4a42564705e 100644 --- a/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseListActivity.kt +++ b/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseListActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.LICENSE_LIST_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** The activity that will show list of licenses corresponding to a third-party dependency. */ @@ -30,6 +32,7 @@ class LicenseListActivity : InjectableAppCompatActivity(), RouteToLicenseTextLis ): Intent { val intent = Intent(context, LicenseListActivity::class.java) intent.putExtra(THIRD_PARTY_DEPENDENCY_INDEX, dependencyIndex) + intent.decorateWithScreenName(LICENSE_LIST_ACTIVITY) return intent } } diff --git a/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseTextViewerActivity.kt b/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseTextViewerActivity.kt index 79a1a932241..4a030fb85e3 100644 --- a/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseTextViewerActivity.kt +++ b/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseTextViewerActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.LICENSE_TEXT_VIEWER_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** The activity that will show the license text of a copyright license. */ @@ -36,6 +38,7 @@ class LicenseTextViewerActivity : InjectableAppCompatActivity() { val intent = Intent(context, LicenseTextViewerActivity::class.java) intent.putExtra(LICENSE_TEXT_VIEWER_ACTIVITY_DEP_INDEX, dependencyIndex) intent.putExtra(LICENSE_TEXT_VIEWER_ACTIVITY_LICENSE_INDEX, licenseIndex) + intent.decorateWithScreenName(LICENSE_TEXT_VIEWER_ACTIVITY) return intent } } diff --git a/app/src/main/java/org/oppia/android/app/help/thirdparty/ThirdPartyDependencyListActivity.kt b/app/src/main/java/org/oppia/android/app/help/thirdparty/ThirdPartyDependencyListActivity.kt index 6e876009c8f..1dec3d420a8 100644 --- a/app/src/main/java/org/oppia/android/app/help/thirdparty/ThirdPartyDependencyListActivity.kt +++ b/app/src/main/java/org/oppia/android/app/help/thirdparty/ThirdPartyDependencyListActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.THIRD_PARTY_DEPENDENCY_LIST_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** The activity for displaying a list of third-party dependencies used to build Oppia Android. */ @@ -25,7 +27,9 @@ class ThirdPartyDependencyListActivity : companion object { /** Returns [Intent] for starting [ThirdPartyDependencyListActivity]. */ fun createThirdPartyDependencyListActivityIntent(context: Context): Intent { - return Intent(context, ThirdPartyDependencyListActivity::class.java) + return Intent(context, ThirdPartyDependencyListActivity::class.java).apply { + decorateWithScreenName(THIRD_PARTY_DEPENDENCY_LIST_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/ExpandedHintListIndexListener.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/ExpandedHintListIndexListener.kt index bdac01524d3..a04d0e8b21b 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/ExpandedHintListIndexListener.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/ExpandedHintListIndexListener.kt @@ -7,7 +7,7 @@ package org.oppia.android.app.hintsandsolution interface ExpandedHintListIndexListener { /** Manage expanded list icon */ - fun onExpandListIconClicked(index: Int?) + fun onExpandListIconClicked(expandedItemsList: ArrayList) /** Manage reveal hint button visibility while orientation change */ fun onRevealHintClicked(index: Int?, isHintRevealed: Boolean?) diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt index af9f96121d1..0552263f5a4 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt @@ -16,7 +16,7 @@ import org.oppia.android.util.extensions.getStringFromBundle import org.oppia.android.util.extensions.putProto import javax.inject.Inject -private const val CURRENT_EXPANDED_LIST_INDEX_SAVED_KEY = +private const val CURRENT_EXPANDED_ITEMS_LIST_SAVED_KEY = "HintsAndSolutionDialogFragment.current_expanded_list_index" private const val HINT_INDEX_SAVED_KEY = "HintsAndSolutionDialogFragment.hint_index" private const val IS_HINT_REVEALED_SAVED_KEY = "HintsAndSolutionDialogFragment.is_hint_revealed" @@ -33,7 +33,7 @@ class HintsAndSolutionDialogFragment : @Inject lateinit var hintsAndSolutionDialogFragmentPresenter: HintsAndSolutionDialogFragmentPresenter - private var currentExpandedHintListIndex: Int? = null + private var expandedItemsList = ArrayList() private var index: Int? = null private var isHintRevealed: Boolean? = null @@ -90,13 +90,10 @@ class HintsAndSolutionDialogFragment : inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { if (savedInstanceState != null) { - currentExpandedHintListIndex = - savedInstanceState.getInt(CURRENT_EXPANDED_LIST_INDEX_SAVED_KEY, -1) - if (currentExpandedHintListIndex == -1) { - currentExpandedHintListIndex = null - } + expandedItemsList = + savedInstanceState.getIntegerArrayList(CURRENT_EXPANDED_ITEMS_LIST_SAVED_KEY) ?: ArrayList() index = savedInstanceState.getInt(HINT_INDEX_SAVED_KEY, -1) if (index == -1) index = null isHintRevealed = savedInstanceState.getBoolean(IS_HINT_REVEALED_SAVED_KEY, false) @@ -125,7 +122,7 @@ class HintsAndSolutionDialogFragment : helpIndex, writtenTranslationContext, id, - currentExpandedHintListIndex, + expandedItemsList, this as ExpandedHintListIndexListener, index, isHintRevealed, @@ -141,9 +138,7 @@ class HintsAndSolutionDialogFragment : override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - if (currentExpandedHintListIndex != null) { - outState.putInt(CURRENT_EXPANDED_LIST_INDEX_SAVED_KEY, currentExpandedHintListIndex!!) - } + outState.putIntegerArrayList(CURRENT_EXPANDED_ITEMS_LIST_SAVED_KEY, expandedItemsList) if (index != null) { outState.putInt(HINT_INDEX_SAVED_KEY, index!!) } @@ -158,9 +153,8 @@ class HintsAndSolutionDialogFragment : } } - override fun onExpandListIconClicked(index: Int?) { - currentExpandedHintListIndex = index - hintsAndSolutionDialogFragmentPresenter.onExpandClicked(index) + override fun onExpandListIconClicked(expandedItemsList: ArrayList) { + this.expandedItemsList = expandedItemsList } override fun revealSolution() { diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt index 50a77998d9e..32410bd2a0c 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt @@ -17,13 +17,12 @@ import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.HintsAndSolutionFragmentBinding -import org.oppia.android.databinding.HintsDividerBinding import org.oppia.android.databinding.HintsSummaryBinding +import org.oppia.android.databinding.ReturnToLessonButtonItemBinding import org.oppia.android.databinding.SolutionSummaryBinding import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType import org.oppia.android.util.parser.html.HtmlParser -import java.lang.IllegalStateException import javax.inject.Inject const val TAG_REVEAL_SOLUTION_DIALOG = "REVEAL_SOLUTION_DIALOG" @@ -39,8 +38,8 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( private val resourceHandler: AppLanguageResourceHandler ) { - private var currentExpandedHintListIndex: Int? = null private var index: Int? = null + private var expandedItemsList = ArrayList() private var isHintRevealed: Boolean? = null private var solutionIndex: Int? = null private var isSolutionRevealed: Boolean? = null @@ -67,16 +66,16 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( helpIndex: HelpIndex, writtenTranslationContext: WrittenTranslationContext, id: String?, - currentExpandedHintListIndex: Int?, + expandedItemsList: ArrayList?, expandedHintListIndexListener: ExpandedHintListIndexListener, index: Int?, isHintRevealed: Boolean?, solutionIndex: Int?, isSolutionRevealed: Boolean? - ): View? { + ): View { binding = HintsAndSolutionFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) - this.currentExpandedHintListIndex = currentExpandedHintListIndex + this.expandedItemsList = expandedItemsList ?: ArrayList() this.expandedHintListIndexListener = expandedHintListIndexListener this.index = index this.isHintRevealed = isHintRevealed @@ -97,13 +96,9 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( this.state = state this.helpIndex = helpIndex this.writtenTranslationContext = writtenTranslationContext - // The newAvailableHintIndex received here is coming from state player but in this - // implementation hints/solutions are shown on every even index and on every odd index we show a - // divider. The relative index therefore needs to be doubled to account for the divider. + val newAvailableHintIndex = computeNewAvailableHintIndex(helpIndex) - viewModel.newAvailableHintIndex.set( - newAvailableHintIndex * RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER - ) + viewModel.newAvailableHintIndex.set(newAvailableHintIndex) viewModel.allHintsExhausted.set(computeWhetherAllHintsAreExhausted(helpIndex)) viewModel.explorationId.set(id) @@ -163,7 +158,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( private enum class ViewType { VIEW_TYPE_HINT_ITEM, VIEW_TYPE_SOLUTION_ITEM, - VIEW_TYPE_HINTS_DIVIDER_ITEM + VIEW_TYPE_RETURN_TO_LESSON_ITEM } private fun createRecyclerViewAdapter(): BindableAdapter { @@ -172,7 +167,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( when (viewModel) { is HintsViewModel -> ViewType.VIEW_TYPE_HINT_ITEM is SolutionViewModel -> ViewType.VIEW_TYPE_SOLUTION_ITEM - is HintsDividerViewModel -> ViewType.VIEW_TYPE_HINTS_DIVIDER_ITEM + is ReturnToLessonViewModel -> ViewType.VIEW_TYPE_RETURN_TO_LESSON_ITEM else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } } @@ -188,16 +183,11 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( setViewModel = this::bindSolutionViewModel, transformViewModel = { it as SolutionViewModel } ) - .registerViewBinder( - viewType = ViewType.VIEW_TYPE_HINTS_DIVIDER_ITEM, - inflateView = { parent -> - HintsDividerBinding.inflate( - LayoutInflater.from(parent.context), - parent, - /* attachToParent= */ false - ).root - }, - bindView = { _, _ -> } + .registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_RETURN_TO_LESSON_ITEM, + inflateDataBinding = ReturnToLessonButtonItemBinding::inflate, + setViewModel = this::bindReturnToLessonViewModel, + transformViewModel = { it as ReturnToLessonViewModel } ) .build() } @@ -210,11 +200,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( val position: Int = itemList.indexOf(hintsViewModel) - var isHintListVisible = false - currentExpandedHintListIndex?.let { - isHintListVisible = it == position - } - binding.isListExpanded = isHintListVisible + binding.isListExpanded = expandedItemsList.contains(position) index?.let { index -> isHintRevealed?.let { isHintRevealed -> @@ -231,9 +217,11 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( resourceBucketName, entityType, hintsViewModel.explorationId.get()!!, - /* imageCenterAlign= */ true + /* imageCenterAlign= */ true, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( - hintsViewModel.hintsAndSolutionSummary.get()!!, binding.hintsAndSolutionSummary + hintsViewModel.hintsAndSolutionSummary.get()!!, + binding.hintsAndSolutionSummary ) if (hintsViewModel.hintCanBeRevealed.get()!!) { @@ -241,50 +229,28 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( binding.revealHintButton.setOnClickListener { hintsViewModel.isHintRevealed.set(true) expandedHintListIndexListener.onRevealHintClicked(position, /* isHintRevealed= */ true) - (fragment.requireActivity() as? RevealHintListener)?.revealHint( - hintIndex = position / RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER - ) - val previousIndex: Int? = currentExpandedHintListIndex - currentExpandedHintListIndex = - if (currentExpandedHintListIndex != null && currentExpandedHintListIndex == position) { - null - } else { - position - } - expandedHintListIndexListener.onExpandListIconClicked(currentExpandedHintListIndex) - if (previousIndex != null && previousIndex != currentExpandedHintListIndex) { - bindingAdapter.notifyItemChanged(previousIndex) - } + (fragment.requireActivity() as? RevealHintListener)?.revealHint(hintIndex = position) + expandOrCollapseItem(position) } } binding.root.setOnClickListener { if (hintsViewModel.isHintRevealed.get()!!) { - val previousIndex: Int? = currentExpandedHintListIndex - currentExpandedHintListIndex = - if (currentExpandedHintListIndex != null && currentExpandedHintListIndex == position) { - null - } else { - position - } - expandedHintListIndexListener.onExpandListIconClicked(currentExpandedHintListIndex) - if (previousIndex != null && - currentExpandedHintListIndex != null && - previousIndex == currentExpandedHintListIndex - ) { - bindingAdapter.notifyItemChanged(currentExpandedHintListIndex!!) - } else { - previousIndex?.let { - bindingAdapter.notifyItemChanged(it) - } - currentExpandedHintListIndex?.let { - bindingAdapter.notifyItemChanged(it) - } - } + expandOrCollapseItem(position) } } } + private fun expandOrCollapseItem(position: Int) { + if (expandedItemsList.contains(position)) { + expandedItemsList.remove(position) + } else { + expandedItemsList.add(position) + } + bindingAdapter.notifyItemChanged(position) + expandedHintListIndexListener.onExpandListIconClicked(expandedItemsList) + } + private fun bindSolutionViewModel( binding: SolutionSummaryBinding, solutionViewModel: SolutionViewModel @@ -292,12 +258,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( binding.viewModel = solutionViewModel val position: Int = itemList.indexOf(solutionViewModel) - - var isHintListVisible = false - currentExpandedHintListIndex?.let { currentExpandedHintListIndex -> - isHintListVisible = currentExpandedHintListIndex == position - } - binding.isListExpanded = isHintListVisible + binding.isListExpanded = expandedItemsList.contains(position) solutionIndex?.let { solutionIndex -> isSolutionRevealed?.let { isSolutionRevealed -> @@ -321,53 +282,44 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( binding.solutionCorrectAnswer.text = solutionViewModel.correctAnswer.get() } binding.solutionSummary.text = htmlParserFactory.create( - resourceBucketName, entityType, viewModel.explorationId.get()!!, /* imageCenterAlign= */ true + resourceBucketName, entityType, viewModel.explorationId.get()!!, /* imageCenterAlign= */ true, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( solutionViewModel.solutionSummary.get()!!, binding.solutionSummary ) if (solutionViewModel.solutionCanBeRevealed.get()!!) { binding.root.visibility = View.VISIBLE - binding.revealSolutionButton.setOnClickListener { + binding.showSolutionButton.setOnClickListener { showRevealSolutionDialogFragment() } } binding.root.setOnClickListener { if (solutionViewModel.isSolutionRevealed.get()!!) { - val previousIndex: Int? = currentExpandedHintListIndex - currentExpandedHintListIndex = - if (currentExpandedHintListIndex != null && currentExpandedHintListIndex == position) { - null - } else { - position - } - expandedHintListIndexListener.onExpandListIconClicked(currentExpandedHintListIndex) - if (previousIndex != null && - currentExpandedHintListIndex != null && - previousIndex == currentExpandedHintListIndex - ) { - bindingAdapter.notifyItemChanged(currentExpandedHintListIndex!!) - } else { - previousIndex?.let { - bindingAdapter.notifyItemChanged(it) - } - currentExpandedHintListIndex?.let { - bindingAdapter.notifyItemChanged(it) - } - } + expandOrCollapseItem(position) } } } + private fun bindReturnToLessonViewModel( + binding: ReturnToLessonButtonItemBinding, + returnToLessonViewModel: ReturnToLessonViewModel + ) { + binding.buttonViewModel = returnToLessonViewModel + + binding.returnToLessonButton.setOnClickListener { + (fragment.requireActivity() as? HintsAndSolutionListener)?.dismiss() + } + } + private fun handleAllHintsExhausted(allHintsExhausted: Boolean) { - if (itemList[itemList.size - RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER] is SolutionViewModel) { - val solutionViewModel = - itemList[itemList.size - RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER] as SolutionViewModel + // The last item of the list is ReturnToLessonViewModel and therefore second last item is + // SolutionViewModel as a result subtracting 2 from itemList size. + if (itemList[itemList.size - 2] is SolutionViewModel) { + val solutionViewModel = itemList[itemList.size - 2] as SolutionViewModel solutionViewModel.solutionCanBeRevealed.set(allHintsExhausted) - bindingAdapter.notifyItemChanged( - itemList.size - RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER - ) + bindingAdapter.notifyItemChanged(itemList.size - 2) } } @@ -386,28 +338,15 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( } fun handleRevealSolution() { - if (itemList[itemList.size - RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER] is SolutionViewModel) { - val solutionViewModel = - itemList[itemList.size - RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER] as SolutionViewModel + if (itemList[itemList.size - 2] is SolutionViewModel) { + val solutionViewModel = itemList[itemList.size - 2] as SolutionViewModel solutionViewModel.isSolutionRevealed.set(true) expandedHintListIndexListener.onRevealSolutionClicked( - /* solutionIndex= */ itemList.size - RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER, + /* solutionIndex= */ itemList.size - 2, /* isSolutionRevealed= */ true ) (fragment.requireActivity() as? RevealSolutionInterface)?.revealSolution() - val previousIndex: Int? = currentExpandedHintListIndex - currentExpandedHintListIndex = - if (currentExpandedHintListIndex != null && - currentExpandedHintListIndex == itemList.size - RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER - ) { - null - } else { - itemList.size - RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER - } - expandedHintListIndexListener.onExpandListIconClicked(currentExpandedHintListIndex) - if (previousIndex != null && previousIndex != currentExpandedHintListIndex) { - bindingAdapter.notifyItemChanged(previousIndex) - } + expandOrCollapseItem(itemList.size - 2) } } @@ -419,12 +358,6 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( } } - fun onExpandClicked(index: Int?) { - currentExpandedHintListIndex = index - if (index != null) - bindingAdapter.notifyItemChanged(index) - } - fun onRevealHintClicked(index: Int?, isHintRevealed: Boolean?) { this.index = index this.isHintRevealed = isHintRevealed diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsDividerViewModel.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsDividerViewModel.kt deleted file mode 100644 index 595b1c04add..00000000000 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsDividerViewModel.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.oppia.android.app.hintsandsolution - -class HintsDividerViewModel : HintsAndSolutionItemViewModel() diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsViewModel.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsViewModel.kt index 20de8ea0b51..414144091e2 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsViewModel.kt @@ -14,13 +14,6 @@ import org.oppia.android.domain.hintsandsolution.isSolutionRevealed import org.oppia.android.domain.translation.TranslationController import javax.inject.Inject -/** - * RecyclerView items are 2 times of (No. of Hints + Solution), - * this is because in UI after each hint or solution there is a horizontal line/view - * which is considered as a separate item in recyclerview. - */ -const val RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER = 2 - private const val DEFAULT_HINT_AND_SOLUTION_SUMMARY = "" /** [ViewModel] for Hints in [HintsAndSolutionDialogFragment]. */ @@ -63,35 +56,26 @@ class HintsViewModel @Inject constructor( for (index in hintList.indices) { if (itemList.isEmpty()) { addHintToList(index, hintList[index]) - } else if (itemList.size > 1) { - val isLastHintRevealed = - (itemList[itemList.size - RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER] as HintsViewModel) - .isHintRevealed.get() - ?: false + } else { + val isPriorHintRevealed = (itemList.last() as HintsViewModel).isHintRevealed.get() ?: false val availableHintIndex = newAvailableHintIndex.get() ?: 0 - if (isLastHintRevealed && - index <= availableHintIndex / RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER - ) { + if (isPriorHintRevealed && index <= availableHintIndex) { addHintToList(index, hintList[index]) - } else { - break - } + } else break } } - if (itemList.size > 1) { - val isLastHintRevealed = - (itemList[itemList.size - RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER] as HintsViewModel) - .isHintRevealed.get() - ?: false + if (itemList.isNotEmpty()) { + val isLastHintRevealed = (itemList.last() as HintsViewModel).isHintRevealed.get() ?: false val areAllHintsExhausted = allHintsExhausted.get() ?: false if (solution.hasExplanation() && - hintList.size * RECYCLERVIEW_INDEX_CORRECTION_MULTIPLIER == itemList.size && + hintList.size == itemList.size && isLastHintRevealed && areAllHintsExhausted ) { addSolutionToList(solution) } } + itemList.add(ReturnToLessonViewModel()) return itemList } @@ -110,7 +94,6 @@ class HintsViewModel @Inject constructor( hintsViewModel.hintsAndSolutionSummary.set(hintContentHtml) hintsViewModel.isHintRevealed.set(helpIndex.isHintRevealed(hintIndex, hintList)) itemList.add(hintsViewModel) - addDividerItem() } private fun addSolutionToList(solution: Solution) { @@ -126,10 +109,5 @@ class HintsViewModel @Inject constructor( solutionViewModel.solutionSummary.set(explanationHtml) solutionViewModel.isSolutionRevealed.set(helpIndex.isSolutionRevealed()) itemList.add(solutionViewModel) - addDividerItem() - } - - private fun addDividerItem() { - itemList.add(HintsDividerViewModel()) } } diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/ReturnToLessonViewModel.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/ReturnToLessonViewModel.kt new file mode 100644 index 00000000000..374bb6fd1ef --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/ReturnToLessonViewModel.kt @@ -0,0 +1,6 @@ +package org.oppia.android.app.hintsandsolution + +import androidx.lifecycle.ViewModel + +/** [ViewModel] for return to lesson button in [HintsAndSolutionDialogFragment]. */ +class ReturnToLessonViewModel : HintsAndSolutionItemViewModel() diff --git a/app/src/main/java/org/oppia/android/app/home/HomeActivity.kt b/app/src/main/java/org/oppia/android/app/home/HomeActivity.kt index b6b3c1281b4..7874aa44c8b 100644 --- a/app/src/main/java/org/oppia/android/app/home/HomeActivity.kt +++ b/app/src/main/java/org/oppia/android/app/home/HomeActivity.kt @@ -12,8 +12,10 @@ import org.oppia.android.app.drawer.TAG_SWITCH_PROFILE_DIALOG import org.oppia.android.app.home.recentlyplayed.RecentlyPlayedActivity import org.oppia.android.app.model.ExitProfileDialogArguments import org.oppia.android.app.model.HighlightItem +import org.oppia.android.app.model.ScreenName.HOME_ACTIVITY import org.oppia.android.app.topic.TopicActivity import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** The central activity for all users entering the app. */ @@ -32,9 +34,10 @@ class HomeActivity : companion object { fun createHomeActivity(context: Context, profileId: Int?): Intent { - val intent = Intent(context, HomeActivity::class.java) - intent.putExtra(NAVIGATION_PROFILE_ID_ARGUMENT_KEY, profileId) - return intent + return Intent(context, HomeActivity::class.java).apply { + putExtra(NAVIGATION_PROFILE_ID_ARGUMENT_KEY, profileId) + decorateWithScreenName(HOME_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt index 40df4d56fd5..acdd4a7d328 100644 --- a/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt @@ -26,6 +26,7 @@ import org.oppia.android.databinding.WelcomeBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.parser.html.StoryHtmlParserEntityType import org.oppia.android.util.parser.html.TopicHtmlParserEntityType import javax.inject.Inject @@ -41,7 +42,8 @@ class HomeFragmentPresenter @Inject constructor( @TopicHtmlParserEntityType private val topicEntityType: String, @StoryHtmlParserEntityType private val storyEntityType: String, private val resourceHandler: AppLanguageResourceHandler, - private val dateTimeUtil: DateTimeUtil + private val dateTimeUtil: DateTimeUtil, + private val translationController: TranslationController ) { private val routeToTopicListener = activity as RouteToTopicListener private lateinit var binding: HomeFragmentBinding @@ -65,7 +67,8 @@ class HomeFragmentPresenter @Inject constructor( topicEntityType, storyEntityType, resourceHandler, - dateTimeUtil + dateTimeUtil, + translationController ) val homeAdapter = createRecyclerViewAdapter() diff --git a/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt b/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt index 16e3011628b..356bbd65f33 100644 --- a/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt @@ -25,6 +25,7 @@ import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.combineWith @@ -48,7 +49,8 @@ class HomeViewModel( @TopicHtmlParserEntityType private val topicEntityType: String, @StoryHtmlParserEntityType private val storyEntityType: String, private val resourceHandler: AppLanguageResourceHandler, - private val dateTimeUtil: DateTimeUtil + private val dateTimeUtil: DateTimeUtil, + private val translationController: TranslationController ) : ObservableViewModel() { private val profileId: ProfileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() @@ -65,7 +67,7 @@ class HomeViewModel( } private val topicListSummaryDataProvider: DataProvider by lazy { - topicListController.getTopicList() + topicListController.getTopicList(profileId) } private val homeItemViewModelListDataProvider: DataProvider> by lazy { @@ -76,10 +78,14 @@ class HomeViewModel( promotedActivityListSummaryDataProvider, PROFILE_AND_PROMOTED_ACTIVITY_COMBINED_PROVIDER_ID ) { profile, promotedActivityList -> - listOfNotNull( - computeWelcomeViewModel(profile), - computePromotedActivityListViewModel(promotedActivityList) - ) + if (profile.numberOfLogins > 1) { + listOfNotNull( + computeWelcomeViewModel(profile), + computePromotedActivityListViewModel(promotedActivityList) + ) + } else { + listOfNotNull(computeWelcomeViewModel(profile)) + } }.combineWith( topicListSummaryDataProvider, HOME_FRAGMENT_COMBINED_PROVIDER_ID @@ -194,7 +200,8 @@ class HomeViewModel( internalProfileId, sortedStoryList.size, storyEntityType, - promotedStory + promotedStory, + translationController ) } } @@ -213,7 +220,8 @@ class HomeViewModel( activity, topicSummary, topicEntityType, - comingSoonTopicList + comingSoonTopicList, + translationController ) } } @@ -227,14 +235,15 @@ class HomeViewModel( private fun computeAllTopicsItemsViewModelList( topicList: TopicList ): List { - val allTopicsList = topicList.topicSummaryList.mapIndexed { topicIndex, topicSummary -> + val allTopicsList = topicList.topicSummaryList.mapIndexed { topicIndex, ephemeralSummary -> TopicSummaryViewModel( activity, - topicSummary, + ephemeralSummary, topicEntityType, fragment as TopicSummaryClickListener, position = topicIndex, - resourceHandler + resourceHandler, + translationController ) } return if (allTopicsList.isNotEmpty()) { diff --git a/app/src/main/java/org/oppia/android/app/home/RouteToExplorationListener.kt b/app/src/main/java/org/oppia/android/app/home/RouteToExplorationListener.kt index 80614517973..3c8081a3152 100755 --- a/app/src/main/java/org/oppia/android/app/home/RouteToExplorationListener.kt +++ b/app/src/main/java/org/oppia/android/app/home/RouteToExplorationListener.kt @@ -1,13 +1,16 @@ package org.oppia.android.app.home +import org.oppia.android.app.model.ExplorationActivityParams +import org.oppia.android.app.model.ProfileId + /** Listener for when an activity should route to a exploration. */ interface RouteToExplorationListener { fun routeToExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, isCheckpointingEnabled: Boolean ) } diff --git a/app/src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicsViewModel.kt b/app/src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicsViewModel.kt index 14b32fd071a..e6f3fbf6a52 100644 --- a/app/src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicsViewModel.kt @@ -6,15 +6,19 @@ import org.oppia.android.R import org.oppia.android.app.home.HomeItemViewModel import org.oppia.android.app.model.ComingSoonTopicList import org.oppia.android.app.model.UpcomingTopic +import org.oppia.android.domain.translation.TranslationController /** [ViewModel] for displaying a coming soon topic summaries. */ class ComingSoonTopicsViewModel( private val activity: AppCompatActivity, val topicSummary: UpcomingTopic, val entityType: String, - val comingSoonTopicList: ComingSoonTopicList + val comingSoonTopicList: ComingSoonTopicList, + translationController: TranslationController ) : HomeItemViewModel() { - val name: String = topicSummary.name + val topicTitle: String by lazy { + translationController.extractString(topicSummary.title, topicSummary.writtenTranslationContext) + } /** * Returns the padding placed at the start of the coming soon topics list. diff --git a/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModel.kt b/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModel.kt index bd586a8cf28..38a8899204c 100755 --- a/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModel.kt @@ -9,6 +9,7 @@ import org.oppia.android.R import org.oppia.android.app.home.RouteToTopicPlayStoryListener import org.oppia.android.app.model.PromotedStory import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.domain.translation.TranslationController import java.util.Objects // TODO(#283): Add download status information to promoted-story-card. @@ -19,8 +20,25 @@ class PromotedStoryViewModel( private val internalProfileId: Int, private val totalStoryCount: Int, val entityType: String, - val promotedStory: PromotedStory + val promotedStory: PromotedStory, + translationController: TranslationController ) : ObservableViewModel() { + val storyTitle by lazy { + translationController.extractString( + promotedStory.storyTitle, promotedStory.storyWrittenTranslationContext + ) + } + val topicTitle by lazy { + translationController.extractString( + promotedStory.topicTitle, promotedStory.topicWrittenTranslationContext + ) + } + val nextChapterTitle by lazy { + translationController.extractString( + promotedStory.nextChapterTitle, promotedStory.nextChapterWrittenTranslationContext + ) + } + private val routeToTopicPlayStoryListener = activity as RouteToTopicPlayStoryListener /** diff --git a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/OngoingStoryViewModel.kt b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/OngoingStoryViewModel.kt index d45374d15fd..6f6b82ea571 100755 --- a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/OngoingStoryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/OngoingStoryViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import org.oppia.android.R import org.oppia.android.app.model.PromotedStory import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.domain.translation.TranslationController // TODO(#297): Add download status information to promoted-story-card. @@ -16,8 +17,25 @@ class OngoingStoryViewModel( val entityType: String, private val ongoingStoryClickListener: OngoingStoryClickListener, private val position: Int, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + translationController: TranslationController ) : RecentlyPlayedItemViewModel() { + val storyTitle by lazy { + translationController.extractString( + ongoingStory.storyTitle, ongoingStory.storyWrittenTranslationContext + ) + } + val topicTitle by lazy { + translationController.extractString( + ongoingStory.topicTitle, ongoingStory.topicWrittenTranslationContext + ) + } + val nextChapterTitle by lazy { + translationController.extractString( + ongoingStory.nextChapterTitle, ongoingStory.nextChapterWrittenTranslationContext + ) + } + fun clickOnOngoingStoryTile(@Suppress("UNUSED_PARAMETER") v: View) { ongoingStoryClickListener.onOngoingStoryClicked(ongoingStory) } @@ -102,7 +120,7 @@ class OngoingStoryViewModel( fun computeLessonThumbnailContentDescription(): String { return resourceHandler.getStringInLocaleWithWrapping( - R.string.lesson_thumbnail_content_description, ongoingStory.nextChapterName + R.string.lesson_thumbnail_content_description, nextChapterTitle ) } } diff --git a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedActivity.kt b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedActivity.kt index d3e139f5d61..8923766a520 100644 --- a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedActivity.kt +++ b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedActivity.kt @@ -8,11 +8,14 @@ import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.ActivityIntentFactories import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.home.RouteToExplorationListener +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.ScreenName.RECENTLY_PLAYED_ACTIVITY import org.oppia.android.app.player.exploration.ExplorationActivity import org.oppia.android.app.resumelesson.ResumeLessonActivity import org.oppia.android.app.topic.RouteToResumeLessonListener +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for recent stories. */ @@ -43,47 +46,48 @@ class RecentlyPlayedActivity : fun createRecentlyPlayedActivityIntent(context: Context, internalProfileId: Int): Intent { return Intent(context, RecentlyPlayedActivity::class.java).apply { putExtra(RECENTLY_PLAYED_ACTIVITY_INTERNAL_PROFILE_ID_KEY, internalProfileId) + decorateWithScreenName(RECENTLY_PLAYED_ACTIVITY) } } } override fun routeToExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, isCheckpointingEnabled: Boolean ) { startActivity( ExplorationActivity.createExplorationActivityIntent( this, - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, isCheckpointingEnabled ) ) } override fun routeToResumeLesson( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, explorationCheckpoint: ExplorationCheckpoint ) { startActivity( ResumeLessonActivity.createResumeLessonActivityIntent( this, - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, explorationCheckpoint ) ) diff --git a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt index 1cabcfac654..9c00b790290 100755 --- a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt @@ -14,6 +14,7 @@ import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.home.RouteToExplorationListener import org.oppia.android.app.model.ChapterPlayState +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.PromotedActivityList @@ -25,6 +26,7 @@ import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.parser.html.StoryHtmlParserEntityType @@ -40,7 +42,8 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( private val topicListController: TopicListController, private val explorationCheckpointController: ExplorationCheckpointController, @StoryHtmlParserEntityType private val entityType: String, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) { private val routeToResumeLessonListener = activity as RouteToResumeLessonListener @@ -123,12 +126,12 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( ) itemList.add(recentSectionTitleViewModel) recentlyPlayedStoryList.forEachIndexed { index, promotedStory -> - val ongoingStoryViewModel = getOngoingStoryViewModel(promotedStory, index) + val ongoingStoryViewModel = createOngoingStoryViewModel(promotedStory, index) itemList.add(ongoingStoryViewModel) } } - private fun getOngoingStoryViewModel( + private fun createOngoingStoryViewModel( promotedStory: PromotedStory, index: Int ): RecentlyPlayedItemViewModel { @@ -138,7 +141,8 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( entityType, fragment as OngoingStoryClickListener, index, - resourceHandler + resourceHandler, + translationController ) } @@ -151,7 +155,7 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( ) itemList.add(olderSectionTitleViewModel) olderPlayedStoryList.forEachIndexed { index, promotedStory -> - val ongoingStoryViewModel = getOngoingStoryViewModel(promotedStory, index) + val ongoingStoryViewModel = createOngoingStoryViewModel(promotedStory, index) itemList.add(ongoingStoryViewModel) } } @@ -165,7 +169,7 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( ) itemList.add(recommendedSectionTitleViewModel) suggestedStoryList.forEachIndexed { index, suggestedStory -> - val ongoingStoryViewModel = getOngoingStoryViewModel(suggestedStory, index) + val ongoingStoryViewModel = createOngoingStoryViewModel(suggestedStory, index) itemList.add(ongoingStoryViewModel) } } @@ -234,6 +238,9 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( } fun onOngoingStoryClicked(promotedStory: PromotedStory) { + val profileId = ProfileId.newBuilder().apply { + internalId = internalProfileId + }.build() val canHavePartialProgressSaved = when (promotedStory.chapterPlayState) { ChapterPlayState.IN_PROGRESS_SAVED, ChapterPlayState.IN_PROGRESS_NOT_SAVED, @@ -245,10 +252,7 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( if (promotedStory.chapterPlayState == ChapterPlayState.IN_PROGRESS_SAVED) { val explorationCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - ProfileId.newBuilder().apply { - internalId = internalProfileId - }.build(), - promotedStory.explorationId + profileId, promotedStory.explorationId ).toLiveData() explorationCheckpointLiveData.observe( @@ -258,11 +262,11 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) routeToResumeLessonListener.routeToResumeLesson( - internalProfileId, + profileId, promotedStory.topicId, promotedStory.storyId, promotedStory.explorationId, - backflowScreen = null, + parentScreen = ExplorationActivityParams.ParentScreen.PARENT_SCREEN_UNSPECIFIED, explorationCheckpoint = it.value ) } else if (it is AsyncResult.Failure) { @@ -317,11 +321,11 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( is AsyncResult.Success -> { oppiaLogger.d("RecentlyPlayedFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( - internalProfileId, + ProfileId.newBuilder().apply { internalId = internalProfileId }.build(), topicId, storyId, explorationId, - backflowScreen = null, + parentScreen = ExplorationActivityParams.ParentScreen.PARENT_SCREEN_UNSPECIFIED, isCheckpointingEnabled = canHavePartialProgressSaved ) activity.finish() diff --git a/app/src/main/java/org/oppia/android/app/home/topiclist/TopicSummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/home/topiclist/TopicSummaryViewModel.kt index 39c8678849a..36807c9d4cf 100755 --- a/app/src/main/java/org/oppia/android/app/home/topiclist/TopicSummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/home/topiclist/TopicSummaryViewModel.kt @@ -3,20 +3,28 @@ package org.oppia.android.app.home.topiclist import androidx.appcompat.app.AppCompatActivity import org.oppia.android.R import org.oppia.android.app.home.HomeItemViewModel -import org.oppia.android.app.model.TopicSummary +import org.oppia.android.app.model.EphemeralTopicSummary import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.domain.translation.TranslationController import java.util.Objects /** The view model corresponding to individual topic summaries in the topic summary RecyclerView. */ class TopicSummaryViewModel( private val activity: AppCompatActivity, - val topicSummary: TopicSummary, + ephemeralTopicSummary: EphemeralTopicSummary, val entityType: String, private val topicSummaryClickListener: TopicSummaryClickListener, private val position: Int, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + translationController: TranslationController ) : HomeItemViewModel() { - val name: String = topicSummary.name + val topicSummary = ephemeralTopicSummary.topicSummary + + val title: String by lazy { + translationController.extractString( + topicSummary.title, ephemeralTopicSummary.writtenTranslationContext + ) + } private val outerMargin by lazy { activity.resources.getDimensionPixelSize(R.dimen.home_outer_margin) diff --git a/app/src/main/java/org/oppia/android/app/mydownloads/MyDownloadsActivity.kt b/app/src/main/java/org/oppia/android/app/mydownloads/MyDownloadsActivity.kt index e9e33c150e3..0c78c38c83b 100644 --- a/app/src/main/java/org/oppia/android/app/mydownloads/MyDownloadsActivity.kt +++ b/app/src/main/java/org/oppia/android/app/mydownloads/MyDownloadsActivity.kt @@ -7,6 +7,8 @@ import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.drawer.NAVIGATION_PROFILE_ID_ARGUMENT_KEY import org.oppia.android.app.home.HomeActivity +import org.oppia.android.app.model.ScreenName.MY_DOWNLOADS_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** The activity for displaying [MyDownloadsFragment]. */ @@ -26,6 +28,7 @@ class MyDownloadsActivity : InjectableAppCompatActivity() { fun createMyDownloadsActivityIntent(context: Context, profileId: Int?): Intent { val intent = Intent(context, MyDownloadsActivity::class.java) intent.putExtra(NAVIGATION_PROFILE_ID_ARGUMENT_KEY, profileId) + intent.decorateWithScreenName(MY_DOWNLOADS_ACTIVITY) return intent } diff --git a/app/src/main/java/org/oppia/android/app/deprecation/AutomaticAppDeprecationNoticeDialogFragment.kt b/app/src/main/java/org/oppia/android/app/notice/AutomaticAppDeprecationNoticeDialogFragment.kt similarity index 97% rename from app/src/main/java/org/oppia/android/app/deprecation/AutomaticAppDeprecationNoticeDialogFragment.kt rename to app/src/main/java/org/oppia/android/app/notice/AutomaticAppDeprecationNoticeDialogFragment.kt index 5e9d58c7b0a..f4f8beed7fe 100644 --- a/app/src/main/java/org/oppia/android/app/deprecation/AutomaticAppDeprecationNoticeDialogFragment.kt +++ b/app/src/main/java/org/oppia/android/app/notice/AutomaticAppDeprecationNoticeDialogFragment.kt @@ -1,4 +1,4 @@ -package org.oppia.android.app.deprecation +package org.oppia.android.app.notice import android.app.Dialog import android.content.Context diff --git a/app/src/main/java/org/oppia/android/app/deprecation/AutomaticAppDeprecationNoticeDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/notice/AutomaticAppDeprecationNoticeDialogFragmentPresenter.kt similarity index 96% rename from app/src/main/java/org/oppia/android/app/deprecation/AutomaticAppDeprecationNoticeDialogFragmentPresenter.kt rename to app/src/main/java/org/oppia/android/app/notice/AutomaticAppDeprecationNoticeDialogFragmentPresenter.kt index 58a40354dd6..b587df03185 100644 --- a/app/src/main/java/org/oppia/android/app/deprecation/AutomaticAppDeprecationNoticeDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/notice/AutomaticAppDeprecationNoticeDialogFragmentPresenter.kt @@ -1,4 +1,4 @@ -package org.oppia.android.app.deprecation +package org.oppia.android.app.notice import android.app.Dialog import androidx.appcompat.app.AlertDialog diff --git a/app/src/main/java/org/oppia/android/app/notice/BetaNoticeClosedListener.kt b/app/src/main/java/org/oppia/android/app/notice/BetaNoticeClosedListener.kt new file mode 100644 index 00000000000..3eee34a7147 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/BetaNoticeClosedListener.kt @@ -0,0 +1,11 @@ +package org.oppia.android.app.notice + +/** Listener for when the beta notice dialog is closed. */ +interface BetaNoticeClosedListener { + /** + * Called when the notice dialog was closed. + * + * @param permanentlyDismiss whether the user never wants to see this notice again + */ + fun onBetaNoticeOkayButtonClicked(permanentlyDismiss: Boolean) +} diff --git a/app/src/main/java/org/oppia/android/app/notice/BetaNoticeDialogFragment.kt b/app/src/main/java/org/oppia/android/app/notice/BetaNoticeDialogFragment.kt new file mode 100644 index 00000000000..5456f0e95ad --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/BetaNoticeDialogFragment.kt @@ -0,0 +1,30 @@ +package org.oppia.android.app.notice + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableDialogFragment +import javax.inject.Inject + +/** + * Dialog fragment to be shown when the user may be unaware that they're using a beta pre-release + * version of the app. + */ +class BetaNoticeDialogFragment : InjectableDialogFragment() { + companion object { + /** Returns a new instance of [BetaNoticeDialogFragment]. */ + fun newInstance(): BetaNoticeDialogFragment = BetaNoticeDialogFragment() + } + + @Inject lateinit var presenter: BetaNoticeDialogFragmentPresenter + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return presenter.handleOnCreateDialog() + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/BetaNoticeDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/notice/BetaNoticeDialogFragmentPresenter.kt new file mode 100644 index 00000000000..4d5547368bb --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/BetaNoticeDialogFragmentPresenter.kt @@ -0,0 +1,32 @@ +package org.oppia.android.app.notice + +import android.app.Dialog +import android.view.View +import android.widget.CheckBox +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import org.oppia.android.R +import javax.inject.Inject + +/** Presenter for the dialog that shows when the beta version app is being used. */ +class BetaNoticeDialogFragmentPresenter @Inject constructor( + private val activity: AppCompatActivity +) { + private val betaNoticeClosedListener by lazy { activity as BetaNoticeClosedListener } + + /** Handles dialog creation for the beta notice. */ + fun handleOnCreateDialog(): Dialog { + val contentView = View.inflate(activity, R.layout.beta_notice_dialog_content, /* root= */ null) + val preferenceCheckbox = + contentView.findViewById(R.id.beta_notice_dialog_preference_checkbox) + return AlertDialog.Builder(activity) + .setTitle(R.string.beta_notice_dialog_title) + .setView(contentView) + .setPositiveButton(R.string.beta_notice_dialog_close_button_text) { _, _ -> + betaNoticeClosedListener.onBetaNoticeOkayButtonClicked(preferenceCheckbox.isChecked) + } + .setCancelable(false) + .create() + .also { it.setCanceledOnTouchOutside(false) } + } +} diff --git a/app/src/main/java/org/oppia/android/app/deprecation/DeprecationNoticeExitAppListener.kt b/app/src/main/java/org/oppia/android/app/notice/DeprecationNoticeExitAppListener.kt similarity index 82% rename from app/src/main/java/org/oppia/android/app/deprecation/DeprecationNoticeExitAppListener.kt rename to app/src/main/java/org/oppia/android/app/notice/DeprecationNoticeExitAppListener.kt index 020ac27ef8e..ff3d7dd1671 100644 --- a/app/src/main/java/org/oppia/android/app/deprecation/DeprecationNoticeExitAppListener.kt +++ b/app/src/main/java/org/oppia/android/app/notice/DeprecationNoticeExitAppListener.kt @@ -1,4 +1,4 @@ -package org.oppia.android.app.deprecation +package org.oppia.android.app.notice /** Listener for when the app deprecation dialog is closed. */ interface DeprecationNoticeExitAppListener { diff --git a/app/src/main/java/org/oppia/android/app/notice/GeneralAvailabilityUpgradeNoticeClosedListener.kt b/app/src/main/java/org/oppia/android/app/notice/GeneralAvailabilityUpgradeNoticeClosedListener.kt new file mode 100644 index 00000000000..715b7d06e6a --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/GeneralAvailabilityUpgradeNoticeClosedListener.kt @@ -0,0 +1,11 @@ +package org.oppia.android.app.notice + +/** Listener for when the general availability update dialog is closed. */ +interface GeneralAvailabilityUpgradeNoticeClosedListener { + /** + * Called when the notice dialog was closed. + * + * @param permanentlyDismiss whether the user never wants to see this notice again + */ + fun onGaUpgradeNoticeOkayButtonClicked(permanentlyDismiss: Boolean) +} diff --git a/app/src/main/java/org/oppia/android/app/notice/GeneralAvailabilityUpgradeNoticeDialogFragment.kt b/app/src/main/java/org/oppia/android/app/notice/GeneralAvailabilityUpgradeNoticeDialogFragment.kt new file mode 100644 index 00000000000..30ca45e7d1c --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/GeneralAvailabilityUpgradeNoticeDialogFragment.kt @@ -0,0 +1,31 @@ +package org.oppia.android.app.notice + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableDialogFragment +import javax.inject.Inject + +/** + * Dialog fragment to be shown when the user may be unaware that they've updated from a pre-release + * version of the app to general availability. + */ +class GeneralAvailabilityUpgradeNoticeDialogFragment : InjectableDialogFragment() { + companion object { + /** Returns a new instance of [GeneralAvailabilityUpgradeNoticeDialogFragment]. */ + fun newInstance(): GeneralAvailabilityUpgradeNoticeDialogFragment = + GeneralAvailabilityUpgradeNoticeDialogFragment() + } + + @Inject lateinit var presenter: GeneralAvailabilityUpgradeNoticeDialogFragmentPresenter + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return presenter.handleOnCreateDialog() + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/GeneralAvailabilityUpgradeNoticeDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/notice/GeneralAvailabilityUpgradeNoticeDialogFragmentPresenter.kt new file mode 100644 index 00000000000..74cdca94c9d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/GeneralAvailabilityUpgradeNoticeDialogFragmentPresenter.kt @@ -0,0 +1,42 @@ +package org.oppia.android.app.notice + +import android.app.Dialog +import android.view.View +import android.widget.CheckBox +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import org.oppia.android.R +import javax.inject.Inject + +/** + * Presenter for the dialog that shows when the user has updated to the general availability version + * app is being used. + */ +class GeneralAvailabilityUpgradeNoticeDialogFragmentPresenter @Inject constructor( + private val activity: AppCompatActivity +) { + private val gaUpgradeNoticeClosedListener by lazy { + activity as GeneralAvailabilityUpgradeNoticeClosedListener + } + + /** Handles dialog creation for the general availability update notice. */ + fun handleOnCreateDialog(): Dialog { + val contentView = + View.inflate( + activity, R.layout.general_availability_upgrade_notice_dialog_content, /* root= */ null + ) + val preferenceCheckbox = + contentView.findViewById(R.id.ga_update_notice_dialog_preference_checkbox) + return AlertDialog.Builder(activity) + .setTitle(R.string.general_availability_notice_dialog_title) + .setView(contentView) + .setPositiveButton(R.string.general_availability_notice_dialog_close_button_text) { _, _ -> + gaUpgradeNoticeClosedListener.onGaUpgradeNoticeOkayButtonClicked( + preferenceCheckbox.isChecked + ) + } + .setCancelable(false) + .create() + .also { it.setCanceledOnTouchOutside(false) } + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/testing/BUILD.bazel b/app/src/main/java/org/oppia/android/app/notice/testing/BUILD.bazel new file mode 100644 index 00000000000..92f1a0ae62b --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/testing/BUILD.bazel @@ -0,0 +1,34 @@ +""" +Test-only utilities corresponding to app notices. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "beta_notice_dialog_frgment_test_activity", + testonly = True, + srcs = [ + "BetaNoticeDialogFragmentTestActivity.kt", + ], + visibility = ["//app:app_testing_visibility"], + deps = [ + "//app", + "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", + ], +) + +kt_android_library( + name = "general_availability_upgrade_notice_dialog_fragment_test_activity", + testonly = True, + srcs = [ + "GeneralAvailabilityUpgradeNoticeDialogFragmentTestActivity.kt", + ], + visibility = ["//app:app_testing_visibility"], + deps = [ + "//app", + "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/notice/testing/BetaNoticeDialogFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/notice/testing/BetaNoticeDialogFragmentTestActivity.kt new file mode 100644 index 00000000000..7d0e612bb26 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/testing/BetaNoticeDialogFragmentTestActivity.kt @@ -0,0 +1,26 @@ +package org.oppia.android.app.notice.testing + +import android.os.Bundle +import org.oppia.android.app.notice.BetaNoticeClosedListener +import org.oppia.android.app.notice.BetaNoticeDialogFragment +import org.oppia.android.app.testing.activity.TestActivity + +/** [TestActivity] for setting up a test environment for testing the beta notice dialog. */ +class BetaNoticeDialogFragmentTestActivity : TestActivity(), BetaNoticeClosedListener { + /** + * [BetaNoticeClosedListener] that must be initialized by the test, and is presumed to be a + * Mockito mock (though this is not, strictly speaking, required). + * + * This listener will be used as the callback for the dialog in response to UI operations. + */ + lateinit var mockCallbackListener: BetaNoticeClosedListener + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + BetaNoticeDialogFragment.newInstance().showNow(supportFragmentManager, "beta_notice_dialog") + } + + override fun onBetaNoticeOkayButtonClicked(permanentlyDismiss: Boolean) { + mockCallbackListener.onBetaNoticeOkayButtonClicked(permanentlyDismiss) + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/testing/GeneralAvailabilityUpgradeNoticeDialogFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/notice/testing/GeneralAvailabilityUpgradeNoticeDialogFragmentTestActivity.kt new file mode 100644 index 00000000000..b378a876e41 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/testing/GeneralAvailabilityUpgradeNoticeDialogFragmentTestActivity.kt @@ -0,0 +1,28 @@ +package org.oppia.android.app.notice.testing + +import android.os.Bundle +import org.oppia.android.app.notice.GeneralAvailabilityUpgradeNoticeClosedListener +import org.oppia.android.app.notice.GeneralAvailabilityUpgradeNoticeDialogFragment +import org.oppia.android.app.testing.activity.TestActivity + +/** [TestActivity] for setting up a test environment for testing the GA upgrade notice dialog. */ +class GeneralAvailabilityUpgradeNoticeDialogFragmentTestActivity : + TestActivity(), GeneralAvailabilityUpgradeNoticeClosedListener { + /** + * [GeneralAvailabilityUpgradeNoticeClosedListener] that must be initialized by the test, and is + * presumed to be a Mockito mock (though this is not, strictly speaking, required). + * + * This listener will be used as the callback for the dialog in response to UI operations. + */ + lateinit var mockCallbackListener: GeneralAvailabilityUpgradeNoticeClosedListener + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + GeneralAvailabilityUpgradeNoticeDialogFragment.newInstance() + .showNow(supportFragmentManager, "ga_upgrade_notice_dialog") + } + + override fun onGaUpgradeNoticeOkayButtonClicked(permanentlyDismiss: Boolean) { + mockCallbackListener.onGaUpgradeNoticeOkayButtonClicked(permanentlyDismiss) + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingActivity.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingActivity.kt index 9f17483389b..333af9573b0 100644 --- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingActivity.kt @@ -5,18 +5,27 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.model.ScreenName.ONBOARDING_ACTIVITY +import org.oppia.android.app.policies.PoliciesActivity +import org.oppia.android.app.policies.RouteToPoliciesListener import org.oppia.android.app.profile.ProfileChooserActivity +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity that contains the onboarding flow for learners. */ -class OnboardingActivity : InjectableAppCompatActivity(), RouteToProfileListListener { +class OnboardingActivity : + InjectableAppCompatActivity(), + RouteToProfileListListener, + RouteToPoliciesListener { @Inject lateinit var onboardingActivityPresenter: OnboardingActivityPresenter companion object { fun createOnboardingActivity(context: Context): Intent { - val intent = Intent(context, OnboardingActivity::class.java) - return intent + return Intent(context, OnboardingActivity::class.java).apply { + decorateWithScreenName(ONBOARDING_ACTIVITY) + } } } @@ -30,4 +39,8 @@ class OnboardingActivity : InjectableAppCompatActivity(), RouteToProfileListList startActivity(ProfileChooserActivity.createProfileChooserActivity(this)) finish() } + + override fun onRouteToPolicies(policyPage: PolicyPage) { + startActivity(PoliciesActivity.createPoliciesActivityIntent(this, policyPage)) + } } diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt index f359a4348c7..04b07ccedc8 100644 --- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt @@ -10,12 +10,16 @@ import androidx.fragment.app.Fragment import androidx.viewpager2.widget.ViewPager2 import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.policies.RouteToPoliciesListener import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.OnboardingFragmentBinding import org.oppia.android.databinding.OnboardingSlideBinding import org.oppia.android.databinding.OnboardingSlideFinalBinding +import org.oppia.android.util.parser.html.HtmlParser +import org.oppia.android.util.parser.html.PolicyType import org.oppia.android.util.statusbar.StatusBarColor import javax.inject.Inject @@ -26,12 +30,13 @@ class OnboardingFragmentPresenter @Inject constructor( private val fragment: Fragment, private val viewModelProvider: ViewModelProvider, private val viewModelProviderFinalSlide: ViewModelProvider, - private val resourceHandler: AppLanguageResourceHandler -) : OnboardingNavigationListener { + private val resourceHandler: AppLanguageResourceHandler, + private val htmlParserFactory: HtmlParser.Factory +) : OnboardingNavigationListener, HtmlParser.PolicyOppiaTagActionListener { private val dotsList = ArrayList() private lateinit var binding: OnboardingFragmentBinding - fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { + fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View { binding = OnboardingFragmentBinding.inflate( inflater, container, @@ -112,12 +117,43 @@ class OnboardingFragmentPresenter @Inject constructor( .registerViewDataBinder( viewType = ViewType.ONBOARDING_FINAL_SLIDE, inflateDataBinding = OnboardingSlideFinalBinding::inflate, - setViewModel = OnboardingSlideFinalBinding::setViewModel, + setViewModel = this::bindOnboardingSlideFinal, transformViewModel = { it as OnboardingSlideFinalViewModel } ) .build() } + private fun bindOnboardingSlideFinal( + binding: OnboardingSlideFinalBinding, + model: OnboardingSlideFinalViewModel + ) { + binding.viewModel = model + + val completeString: String = + resourceHandler.getStringInLocaleWithWrapping( + R.string.agree_to_terms, + resourceHandler.getStringInLocale(R.string.app_name) + ) + binding.slideTermsOfServiceAndPrivacyPolicyLinksTextView.text = htmlParserFactory.create( + policyOppiaTagActionListener = this, + displayLocale = resourceHandler.getDisplayLocale() + ).parseOppiaHtml( + completeString, + binding.slideTermsOfServiceAndPrivacyPolicyLinksTextView, + supportsLinks = true, + supportsConceptCards = false + ) + } + + override fun onPolicyPageLinkClicked(policyType: PolicyType) { + when (policyType) { + PolicyType.PRIVACY_POLICY -> + (activity as RouteToPoliciesListener).onRouteToPolicies(PolicyPage.PRIVACY_POLICY) + PolicyType.TERMS_OF_SERVICE -> + (activity as RouteToPoliciesListener).onRouteToPolicies(PolicyPage.TERMS_OF_SERVICE) + } + } + private fun getOnboardingSlideFinalViewModel(): OnboardingSlideFinalViewModel { return viewModelProviderFinalSlide.getForFragment( fragment, diff --git a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt index a3d06715fae..2f8e6f9e84d 100644 --- a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt @@ -4,20 +4,27 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModel import org.oppia.android.R import org.oppia.android.app.home.RouteToTopicListener -import org.oppia.android.app.model.Topic +import org.oppia.android.app.model.EphemeralTopic import org.oppia.android.app.shim.IntentFactoryShim import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.domain.translation.TranslationController /** [ViewModel] for displaying topic item in [OngoingTopicListActivity]. */ class OngoingTopicItemViewModel( private val activity: AppCompatActivity, private val internalProfileId: Int, - val topic: Topic, + ephemeralTopic: EphemeralTopic, val entityType: String, private val intentFactoryShim: IntentFactoryShim, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + translationController: TranslationController ) : ObservableViewModel(), RouteToTopicListener { + val topic = ephemeralTopic.topic + + val topicTitle: String by lazy { + translationController.extractString(topic.title, ephemeralTopic.writtenTranslationContext) + } fun onTopicItemClicked() { routeToTopic(internalProfileId, topic.topicId) diff --git a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivity.kt b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivity.kt index 489f7eac60b..3e3a602bf2e 100644 --- a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivity.kt +++ b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.ONGOING_TOPIC_LIST_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for ongoing topics. */ @@ -28,9 +30,10 @@ class OngoingTopicListActivity : InjectableAppCompatActivity() { /** Returns a new [Intent] to route to [OngoingTopicListActivity] for a specified profile ID. */ fun createOngoingTopicListActivityIntent(context: Context, internalProfileId: Int): Intent { - val intent = Intent(context, OngoingTopicListActivity::class.java) - intent.putExtra(ONGOING_TOPIC_LIST_ACTIVITY_PROFILE_ID_KEY, internalProfileId) - return intent + return Intent(context, OngoingTopicListActivity::class.java).apply { + putExtra(ONGOING_TOPIC_LIST_ACTIVITY_PROFILE_ID_KEY, internalProfileId) + decorateWithScreenName(ONGOING_TOPIC_LIST_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt index 9d3e2fbd9fb..37ecd2369ad 100644 --- a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt @@ -11,6 +11,7 @@ import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.parser.html.TopicHtmlParserEntityType @@ -24,7 +25,8 @@ class OngoingTopicListViewModel @Inject constructor( private val oppiaLogger: OppiaLogger, private val intentFactoryShim: IntentFactoryShim, @TopicHtmlParserEntityType private val entityType: String, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : ObservableViewModel() { /** [internalProfileId] needs to be set before any of the live data members can be accessed. */ private var internalProfileId: Int = -1 @@ -69,9 +71,15 @@ class OngoingTopicListViewModel @Inject constructor( ): List { val itemViewModelList: MutableList = mutableListOf() itemViewModelList.addAll( - ongoingTopicList.topicList.map { topic -> + ongoingTopicList.topicList.map { ephemeralTopic -> OngoingTopicItemViewModel( - activity, internalProfileId, topic, entityType, intentFactoryShim, resourceHandler + activity, + internalProfileId, + ephemeralTopic, + entityType, + intentFactoryShim, + resourceHandler, + translationController ) } ) diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageActivity.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageActivity.kt index f0ae26ecd18..abee6ad3500 100644 --- a/app/src/main/java/org/oppia/android/app/options/AppLanguageActivity.kt +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.APP_LANGUAGE_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** The activity to change the language of the app. */ @@ -43,10 +45,11 @@ class AppLanguageActivity : InjectableAppCompatActivity() { prefKey: String, summaryValue: String? ): Intent { - val intent = Intent(context, AppLanguageActivity::class.java) - intent.putExtra(APP_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY, prefKey) - intent.putExtra(APP_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY, summaryValue) - return intent + return Intent(context, AppLanguageActivity::class.java).apply { + putExtra(APP_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY, prefKey) + putExtra(APP_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY, summaryValue) + decorateWithScreenName(APP_LANGUAGE_ACTIVITY) + } } fun getAppLanguagePreferenceTitleExtraKey(): String { diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageFragment.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageFragment.kt index e5b3b5e5ab0..d83088db9af 100644 --- a/app/src/main/java/org/oppia/android/app/options/AppLanguageFragment.kt +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageFragment.kt @@ -17,9 +17,7 @@ private const val APP_LANGUAGE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY = private const val SELECTED_LANGUAGE_SAVED_KEY = "AppLanguageFragment.selected_language" /** The fragment to change the language of the app. */ -class AppLanguageFragment : - InjectableFragment(), - LanguageRadioButtonListener { +class AppLanguageFragment : InjectableFragment(), AppLanguageRadioButtonListener { @Inject lateinit var appLanguageFragmentPresenter: AppLanguageFragmentPresenter diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt index 1b4b2e6a1f8..89810a9266f 100644 --- a/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt @@ -6,13 +6,13 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.databinding.AppLanguageFragmentBinding -import org.oppia.android.databinding.LanguageItemsBinding +import org.oppia.android.databinding.AppLanguageItemBinding import javax.inject.Inject /** The presenter for [AppLanguageFragment]. */ class AppLanguageFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val languageSelectionViewModel: LanguageSelectionViewModel + private val appLanguageSelectionViewModel: AppLanguageSelectionViewModel ) { private lateinit var prefSummaryValue: String fun handleOnCreateView( @@ -27,8 +27,8 @@ class AppLanguageFragmentPresenter @Inject constructor( /* attachToRoot= */ false ) this.prefSummaryValue = prefSummaryValue - binding.viewModel = languageSelectionViewModel - languageSelectionViewModel.selectedLanguage.value = prefSummaryValue + binding.viewModel = appLanguageSelectionViewModel + appLanguageSelectionViewModel.selectedLanguage.value = prefSummaryValue binding.languageRecyclerView.apply { adapter = createRecyclerViewAdapter() } @@ -37,16 +37,16 @@ class AppLanguageFragmentPresenter @Inject constructor( } fun getLanguageSelected(): String? { - return languageSelectionViewModel.selectedLanguage.value + return appLanguageSelectionViewModel.selectedLanguage.value } - private fun createRecyclerViewAdapter(): BindableAdapter { + private fun createRecyclerViewAdapter(): BindableAdapter { return BindableAdapter.SingleTypeBuilder - .newBuilder() + .newBuilder() .setLifecycleOwner(fragment) .registerViewDataBinderWithSameModelType( - inflateDataBinding = LanguageItemsBinding::inflate, - setViewModel = LanguageItemsBinding::setViewModel + inflateDataBinding = AppLanguageItemBinding::inflate, + setViewModel = AppLanguageItemBinding::setViewModel ).build() } @@ -61,7 +61,7 @@ class AppLanguageFragmentPresenter @Inject constructor( } fun onLanguageSelected(selectedLanguage: String) { - languageSelectionViewModel.selectedLanguage.value = selectedLanguage + appLanguageSelectionViewModel.selectedLanguage.value = selectedLanguage updateAppLanguage(selectedLanguage) } } diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageItemViewModel.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageItemViewModel.kt new file mode 100644 index 00000000000..c328328936a --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageItemViewModel.kt @@ -0,0 +1,27 @@ +package org.oppia.android.app.options + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import org.oppia.android.app.viewmodel.ObservableViewModel + +/** + * Language item view model for the recycler view in [AppLanguageFragment] and. + * + * @property language the app language corresponding to this language item to be displayed + * @property currentSelectedLanguage the [LiveData] tracking the currently selected language + * @property appLanguageRadioButtonListener the listener which will be called if this language is + * selected by the user + */ +class AppLanguageItemViewModel( + val language: String, + private val currentSelectedLanguage: LiveData, + val appLanguageRadioButtonListener: AppLanguageRadioButtonListener +) : ObservableViewModel() { + /** + * Indicates whether the language corresponding to this view model is _currently_ selected in the + * radio button list. + */ + val isLanguageSelected: LiveData by lazy { + Transformations.map(currentSelectedLanguage) { it == language } + } +} diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageRadioButtonListener.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageRadioButtonListener.kt new file mode 100644 index 00000000000..5ae0f387241 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageRadioButtonListener.kt @@ -0,0 +1,7 @@ +package org.oppia.android.app.options + +/** Listener for when a language is selected for the [AppLanguageFragment]. */ +interface AppLanguageRadioButtonListener { + /** Called when the user selected a new app language to use as their default preference. */ + fun onLanguageSelected(appLanguage: String) +} diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageSelectionViewModel.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageSelectionViewModel.kt new file mode 100644 index 00000000000..72dd156f64d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageSelectionViewModel.kt @@ -0,0 +1,27 @@ +package org.oppia.android.app.options + +import androidx.fragment.app.Fragment +import androidx.lifecycle.MutableLiveData +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.viewmodel.ObservableViewModel +import javax.inject.Inject + +/** Language list view model for the recycler view in [AppLanguageFragment]. */ +@FragmentScope +class AppLanguageSelectionViewModel @Inject constructor( + val fragment: Fragment +) : ObservableViewModel() { + /** The name of the app language currently selected in the radio button list. */ + val selectedLanguage = MutableLiveData() + private val appLanguageRadioButtonListener = fragment as AppLanguageRadioButtonListener + + private val appLanguagesList = listOf( + AppLanguageItemViewModel("English", selectedLanguage, appLanguageRadioButtonListener), + AppLanguageItemViewModel("French", selectedLanguage, appLanguageRadioButtonListener), + AppLanguageItemViewModel("Hindi", selectedLanguage, appLanguageRadioButtonListener), + AppLanguageItemViewModel("Chinese", selectedLanguage, appLanguageRadioButtonListener) + ) + + /** The list of [AppLanguageItemViewModel]s which can be bound to a recycler view. */ + val recyclerViewAppLanguageList: List by lazy { appLanguagesList } +} diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivity.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivity.kt index 860cef3b6d6..d148c0324d7 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivity.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivity.kt @@ -5,72 +5,67 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguageActivityParams +import org.oppia.android.app.model.AudioLanguageActivityStateBundle +import org.oppia.android.app.model.ScreenName.AUDIO_LANGUAGE_ACTIVITY +import org.oppia.android.util.extensions.getProto +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.extensions.putProto +import org.oppia.android.util.extensions.putProtoExtra +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** The activity to change the Default Audio language of the app. */ class AudioLanguageActivity : InjectableAppCompatActivity() { - - @Inject - lateinit var audioLanguageActivityPresenter: AudioLanguageActivityPresenter - private lateinit var prefKey: String - private lateinit var prefSummaryValue: String + @Inject lateinit var audioLanguageActivityPresenter: AudioLanguageActivityPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) - prefKey = checkNotNull(intent.getStringExtra(AUDIO_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY)) { - "Expected $AUDIO_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY to be in intent extras." - } - prefSummaryValue = if (savedInstanceState != null) { - savedInstanceState.get(AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY) as String - } else { - checkNotNull(intent.getStringExtra(AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY)) { - "Expected $AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY to be in intent extras." - } - } - audioLanguageActivityPresenter.handleOnCreate(prefKey, prefSummaryValue) + audioLanguageActivityPresenter.handleOnCreate( + savedInstanceState?.retrieveLanguageFromSavedState() ?: intent.retrieveLanguageFromParams() + ) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + val state = AudioLanguageActivityStateBundle.newBuilder().apply { + audioLanguage = audioLanguageActivityPresenter.getLanguageSelected() + }.build() + outState.putProto(ACTIVITY_SAVED_STATE_KEY, state) } + override fun onBackPressed() = audioLanguageActivityPresenter.finishWithResult() + companion object { - internal const val AUDIO_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY = - "AudioLanguageActivity.audio_language_preference_title" - const val AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY = - "AudioLanguageActivity.audio_language_preference_summary_value" + private const val ACTIVITY_PARAMS_KEY = "AudioLanguageActivity.params" + private const val ACTIVITY_SAVED_STATE_KEY = "AudioLanguageActivity.saved_state" /** Returns a new [Intent] to route to [AudioLanguageActivity]. */ fun createAudioLanguageActivityIntent( context: Context, - prefKey: String, - summaryValue: String? + audioLanguage: AudioLanguage ): Intent { - val intent = Intent(context, AudioLanguageActivity::class.java) - intent.putExtra(AUDIO_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY, prefKey) - intent.putExtra(AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY, summaryValue) - return intent + return Intent(context, AudioLanguageActivity::class.java).apply { + val arguments = AudioLanguageActivityParams.newBuilder().apply { + this.audioLanguage = audioLanguage + }.build() + putProtoExtra(ACTIVITY_PARAMS_KEY, arguments) + decorateWithScreenName(AUDIO_LANGUAGE_ACTIVITY) + } } - fun getKeyAudioLanguagePreferenceTitle(): String { - return AUDIO_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY + private fun Intent.retrieveLanguageFromParams(): AudioLanguage { + return getProtoExtra( + ACTIVITY_PARAMS_KEY, AudioLanguageActivityParams.getDefaultInstance() + ).audioLanguage } - fun getKeyAudioLanguagePreferenceSummaryValue(): String { - return AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY + private fun Bundle.retrieveLanguageFromSavedState(): AudioLanguage { + return getProto( + ACTIVITY_SAVED_STATE_KEY, AudioLanguageActivityStateBundle.getDefaultInstance() + ).audioLanguage } } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putString( - AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY, - audioLanguageActivityPresenter.getLanguageSelected() - ) - } - - override fun onBackPressed() { - val message = audioLanguageActivityPresenter.getLanguageSelected() - val intent = Intent() - intent.putExtra(MESSAGE_AUDIO_LANGUAGE_ARGUMENT_KEY, message) - setResult(REQUEST_CODE_AUDIO_LANGUAGE, intent) - finish() - } } diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt index 0b303875ed9..0a842397a4b 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt @@ -5,46 +5,60 @@ import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguageActivityResultBundle import org.oppia.android.databinding.AudioLanguageActivityBinding +import org.oppia.android.util.extensions.putProtoExtra import javax.inject.Inject /** The presenter for [AudioLanguageActivity]. */ @ActivityScope class AudioLanguageActivityPresenter @Inject constructor(private val activity: AppCompatActivity) { + private lateinit var audioLanguage: AudioLanguage - private lateinit var prefSummaryValue: String - - fun handleOnCreate(prefKey: String, prefValue: String) { - val binding: AudioLanguageActivityBinding = DataBindingUtil.setContentView( - activity, - R.layout.audio_language_activity, - ) - val toolbar = binding.audioLanguageToolbar - toolbar.setNavigationOnClickListener { - val intent = Intent().apply { - putExtra(MESSAGE_AUDIO_LANGUAGE_ARGUMENT_KEY, prefSummaryValue) - } - (activity as AudioLanguageActivity).setResult(REQUEST_CODE_AUDIO_LANGUAGE, intent) - activity.finish() + /** Handles when the activity is first created. */ + fun handleOnCreate(audioLanguage: AudioLanguage) { + this.audioLanguage = audioLanguage + + val binding: AudioLanguageActivityBinding = + DataBindingUtil.setContentView(activity, R.layout.audio_language_activity) + binding.audioLanguageToolbar.setNavigationOnClickListener { + finishWithResult() } - setLanguageSelected(prefValue) if (getAudioLanguageFragment() == null) { - val audioLanguageFragment = AudioLanguageFragment.newInstance(prefKey, prefValue) + val audioLanguageFragment = AudioLanguageFragment.newInstance(audioLanguage) activity.supportFragmentManager.beginTransaction() .add(R.id.audio_language_fragment_container, audioLanguageFragment).commitNow() } } - fun setLanguageSelected(audioLanguage: String) { - prefSummaryValue = audioLanguage + /** Updates the currently selected [AudioLanguage] to the specified [audioLanguage]. */ + fun setLanguageSelected(audioLanguage: AudioLanguage) { + this.audioLanguage = audioLanguage } - fun getLanguageSelected(): String { - return prefSummaryValue + /** Returns the current [AudioLanguage] selected in the activity. */ + fun getLanguageSelected(): AudioLanguage = audioLanguage + + /** + * Finishes the current activity with a result (specifically, an intent result with + * [AudioLanguageActivityResultBundle] populated with the [AudioLanguage] that was selected in the + * activity). + */ + fun finishWithResult() { + val intent = Intent().apply { + val result = AudioLanguageActivityResultBundle.newBuilder().apply { + this.audioLanguage = this@AudioLanguageActivityPresenter.audioLanguage + }.build() + putProtoExtra(MESSAGE_AUDIO_LANGUAGE_RESULTS_KEY, result) + } + + activity.setResult(REQUEST_CODE_AUDIO_LANGUAGE, intent) + activity.finish() } private fun getAudioLanguageFragment(): AudioLanguageFragment? { return activity.supportFragmentManager - .findFragmentById(R.id.audio_language_fragment_container) as AudioLanguageFragment? + .findFragmentById(R.id.audio_language_fragment_container) as? AudioLanguageFragment } } diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt index 4ae93257ada..fd98e6259cd 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt @@ -7,34 +7,16 @@ import android.view.View import android.view.ViewGroup import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment -import org.oppia.android.util.extensions.getStringFromBundle +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguageFragmentArguments +import org.oppia.android.app.model.AudioLanguageFragmentStateBundle +import org.oppia.android.util.extensions.getProto +import org.oppia.android.util.extensions.putProto import javax.inject.Inject -private const val AUDIO_LANGUAGE_PREFERENCE_TITLE_ARGUMENT_KEY = - "AudioLanguageFragment.audio_language_preference_title" -private const val AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY = - "AudioLanguageFragment.audio_language_preference_summary_value" -private const val SELECTED_AUDIO_LANGUAGE_SAVED_KEY = - "AudioLanguageFragment.selected_audio_language" - /** The fragment to change the default audio language of the app. */ -class AudioLanguageFragment : - InjectableFragment(), - LanguageRadioButtonListener { - - @Inject - lateinit var audioLanguageFragmentPresenter: AudioLanguageFragmentPresenter - - companion object { - fun newInstance(prefsKey: String, prefsSummaryValue: String): AudioLanguageFragment { - val args = Bundle() - args.putString(AUDIO_LANGUAGE_PREFERENCE_TITLE_ARGUMENT_KEY, prefsKey) - args.putString(AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY, prefsSummaryValue) - val fragment = AudioLanguageFragment() - fragment.arguments = args - return fragment - } - } +class AudioLanguageFragment : InjectableFragment(), AudioLanguageRadioButtonListener { + @Inject lateinit var audioLanguageFragmentPresenter: AudioLanguageFragmentPresenter override fun onAttach(context: Context) { super.onAttach(context) @@ -45,36 +27,56 @@ class AudioLanguageFragment : inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - val args = - checkNotNull(arguments) { "Expected arguments to be passed to AudioLanguageFragment" } - val prefsKey = args.getStringFromBundle(AUDIO_LANGUAGE_PREFERENCE_TITLE_ARGUMENT_KEY) - val audioLanguageDefaultSummary = checkNotNull( - args.getStringFromBundle(AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY) - ) - val prefsSummaryValue = if (savedInstanceState == null) { - audioLanguageDefaultSummary - } else { - savedInstanceState.get(SELECTED_AUDIO_LANGUAGE_SAVED_KEY) as? String - ?: audioLanguageDefaultSummary - } - return audioLanguageFragmentPresenter.handleOnCreateView( - inflater, - container, - prefsKey!!, - prefsSummaryValue - ) + ): View { + val audioLanguage = + checkNotNull( + savedInstanceState?.retrieveLanguageFromSavedState() + ?: arguments?.retrieveLanguageFromArguments() + ) { "Expected arguments to be passed to AudioLanguageFragment" } + return audioLanguageFragmentPresenter.handleOnCreateView(inflater, container, audioLanguage) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putString( - SELECTED_AUDIO_LANGUAGE_SAVED_KEY, - audioLanguageFragmentPresenter.getLanguageSelected() - ) + val state = AudioLanguageFragmentStateBundle.newBuilder().apply { + audioLanguage = audioLanguageFragmentPresenter.getLanguageSelected() + }.build() + outState.putProto(FRAGMENT_SAVED_STATE_KEY, state) + } + + override fun onLanguageSelected(audioLanguage: AudioLanguage) { + audioLanguageFragmentPresenter.onLanguageSelected(audioLanguage) } - override fun onLanguageSelected(selectedLanguage: String) { - audioLanguageFragmentPresenter.onLanguageSelected(selectedLanguage) + companion object { + private const val FRAGMENT_ARGUMENTS_KEY = "AudioLanguageFragment.arguments" + private const val FRAGMENT_SAVED_STATE_KEY = "AudioLanguageFragment.saved_state" + + /** + * Returns a new [AudioLanguageFragment] corresponding to the specified [AudioLanguage] (as the + * initial selection). + */ + fun newInstance(audioLanguage: AudioLanguage): AudioLanguageFragment { + return AudioLanguageFragment().apply { + arguments = Bundle().apply { + val args = AudioLanguageFragmentArguments.newBuilder().apply { + this.audioLanguage = audioLanguage + }.build() + putProto(FRAGMENT_ARGUMENTS_KEY, args) + } + } + } + + private fun Bundle.retrieveLanguageFromArguments(): AudioLanguage { + return getProto( + FRAGMENT_ARGUMENTS_KEY, AudioLanguageFragmentArguments.getDefaultInstance() + ).audioLanguage + } + + private fun Bundle.retrieveLanguageFromSavedState(): AudioLanguage { + return getProto( + FRAGMENT_SAVED_STATE_KEY, AudioLanguageFragmentStateBundle.getDefaultInstance() + ).audioLanguage + } } } diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt index 9e8cb65ee06..f235685035f 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt @@ -4,53 +4,56 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import org.oppia.android.app.model.AudioLanguage import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.databinding.AudioLanguageFragmentBinding -import org.oppia.android.databinding.LanguageItemsBinding +import org.oppia.android.databinding.AudioLanguageItemBinding import javax.inject.Inject /** The presenter for [AudioLanguageFragment]. */ class AudioLanguageFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val languageSelectionViewModel: LanguageSelectionViewModel + private val audioLanguageSelectionViewModel: AudioLanguageSelectionViewModel ) { - private lateinit var prefSummaryValue: String + /** + * Returns a newly inflated view to render the fragment with the specified [audioLanguage] as the + * initial selected language. + */ fun handleOnCreateView( inflater: LayoutInflater, container: ViewGroup?, - prefKey: String, - prefValue: String - ): View? { - val binding = AudioLanguageFragmentBinding.inflate( + audioLanguage: AudioLanguage + ): View { + return AudioLanguageFragmentBinding.inflate( inflater, container, /* attachToRoot= */ false - ) - binding.viewModel = languageSelectionViewModel - prefSummaryValue = prefValue - languageSelectionViewModel.selectedLanguage.value = prefSummaryValue - binding.audioLanguageRecyclerView.apply { - adapter = createRecyclerViewAdapter() - } - - return binding.root + ).apply { + audioLanguageSelectionViewModel.selectedLanguage.value = audioLanguage + audioLanguageRecyclerView.apply { + viewModel = audioLanguageSelectionViewModel + adapter = createRecyclerViewAdapter() + } + }.root } - fun getLanguageSelected(): String? { - return languageSelectionViewModel.selectedLanguage.value + /** Returns the language currently selected in the fragment. */ + fun getLanguageSelected(): AudioLanguage { + return audioLanguageSelectionViewModel.selectedLanguage.value + ?: AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED } - private fun createRecyclerViewAdapter(): BindableAdapter { + private fun createRecyclerViewAdapter(): BindableAdapter { return BindableAdapter.SingleTypeBuilder - .newBuilder() + .newBuilder() .setLifecycleOwner(fragment) .registerViewDataBinderWithSameModelType( - inflateDataBinding = LanguageItemsBinding::inflate, - setViewModel = LanguageItemsBinding::setViewModel + inflateDataBinding = AudioLanguageItemBinding::inflate, + setViewModel = AudioLanguageItemBinding::setViewModel ).build() } - private fun updateAudioLanguage(audioLanguage: String) { + private fun updateAudioLanguage(audioLanguage: AudioLanguage) { // The first branch of (when) will be used in the case of multipane when (val parentActivity = fragment.activity) { is OptionsActivity -> @@ -60,8 +63,9 @@ class AudioLanguageFragmentPresenter @Inject constructor( } } - fun onLanguageSelected(selectedLanguage: String) { - languageSelectionViewModel.selectedLanguage.value = selectedLanguage - updateAudioLanguage(selectedLanguage) + /** Handles when a new [AudioLanguage] has been selected by the user. */ + fun onLanguageSelected(audioLanguage: AudioLanguage) { + audioLanguageSelectionViewModel.selectedLanguage.value = audioLanguage + updateAudioLanguage(audioLanguage) } } diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageItemViewModel.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageItemViewModel.kt new file mode 100644 index 00000000000..7ebe3fa7931 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageItemViewModel.kt @@ -0,0 +1,32 @@ +package org.oppia.android.app.options + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.viewmodel.ObservableViewModel + +/** + * Language item view model for the recycler view in [AppLanguageFragment] and + * [AudioLanguageFragment]. + * + * @property language the [AudioLanguage] corresponding to this language item to be displayed + * @property languageDisplayName the human-readable version of [language] to display to users to + * represent the language + * @property currentSelectedLanguage the [LiveData] tracking the currently selected [AudioLanguage] + * @property audioLanguageRadioButtonListener the listener which will be called if this language is + * selected by the user + */ +class AudioLanguageItemViewModel( + val language: AudioLanguage, + val languageDisplayName: String, + private val currentSelectedLanguage: LiveData, + val audioLanguageRadioButtonListener: AudioLanguageRadioButtonListener +) : ObservableViewModel() { + /** + * Indicates whether the language corresponding to this view model is _currently_ selected in the + * radio button list. + */ + val isLanguageSelected: LiveData by lazy { + Transformations.map(currentSelectedLanguage) { it == language } + } +} diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageRadioButtonListener.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageRadioButtonListener.kt new file mode 100644 index 00000000000..a60f661cb48 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageRadioButtonListener.kt @@ -0,0 +1,9 @@ +package org.oppia.android.app.options + +import org.oppia.android.app.model.AudioLanguage + +/** Listener for when the a language is selected for the [AudioLanguageFragment]. */ +interface AudioLanguageRadioButtonListener { + /** Called when the user selected a new [AudioLanguage] to use as their default preference. */ + fun onLanguageSelected(audioLanguage: AudioLanguage) +} diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt new file mode 100644 index 00000000000..c9e0d998e1b --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt @@ -0,0 +1,40 @@ +package org.oppia.android.app.options + +import androidx.fragment.app.Fragment +import androidx.lifecycle.MutableLiveData +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.viewmodel.ObservableViewModel +import javax.inject.Inject + +/** Language list view model for the recycler view in [AudioLanguageFragment]. */ +@FragmentScope +class AudioLanguageSelectionViewModel @Inject constructor( + private val fragment: Fragment, + private val appLanguageResourceHandler: AppLanguageResourceHandler +) : ObservableViewModel() { + /** The [AudioLanguage] currently selected in the radio button list. */ + val selectedLanguage = MutableLiveData() + + /** The list of [AudioLanguageItemViewModel]s which can be bound to a recycler view. */ + val recyclerViewAudioLanguageList: List by lazy { + AudioLanguage.values().filter { it !in IGNORED_AUDIO_LANGUAGES }.map(::createItemViewModel) + } + + private fun createItemViewModel(language: AudioLanguage): AudioLanguageItemViewModel { + return AudioLanguageItemViewModel( + language, + appLanguageResourceHandler.computeLocalizedDisplayName(language), + selectedLanguage, + fragment as AudioLanguageRadioButtonListener + ) + } + + private companion object { + private val IGNORED_AUDIO_LANGUAGES = + listOf( + AudioLanguage.NO_AUDIO, AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED, AudioLanguage.UNRECOGNIZED + ) + } +} diff --git a/app/src/main/java/org/oppia/android/app/options/LanguageItemViewModel.kt b/app/src/main/java/org/oppia/android/app/options/LanguageItemViewModel.kt deleted file mode 100644 index 05fabe0cfe2..00000000000 --- a/app/src/main/java/org/oppia/android/app/options/LanguageItemViewModel.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.oppia.android.app.options - -import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations -import org.oppia.android.app.viewmodel.ObservableViewModel - -/** - * Language item view model for the recycler view in [AppLanguageFragment] and - * [AudioLanguageFragment]. - */ -class LanguageItemViewModel( - val language: String, - private val selectedLanguage: LiveData, - val languageRadioButtonListener: LanguageRadioButtonListener -) : ObservableViewModel() { - val isLanguageSelected: LiveData by lazy { - Transformations.map(selectedLanguage) { it == language } - } -} diff --git a/app/src/main/java/org/oppia/android/app/options/LanguageRadioButtonListener.kt b/app/src/main/java/org/oppia/android/app/options/LanguageRadioButtonListener.kt deleted file mode 100644 index 5eff5d5e21c..00000000000 --- a/app/src/main/java/org/oppia/android/app/options/LanguageRadioButtonListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.oppia.android.app.options - -/** - * Listener for when the language is selected from the [AppLanguageFragment] or - * [AudioLanguageFragment]. - */ -interface LanguageRadioButtonListener { - fun onLanguageSelected(selectedLanguage: String) -} diff --git a/app/src/main/java/org/oppia/android/app/options/LanguageSelectionViewModel.kt b/app/src/main/java/org/oppia/android/app/options/LanguageSelectionViewModel.kt deleted file mode 100644 index 83ced75d498..00000000000 --- a/app/src/main/java/org/oppia/android/app/options/LanguageSelectionViewModel.kt +++ /dev/null @@ -1,44 +0,0 @@ -package org.oppia.android.app.options - -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.lifecycle.MutableLiveData -import org.oppia.android.app.fragment.FragmentScope -import org.oppia.android.app.viewmodel.ObservableViewModel -import javax.inject.Inject - -/** - * Language list view model for the recycler view in [AppLanguageFragment] and - * [AudioLanguageFragment]. - */ -@FragmentScope -class LanguageSelectionViewModel @Inject constructor( - val activity: AppCompatActivity, - val fragment: Fragment -) : ObservableViewModel() { - - val selectedLanguage = MutableLiveData() - val languageRadioButtonListener = fragment as LanguageRadioButtonListener - - private val appLanguagesList = listOf( - LanguageItemViewModel("English", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("French", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("Hindi", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("Chinese", selectedLanguage, languageRadioButtonListener) - ) - private val audioLanguagesList = listOf( - LanguageItemViewModel("No Audio", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("English", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("French", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("Hindi", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("Chinese", selectedLanguage, languageRadioButtonListener) - ) - - val recyclerViewAudioLanguageList: List by lazy { - audioLanguagesList - } - - val recyclerViewAppLanguageList: List by lazy { - appLanguagesList - } -} diff --git a/app/src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt b/app/src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt index 77f1aac8e2c..4d9f1e1f12f 100644 --- a/app/src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt +++ b/app/src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt @@ -1,6 +1,12 @@ package org.oppia.android.app.options +import org.oppia.android.app.model.AudioLanguage + /** Listener for when an activity should load a [AudioLanguageFragment]. */ interface LoadAudioLanguageListListener { - fun loadAudioLanguageFragment(audioLanguage: String) + /** + * Called when the user wishes to change their default audio language (where [audioLanguage] is + * the current default language), when the app is in tablet mode. + */ + fun loadAudioLanguageFragment(audioLanguage: AudioLanguage) } diff --git a/app/src/main/java/org/oppia/android/app/options/LoadReadingTextSizeListener.kt b/app/src/main/java/org/oppia/android/app/options/LoadReadingTextSizeListener.kt index 66a233a97ac..136616923c3 100644 --- a/app/src/main/java/org/oppia/android/app/options/LoadReadingTextSizeListener.kt +++ b/app/src/main/java/org/oppia/android/app/options/LoadReadingTextSizeListener.kt @@ -1,6 +1,12 @@ package org.oppia.android.app.options +import org.oppia.android.app.model.ReadingTextSize + /** Listener for when an activity should load a [ReadingTextSizeFragment]. */ interface LoadReadingTextSizeListener { - fun loadReadingTextSizeFragment(textSize: String) + /** + * Loads a tablet UI panel for changing the current UI reading text size (with the current text + * size being passed in via [textSize]). + */ + fun loadReadingTextSizeFragment(textSize: ReadingTextSize) } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt b/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt index acb9712e054..264080d7fd9 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt @@ -9,10 +9,9 @@ import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.AppLanguage -import org.oppia.android.app.model.AudioLanguage import org.oppia.android.app.model.Profile import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.ReadingTextSize +import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableArrayList import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController @@ -28,7 +27,8 @@ class OptionControlsViewModel @Inject constructor( activity: AppCompatActivity, private val profileManagementController: ProfileManagementController, private val oppiaLogger: OppiaLogger, - @EnableLanguageSelectionUi private val enableLanguageSelectionUi: PlatformParameterValue + @EnableLanguageSelectionUi private val enableLanguageSelectionUi: PlatformParameterValue, + private val resourceHandler: AppLanguageResourceHandler ) : OptionsItemViewModel() { private val itemViewModelList: ObservableList = ObservableArrayList() private lateinit var profileId: ProfileId @@ -84,22 +84,24 @@ class OptionControlsViewModel @Inject constructor( } private fun processProfileList(profile: Profile): List { - itemViewModelList.clear() val optionsReadingTextSizeViewModel = - OptionsReadingTextSizeViewModel(routeToReadingTextSizeListener, loadReadingTextSizeListener) + OptionsReadingTextSizeViewModel( + routeToReadingTextSizeListener, loadReadingTextSizeListener, resourceHandler + ) val optionsAppLanguageViewModel = OptionsAppLanguageViewModel(routeToAppLanguageListListener, loadAppLanguageListListener) val optionAudioViewViewModel = OptionsAudioLanguageViewModel( routeToAudioLanguageListListener, - loadAudioLanguageListListener + loadAudioLanguageListListener, + profile.audioLanguage, + resourceHandler.computeLocalizedDisplayName(profile.audioLanguage) ) - optionsReadingTextSizeViewModel.readingTextSize.set(getReadingTextSize(profile.readingTextSize)) + optionsReadingTextSizeViewModel.readingTextSize.set(profile.readingTextSize) optionsAppLanguageViewModel.appLanguage.set(getAppLanguage(profile.appLanguage)) - optionAudioViewViewModel.audioLanguage.set(getAudioLanguage(profile.audioLanguage)) itemViewModelList.add(optionsReadingTextSizeViewModel as OptionsItemViewModel) @@ -126,15 +128,6 @@ class OptionControlsViewModel @Inject constructor( this.isFirstOpen = isFirstOpen } - fun getReadingTextSize(readingTextSize: ReadingTextSize): String { - return when (readingTextSize) { - ReadingTextSize.SMALL_TEXT_SIZE -> "Small" - ReadingTextSize.MEDIUM_TEXT_SIZE -> "Medium" - ReadingTextSize.LARGE_TEXT_SIZE -> "Large" - else -> "Extra Large" - } - } - fun getAppLanguage(appLanguage: AppLanguage): String { return when (appLanguage) { AppLanguage.ENGLISH_APP_LANGUAGE -> "English" @@ -144,15 +137,4 @@ class OptionControlsViewModel @Inject constructor( else -> "English" } } - - fun getAudioLanguage(audioLanguage: AudioLanguage): String { - return when (audioLanguage) { - AudioLanguage.NO_AUDIO -> "No Audio" - AudioLanguage.ENGLISH_AUDIO_LANGUAGE -> "English" - AudioLanguage.HINDI_AUDIO_LANGUAGE -> "Hindi" - AudioLanguage.FRENCH_AUDIO_LANGUAGE -> "French" - AudioLanguage.CHINESE_AUDIO_LANGUAGE -> "Chinese" - else -> "No Audio" - } - } } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt b/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt index 10c6d366fd4..ed3ae344b4f 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt @@ -8,8 +8,15 @@ import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.drawer.NAVIGATION_PROFILE_ID_ARGUMENT_KEY +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguageActivityResultBundle +import org.oppia.android.app.model.ReadingTextSize +import org.oppia.android.app.model.ReadingTextSizeActivityResultBundle +import org.oppia.android.app.model.ScreenName.OPTIONS_ACTIVITY import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.extensions.getProtoExtra import org.oppia.android.util.extensions.getStringFromBundle +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject private const val SELECTED_OPTIONS_TITLE_SAVED_KEY = "OptionsActivity.selected_options_title" @@ -47,10 +54,11 @@ class OptionsActivity : profileId: Int?, isFromNavigationDrawer: Boolean ): Intent { - val intent = Intent(context, OptionsActivity::class.java) - intent.putExtra(NAVIGATION_PROFILE_ID_ARGUMENT_KEY, profileId) - intent.putExtra(BOOL_IS_FROM_NAVIGATION_DRAWER_EXTRA_KEY, isFromNavigationDrawer) - return intent + return Intent(context, OptionsActivity::class.java).apply { + putExtra(NAVIGATION_PROFILE_ID_ARGUMENT_KEY, profileId) + putExtra(BOOL_IS_FROM_NAVIGATION_DRAWER_EXTRA_KEY, isFromNavigationDrawer) + decorateWithScreenName(OPTIONS_ACTIVITY) + } } } @@ -82,17 +90,25 @@ class OptionsActivity : override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) + checkNotNull(data) { + "Expected data to be passed as an activity result for request: $requestCode." + } when (requestCode) { REQUEST_CODE_TEXT_SIZE -> { - val textSize = data!!.getStringExtra(MESSAGE_READING_TEXT_SIZE_ARGUMENT_KEY) as String - optionActivityPresenter.updateReadingTextSize(textSize) + val textSizeResults = data.getProtoExtra( + MESSAGE_READING_TEXT_SIZE_RESULTS_KEY, + ReadingTextSizeActivityResultBundle.getDefaultInstance() + ) + optionActivityPresenter.updateReadingTextSize(textSizeResults.selectedReadingTextSize) } REQUEST_CODE_APP_LANGUAGE -> { - val appLanguage = data!!.getStringExtra(MESSAGE_APP_LANGUAGE_ARGUMENT_KEY) as String + val appLanguage = data.getStringExtra(MESSAGE_APP_LANGUAGE_ARGUMENT_KEY) as String optionActivityPresenter.updateAppLanguage(appLanguage) } - else -> { - val audioLanguage = data!!.getStringExtra(MESSAGE_AUDIO_LANGUAGE_ARGUMENT_KEY) as String + REQUEST_CODE_AUDIO_LANGUAGE -> { + val audioLanguage = data.getProtoExtra( + MESSAGE_AUDIO_LANGUAGE_RESULTS_KEY, AudioLanguageActivityResultBundle.getDefaultInstance() + ).audioLanguage optionActivityPresenter.updateAudioLanguage(audioLanguage) } } @@ -109,29 +125,21 @@ class OptionsActivity : ) } - override fun routeAudioLanguageList(audioLanguage: String?) { + override fun routeAudioLanguageList(audioLanguage: AudioLanguage) { startActivityForResult( - AudioLanguageActivity.createAudioLanguageActivityIntent( - this, - AUDIO_LANGUAGE, - audioLanguage - ), + AudioLanguageActivity.createAudioLanguageActivityIntent(this, audioLanguage), REQUEST_CODE_AUDIO_LANGUAGE ) } - override fun routeReadingTextSize(readingTextSize: String?) { + override fun routeReadingTextSize(readingTextSize: ReadingTextSize) { startActivityForResult( - ReadingTextSizeActivity.createReadingTextSizeActivityIntent( - this, - READING_TEXT_SIZE, - readingTextSize - ), + ReadingTextSizeActivity.createReadingTextSizeActivityIntent(this, readingTextSize), REQUEST_CODE_TEXT_SIZE ) } - override fun loadReadingTextSizeFragment(textSize: String) { + override fun loadReadingTextSizeFragment(textSize: ReadingTextSize) { selectedFragment = READING_TEXT_SIZE_FRAGMENT optionActivityPresenter.setExtraOptionTitle( resourceHandler.getStringInLocale(R.string.reading_text_size) @@ -147,7 +155,7 @@ class OptionsActivity : optionActivityPresenter.loadAppLanguageFragment(appLanguage) } - override fun loadAudioLanguageFragment(audioLanguage: String) { + override fun loadAudioLanguageFragment(audioLanguage: AudioLanguage) { selectedFragment = AUDIO_LANGUAGE_FRAGMENT optionActivityPresenter.setExtraOptionTitle( resourceHandler.getStringInLocale(R.string.audio_language) diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt index 2bce5d2fb5c..abe803f31af 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt @@ -9,6 +9,8 @@ import androidx.drawerlayout.widget.DrawerLayout import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.drawer.NavigationDrawerFragment +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.ReadingTextSize import javax.inject.Inject /** The presenter for [OptionsActivity]. */ @@ -79,7 +81,7 @@ class OptionsActivityPresenter @Inject constructor( ) as OptionsFragment? } - fun updateReadingTextSize(textSize: String) { + fun updateReadingTextSize(textSize: ReadingTextSize) { getOptionFragment()?.updateReadingTextSize(textSize) } @@ -87,15 +89,15 @@ class OptionsActivityPresenter @Inject constructor( getOptionFragment()?.updateAppLanguage(appLanguage) } - fun updateAudioLanguage(audioLanguage: String) { + fun updateAudioLanguage(audioLanguage: AudioLanguage) { getOptionFragment()?.updateAudioLanguage(audioLanguage) } - fun loadReadingTextSizeFragment(textSize: String) { + fun loadReadingTextSizeFragment(textSize: ReadingTextSize) { val readingTextSizeFragment = ReadingTextSizeFragment.newInstance(textSize) activity.supportFragmentManager .beginTransaction() - .add(R.id.multipane_options_container, readingTextSizeFragment) + .replace(R.id.multipane_options_container, readingTextSizeFragment) .commitNow() getOptionFragment()?.setSelectedFragment(READING_TEXT_SIZE_FRAGMENT) } @@ -105,17 +107,16 @@ class OptionsActivityPresenter @Inject constructor( AppLanguageFragment.newInstance(APP_LANGUAGE, appLanguage) activity.supportFragmentManager .beginTransaction() - .add(R.id.multipane_options_container, appLanguageFragment) + .replace(R.id.multipane_options_container, appLanguageFragment) .commitNow() getOptionFragment()?.setSelectedFragment(APP_LANGUAGE_FRAGMENT) } - fun loadAudioLanguageFragment(audioLanguage: String) { - val audioLanguageFragment = - AudioLanguageFragment.newInstance(AUDIO_LANGUAGE, audioLanguage) + fun loadAudioLanguageFragment(audioLanguage: AudioLanguage) { + val audioLanguageFragment = AudioLanguageFragment.newInstance(audioLanguage) activity.supportFragmentManager .beginTransaction() - .add(R.id.multipane_options_container, audioLanguageFragment) + .replace(R.id.multipane_options_container, audioLanguageFragment) .commitNow() getOptionFragment()?.setSelectedFragment(AUDIO_LANGUAGE_FRAGMENT) } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt b/app/src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt index 95ccd0e773d..4cde780e528 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt @@ -1,23 +1,23 @@ package org.oppia.android.app.options -import androidx.databinding.ObservableField +import org.oppia.android.app.model.AudioLanguage /** Audio language settings view model for the recycler view in [OptionsFragment]. */ class OptionsAudioLanguageViewModel( private val routeToAudioLanguageListListener: RouteToAudioLanguageListListener, - private val loadAudioLanguageListListener: LoadAudioLanguageListListener + private val loadAudioLanguageListListener: LoadAudioLanguageListListener, + private val audioLanguage: AudioLanguage, + val audioLanguageDisplayName: String ) : OptionsItemViewModel() { - val audioLanguage = ObservableField("") - - fun setAudioLanguage(audioLanguageValue: String) { - audioLanguage.set(audioLanguageValue) - } - + /** + * Handles when the user wishes to change their default audio language and clicks on the button to + * open that configuration screen/pane. + */ fun onAudioLanguageClicked() { if (isMultipane.get()!!) { - loadAudioLanguageListListener.loadAudioLanguageFragment(audioLanguage.get()!!) + loadAudioLanguageListListener.loadAudioLanguageFragment(audioLanguage) } else { - routeToAudioLanguageListListener.routeAudioLanguageList(audioLanguage.get()) + routeToAudioLanguageListListener.routeAudioLanguageList(audioLanguage) } } } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt b/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt index 0e11b55bdc5..ecf01ac8773 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt @@ -7,12 +7,14 @@ import android.view.View import android.view.ViewGroup import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.util.extensions.getStringFromBundle import javax.inject.Inject -const val MESSAGE_READING_TEXT_SIZE_ARGUMENT_KEY = "OptionsFragment.message_reading_text_size" +const val MESSAGE_READING_TEXT_SIZE_RESULTS_KEY = "OptionsFragment.message_reading_text_size" const val MESSAGE_APP_LANGUAGE_ARGUMENT_KEY = "OptionsFragment.message_app_language" -const val MESSAGE_AUDIO_LANGUAGE_ARGUMENT_KEY = "OptionsFragment.message_audio_language" +const val MESSAGE_AUDIO_LANGUAGE_RESULTS_KEY = "OptionsFragment.message_audio_language" const val REQUEST_CODE_TEXT_SIZE = 1 const val REQUEST_CODE_APP_LANGUAGE = 2 const val REQUEST_CODE_AUDIO_LANGUAGE = 3 @@ -66,7 +68,7 @@ class OptionsFragment : InjectableFragment() { ) } - fun updateReadingTextSize(textSize: String) { + fun updateReadingTextSize(textSize: ReadingTextSize) { optionsFragmentPresenter.runAfterUIInitialization { optionsFragmentPresenter.updateReadingTextSize(textSize) } @@ -78,7 +80,7 @@ class OptionsFragment : InjectableFragment() { } } - fun updateAudioLanguage(audioLanguage: String) { + fun updateAudioLanguage(audioLanguage: AudioLanguage) { optionsFragmentPresenter.runAfterUIInitialization { optionsFragmentPresenter.updateAudioLanguage(audioLanguage) } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt index 369f0efa623..86978573d83 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt @@ -26,9 +26,7 @@ import org.oppia.android.util.data.DataProviders.Companion.toLiveData import java.security.InvalidParameterException import javax.inject.Inject -const val READING_TEXT_SIZE = "READING_TEXT_SIZE" const val APP_LANGUAGE = "APP_LANGUAGE" -const val AUDIO_LANGUAGE = "AUDIO_LANGUAGE" private const val READING_TEXT_SIZE_TAG = "ReadingTextSize" private const val APP_LANGUAGE_TAG = "AppLanguage" private const val AUDIO_LANGUAGE_TAG = "AudioLanguage" @@ -52,7 +50,6 @@ class OptionsFragmentPresenter @Inject constructor( private lateinit var recyclerViewAdapter: RecyclerView.Adapter<*> private var internalProfileId: Int = -1 private lateinit var profileId: ProfileId - private var readingTextSize = ReadingTextSize.SMALL_TEXT_SIZE private var appLanguage = AppLanguage.ENGLISH_APP_LANGUAGE private var audioLanguage = AudioLanguage.NO_AUDIO private val viewModel = getOptionControlsItemViewModel() @@ -185,86 +182,20 @@ class OptionsFragmentPresenter @Inject constructor( VIEW_TYPE_AUDIO_LANGUAGE } - fun updateReadingTextSize(textSize: String) { - when (textSize) { - getOptionControlsItemViewModel().getReadingTextSize(ReadingTextSize.SMALL_TEXT_SIZE) -> { - profileManagementController.updateReadingTextSize( - profileId, - ReadingTextSize.SMALL_TEXT_SIZE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> readingTextSize = ReadingTextSize.SMALL_TEXT_SIZE - is AsyncResult.Failure -> { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: small text size", it.error - ) - } - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel().getReadingTextSize(ReadingTextSize.MEDIUM_TEXT_SIZE) -> { - profileManagementController.updateReadingTextSize( - profileId, - ReadingTextSize.MEDIUM_TEXT_SIZE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> readingTextSize = ReadingTextSize.MEDIUM_TEXT_SIZE - is AsyncResult.Failure -> { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: medium text size", it.error - ) - } - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel().getReadingTextSize(ReadingTextSize.LARGE_TEXT_SIZE) -> { - profileManagementController.updateReadingTextSize( - profileId, - ReadingTextSize.LARGE_TEXT_SIZE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> readingTextSize = ReadingTextSize.LARGE_TEXT_SIZE - is AsyncResult.Failure -> { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: large text size", it.error - ) - } - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel() - .getReadingTextSize(ReadingTextSize.EXTRA_LARGE_TEXT_SIZE) -> { - profileManagementController.updateReadingTextSize( - profileId, - ReadingTextSize.EXTRA_LARGE_TEXT_SIZE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> readingTextSize = ReadingTextSize.EXTRA_LARGE_TEXT_SIZE - is AsyncResult.Failure -> { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: extra large text size", it.error - ) - } - is AsyncResult.Pending -> {} // Wait for a result. - } + fun updateReadingTextSize(textSize: ReadingTextSize) { + profileManagementController.updateReadingTextSize(profileId, textSize).toLiveData().observe( + fragment, + { + when (it) { + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: updating to $textSize", it.error + ) } - ) + else -> {} // Nothing needs to be done unless the update failed. + } } - } + ) recyclerViewAdapter.notifyItemChanged(0) } @@ -339,87 +270,14 @@ class OptionsFragmentPresenter @Inject constructor( recyclerViewAdapter.notifyItemChanged(1) } - fun updateAudioLanguage(language: String) { - when (language) { - getOptionControlsItemViewModel().getAudioLanguage(AudioLanguage.NO_AUDIO) -> { - profileManagementController.updateAudioLanguage( - profileId, - AudioLanguage.NO_AUDIO - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> audioLanguage = AudioLanguage.NO_AUDIO - is AsyncResult.Failure -> - oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: No Audio", it.error) - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel().getAudioLanguage(AudioLanguage.ENGLISH_AUDIO_LANGUAGE) -> { - profileManagementController.updateAudioLanguage( - profileId, - AudioLanguage.ENGLISH_AUDIO_LANGUAGE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE - is AsyncResult.Failure -> - oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: English", it.error) - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel().getAudioLanguage(AudioLanguage.HINDI_AUDIO_LANGUAGE) -> { - profileManagementController.updateAudioLanguage( - profileId, - AudioLanguage.HINDI_AUDIO_LANGUAGE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> audioLanguage = AudioLanguage.HINDI_AUDIO_LANGUAGE - is AsyncResult.Failure -> - oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: Hindi", it.error) - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel().getAudioLanguage(AudioLanguage.CHINESE_AUDIO_LANGUAGE) -> { - profileManagementController.updateAudioLanguage( - profileId, - AudioLanguage.CHINESE_AUDIO_LANGUAGE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> audioLanguage = AudioLanguage.CHINESE_AUDIO_LANGUAGE - is AsyncResult.Failure -> - oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: Chinese", it.error) - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel().getAudioLanguage(AudioLanguage.FRENCH_AUDIO_LANGUAGE) -> { - profileManagementController.updateAudioLanguage( - profileId, - AudioLanguage.FRENCH_AUDIO_LANGUAGE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> audioLanguage = AudioLanguage.FRENCH_AUDIO_LANGUAGE - is AsyncResult.Failure -> - oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: French", it.error) - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) + fun updateAudioLanguage(language: AudioLanguage) { + val updateLanguageResult = profileManagementController.updateAudioLanguage(profileId, language) + updateLanguageResult.toLiveData().observe(fragment) { + when (it) { + is AsyncResult.Success -> audioLanguage = language + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: $language", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsReadingTextSizeViewModel.kt b/app/src/main/java/org/oppia/android/app/options/OptionsReadingTextSizeViewModel.kt index 39014f849de..626069bec7f 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsReadingTextSizeViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsReadingTextSizeViewModel.kt @@ -1,17 +1,29 @@ package org.oppia.android.app.options import androidx.databinding.ObservableField +import org.oppia.android.R +import org.oppia.android.app.model.ReadingTextSize +import org.oppia.android.app.translation.AppLanguageResourceHandler /** ReadingTextSize settings view model for the recycler view in [OptionsFragment]. */ class OptionsReadingTextSizeViewModel( private val routeToReadingTextSizeListener: RouteToReadingTextSizeListener, - private val loadReadingTextSizeListener: LoadReadingTextSizeListener + private val loadReadingTextSizeListener: LoadReadingTextSizeListener, + private val resourceHandler: AppLanguageResourceHandler ) : OptionsItemViewModel() { - val readingTextSize = ObservableField("") - - fun setReadingTextSize(readingTextSizeValue: String) { - readingTextSize.set(readingTextSizeValue) - } + val readingTextSize = ObservableField(ReadingTextSize.TEXT_SIZE_UNSPECIFIED) + val textSizeName: String + get() { + return when (readingTextSize.get()!!) { + ReadingTextSize.SMALL_TEXT_SIZE -> + resourceHandler.getStringInLocale(R.string.reading_text_size_small) + ReadingTextSize.MEDIUM_TEXT_SIZE -> + resourceHandler.getStringInLocale(R.string.reading_text_size_medium) + ReadingTextSize.LARGE_TEXT_SIZE -> + resourceHandler.getStringInLocale(R.string.reading_text_size_large) + else -> resourceHandler.getStringInLocale(R.string.reading_text_size_extra_large) + } + } fun loadReadingTextSizeFragment() { loadReadingTextSizeListener.loadReadingTextSizeFragment(readingTextSize.get()!!) @@ -21,7 +33,7 @@ class OptionsReadingTextSizeViewModel( if (isMultipane.get()!!) { loadReadingTextSizeListener.loadReadingTextSizeFragment(readingTextSize.get()!!) } else { - routeToReadingTextSizeListener.routeReadingTextSize(readingTextSize.get()) + routeToReadingTextSizeListener.routeReadingTextSize(readingTextSize.get()!!) } } } 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 ada56a940b2..c209f4856d6 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 @@ -5,8 +5,21 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ReadingTextSize +import org.oppia.android.app.model.ReadingTextSizeActivityParams +import org.oppia.android.app.model.ReadingTextSizeActivityResultBundle +import org.oppia.android.app.model.ReadingTextSizeActivityStateBundle +import org.oppia.android.app.model.ScreenName.READING_TEXT_SIZE_ACTIVITY +import org.oppia.android.util.extensions.getProto +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.extensions.putProto +import org.oppia.android.util.extensions.putProtoExtra +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject +private const val ACTIVITY_PARAMS_KEY = "ReadingTextSizeActivity.params" +private const val ACTIVITY_SAVED_STATE_KEY = "ReadingTextSizeActivity.saved_state" + /** The activity to change the text size of the reading content in the app. */ class ReadingTextSizeActivity : InjectableAppCompatActivity() { @@ -16,54 +29,54 @@ class ReadingTextSizeActivity : InjectableAppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) - val prefSummaryValue = ( - if (savedInstanceState != null) { - savedInstanceState.get(KEY_READING_TEXT_SIZE_PREFERENCE_SUMMARY_VALUE) - } else { - intent.getStringExtra(KEY_READING_TEXT_SIZE_PREFERENCE_SUMMARY_VALUE) - } - ) as String - readingTextSizeActivityPresenter.handleOnCreate(prefSummaryValue) + + val readingTextSize = + savedInstanceState?.retrieveStateBundle()?.selectedReadingTextSize + ?: retrieveActivityParams().readingTextSize + readingTextSizeActivityPresenter.handleOnCreate(readingTextSize) } companion object { - internal const val KEY_READING_TEXT_SIZE_PREFERENCE_TITLE = "READING_TEXT_SIZE_PREFERENCE" - const val KEY_READING_TEXT_SIZE_PREFERENCE_SUMMARY_VALUE = - "READING_TEXT_SIZE_PREFERENCE_SUMMARY_VALUE" /** Returns a new [Intent] to route to [ReadingTextSizeActivity]. */ fun createReadingTextSizeActivityIntent( context: Context, - prefKey: String, - summaryValue: String? + initialReadingTextSize: ReadingTextSize ): Intent { - val intent = Intent(context, ReadingTextSizeActivity::class.java) - intent.putExtra(KEY_READING_TEXT_SIZE_PREFERENCE_TITLE, prefKey) - intent.putExtra(KEY_READING_TEXT_SIZE_PREFERENCE_SUMMARY_VALUE, summaryValue) - return intent - } - - fun getKeyReadingTextSizePreferenceTitle(): String { - return KEY_READING_TEXT_SIZE_PREFERENCE_TITLE - } - - fun getKeyReadingTextSizePreferenceSummaryValue(): String { - return KEY_READING_TEXT_SIZE_PREFERENCE_SUMMARY_VALUE + val params = ReadingTextSizeActivityParams.newBuilder().apply { + readingTextSize = initialReadingTextSize + }.build() + return Intent(context, ReadingTextSizeActivity::class.java).apply { + putProtoExtra(ACTIVITY_PARAMS_KEY, params) + decorateWithScreenName(READING_TEXT_SIZE_ACTIVITY) + } } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putString( - KEY_READING_TEXT_SIZE_PREFERENCE_SUMMARY_VALUE, - readingTextSizeActivityPresenter.getSelectedReadingTextSize() - ) + val stateBundle = ReadingTextSizeActivityStateBundle.newBuilder().apply { + selectedReadingTextSize = readingTextSizeActivityPresenter.getSelectedReadingTextSize() + }.build() + outState.putProto(ACTIVITY_SAVED_STATE_KEY, stateBundle) } override fun onBackPressed() { - val message = readingTextSizeActivityPresenter.getSelectedReadingTextSize() - val intent = Intent() - intent.putExtra(MESSAGE_READING_TEXT_SIZE_ARGUMENT_KEY, message) + val resultBundle = ReadingTextSizeActivityResultBundle.newBuilder().apply { + selectedReadingTextSize = readingTextSizeActivityPresenter.getSelectedReadingTextSize() + }.build() + val intent = Intent().apply { + putProtoExtra(MESSAGE_READING_TEXT_SIZE_RESULTS_KEY, resultBundle) + } setResult(REQUEST_CODE_TEXT_SIZE, intent) finish() } + + private fun retrieveActivityParams() = + intent.getProtoExtra(ACTIVITY_PARAMS_KEY, ReadingTextSizeActivityParams.getDefaultInstance()) + + private fun Bundle.retrieveStateBundle(): ReadingTextSizeActivityStateBundle { + return getProto( + ACTIVITY_SAVED_STATE_KEY, ReadingTextSizeActivityStateBundle.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 8039a711f2e..3ab661a3ced 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 @@ -4,6 +4,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.app.model.ReadingTextSize import javax.inject.Inject /** The presenter for [ReadingTextSizeActivity]. */ @@ -11,14 +12,14 @@ import javax.inject.Inject class ReadingTextSizeActivityPresenter @Inject constructor( private val activity: AppCompatActivity ) { - private lateinit var fontSize: String + private lateinit var fontSize: ReadingTextSize - fun handleOnCreate(prefSummaryValue: String) { + fun handleOnCreate(preferredTextSize: ReadingTextSize) { activity.setContentView(R.layout.reading_text_size_activity) setToolbar() - fontSize = prefSummaryValue + fontSize = preferredTextSize if (getReadingTextSizeFragment() == null) { - val readingTextSizeFragment = ReadingTextSizeFragment.newInstance(prefSummaryValue) + val readingTextSizeFragment = ReadingTextSizeFragment.newInstance(preferredTextSize) activity.supportFragmentManager.beginTransaction() .add(R.id.reading_text_size_container, readingTextSizeFragment).commitNow() } @@ -31,11 +32,11 @@ class ReadingTextSizeActivityPresenter @Inject constructor( } } - fun setSelectedReadingTextSize(fontSize: String) { + fun setSelectedReadingTextSize(fontSize: ReadingTextSize) { this.fontSize = fontSize } - fun getSelectedReadingTextSize(): String { + fun getSelectedReadingTextSize(): ReadingTextSize { return fontSize } 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 3547a7362e8..1cdae8c8578 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 @@ -7,12 +7,15 @@ import android.view.View import android.view.ViewGroup import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment +import org.oppia.android.app.model.ReadingTextSize +import org.oppia.android.app.model.ReadingTextSizeFragmentArguments +import org.oppia.android.app.model.ReadingTextSizeFragmentStateBundle +import org.oppia.android.util.extensions.getProto +import org.oppia.android.util.extensions.putProto import javax.inject.Inject -private const val READING_TEXT_SIZE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY = - "ReadingTextSizeFragment.reading_text_size_preference_summary_value" -private const val SELECTED_READING_TEXT_SIZE_SAVED_KEY = - "ReadingTextSizeFragment.selected_text_size" +private const val FRAGMENT_ARGUMENTS_KEY = "ReadingTextSizeFragment.arguments" +private const val FRAGMENT_SAVED_STATE_KEY = "ReadingTextSizeFragment.saved_state" /** The fragment to change the text size of the reading content in the app. */ class ReadingTextSizeFragment : InjectableFragment(), TextSizeRadioButtonListener { @@ -20,12 +23,15 @@ class ReadingTextSizeFragment : InjectableFragment(), TextSizeRadioButtonListene lateinit var readingTextSizeFragmentPresenter: ReadingTextSizeFragmentPresenter companion object { - fun newInstance(readingTextSize: String): ReadingTextSizeFragment { - val args = Bundle() - args.putString(READING_TEXT_SIZE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY, readingTextSize) - val fragment = ReadingTextSizeFragment() - fragment.arguments = args - return fragment + fun newInstance(readingTextSize: ReadingTextSize): ReadingTextSizeFragment { + val protoArguments = ReadingTextSizeFragmentArguments.newBuilder().apply { + this.readingTextSize = readingTextSize + }.build() + return ReadingTextSizeFragment().apply { + arguments = Bundle().apply { + putProto(FRAGMENT_ARGUMENTS_KEY, protoArguments) + } + } } } @@ -39,25 +45,33 @@ class ReadingTextSizeFragment : InjectableFragment(), TextSizeRadioButtonListene container: ViewGroup?, savedInstanceState: Bundle? ): View? { - val args = - checkNotNull(arguments) { "Expected arguments to be passed to ReadingTextSizeFragment" } - val readingTextSize = if (savedInstanceState == null) { - args.get(READING_TEXT_SIZE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY) as String - } else { - savedInstanceState.get(SELECTED_READING_TEXT_SIZE_SAVED_KEY) as String - } + val readingTextSize = + savedInstanceState?.retrieveStateBundle()?.selectedReadingTextSize + ?: retrieveFragmentArguments().readingTextSize return readingTextSizeFragmentPresenter.handleOnCreateView(inflater, container, readingTextSize) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putString( - SELECTED_READING_TEXT_SIZE_SAVED_KEY, - readingTextSizeFragmentPresenter.getTextSizeSelected() - ) + val stateBundle = ReadingTextSizeFragmentStateBundle.newBuilder().apply { + selectedReadingTextSize = readingTextSizeFragmentPresenter.getTextSizeSelected() + }.build() + outState.putProto(FRAGMENT_SAVED_STATE_KEY, stateBundle) } - override fun onTextSizeSelected(selectedTextSize: String) { + override fun onTextSizeSelected(selectedTextSize: ReadingTextSize) { readingTextSizeFragmentPresenter.onTextSizeSelected(selectedTextSize) } + + private fun retrieveFragmentArguments(): ReadingTextSizeFragmentArguments { + return checkNotNull(arguments) { + "Expected arguments to be passed to ReadingTextSizeFragment" + }.getProto(FRAGMENT_ARGUMENTS_KEY, ReadingTextSizeFragmentArguments.getDefaultInstance()) + } + + private fun Bundle.retrieveStateBundle(): ReadingTextSizeFragmentStateBundle { + return getProto( + FRAGMENT_SAVED_STATE_KEY, ReadingTextSizeFragmentStateBundle.getDefaultInstance() + ) + } } diff --git a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragmentPresenter.kt index 79fb3be6870..1331a2577c3 100644 --- a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragmentPresenter.kt @@ -4,9 +4,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import org.oppia.android.R +import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.recyclerview.BindableAdapter -import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.databinding.ReadingTextSizeFragmentBinding import org.oppia.android.databinding.TextSizeItemsBinding import javax.inject.Inject @@ -14,17 +13,12 @@ import javax.inject.Inject /** The presenter for [ReadingTextSizeFragment]. */ class ReadingTextSizeFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val readingTextSizeSelectionViewModel: ReadingTextSizeSelectionViewModel, - resourceHandler: AppLanguageResourceHandler + private val readingTextSizeSelectionViewModel: ReadingTextSizeSelectionViewModel ) { - private var fontSize: String = resourceHandler.getStringInLocale( - R.string.reading_text_size_medium - ) - fun handleOnCreateView( inflater: LayoutInflater, container: ViewGroup?, - readingTextSize: String + readingTextSize: ReadingTextSize ): View? { val binding = ReadingTextSizeFragmentBinding.inflate( inflater, @@ -32,20 +26,17 @@ class ReadingTextSizeFragmentPresenter @Inject constructor( /* attachToRoot= */ false ) - fontSize = readingTextSize - updateTextSize(fontSize) + updateTextSize(readingTextSize) binding.viewModel = readingTextSizeSelectionViewModel - readingTextSizeSelectionViewModel.selectedTextSize.value = fontSize + readingTextSizeSelectionViewModel.selectedTextSize = readingTextSize binding.textSizeRecyclerView.apply { adapter = createRecyclerViewAdapter() } return binding.root } - fun getTextSizeSelected(): String? { - return readingTextSizeSelectionViewModel.selectedTextSize.value - } + fun getTextSizeSelected(): ReadingTextSize? = readingTextSizeSelectionViewModel.selectedTextSize private fun createRecyclerViewAdapter(): BindableAdapter { return BindableAdapter.SingleTypeBuilder @@ -57,7 +48,7 @@ class ReadingTextSizeFragmentPresenter @Inject constructor( ).build() } - private fun updateTextSize(textSize: String) { + private fun updateTextSize(textSize: ReadingTextSize) { // The first branch of (when) will be used in the case of multipane when (val parentActivity = fragment.activity) { is OptionsActivity -> parentActivity.optionActivityPresenter.updateReadingTextSize(textSize) @@ -66,8 +57,8 @@ class ReadingTextSizeFragmentPresenter @Inject constructor( } } - fun onTextSizeSelected(selectedTextSize: String) { - readingTextSizeSelectionViewModel.selectedTextSize.value = selectedTextSize + fun onTextSizeSelected(selectedTextSize: ReadingTextSize) { + readingTextSizeSelectionViewModel.selectedTextSize = selectedTextSize updateTextSize(selectedTextSize) } } diff --git a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeSelectionViewModel.kt b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeSelectionViewModel.kt index fb8ea3fac19..a18fceedc3c 100644 --- a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeSelectionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeSelectionViewModel.kt @@ -12,45 +12,46 @@ import javax.inject.Inject @FragmentScope class ReadingTextSizeSelectionViewModel @Inject constructor( fragment: Fragment, - private val resourceHandler: AppLanguageResourceHandler + resourceHandler: AppLanguageResourceHandler ) : ObservableViewModel() { private val resourceBundle = fragment.requireContext().resources - val selectedTextSize = MutableLiveData() + private val changeableSelectedTextSize = MutableLiveData() private val textSizeRadioButtonListener = fragment as TextSizeRadioButtonListener + var selectedTextSize: ReadingTextSize? + get() = changeableSelectedTextSize.value + set(value) { changeableSelectedTextSize.value = value } - private val textSizeList = listOf( + private val textSizeList = listOf( TextSizeItemViewModel( resourceBundle, ReadingTextSize.SMALL_TEXT_SIZE, - selectedTextSize, + changeableSelectedTextSize, textSizeRadioButtonListener, resourceHandler ), TextSizeItemViewModel( resourceBundle, ReadingTextSize.MEDIUM_TEXT_SIZE, - selectedTextSize, + changeableSelectedTextSize, textSizeRadioButtonListener, resourceHandler ), TextSizeItemViewModel( resourceBundle, ReadingTextSize.LARGE_TEXT_SIZE, - selectedTextSize, + changeableSelectedTextSize, textSizeRadioButtonListener, resourceHandler ), TextSizeItemViewModel( resourceBundle, ReadingTextSize.EXTRA_LARGE_TEXT_SIZE, - selectedTextSize, + changeableSelectedTextSize, textSizeRadioButtonListener, resourceHandler ), ) - val recyclerViewTextSizeList: List by lazy { - textSizeList - } + val recyclerViewTextSizeList: List by lazy { textSizeList } } diff --git a/app/src/main/java/org/oppia/android/app/options/RouteToAudioLanguageListListener.kt b/app/src/main/java/org/oppia/android/app/options/RouteToAudioLanguageListListener.kt index 9ccf2ac035d..363fa9588d6 100644 --- a/app/src/main/java/org/oppia/android/app/options/RouteToAudioLanguageListListener.kt +++ b/app/src/main/java/org/oppia/android/app/options/RouteToAudioLanguageListListener.kt @@ -1,6 +1,12 @@ package org.oppia.android.app.options +import org.oppia.android.app.model.AudioLanguage + /** Listener for when an activity should route to a [AudioLanguageActivity]. */ interface RouteToAudioLanguageListListener { - fun routeAudioLanguageList(audioLanguage: String?) + /** + * Called when the user wishes to change their default audio language (where [audioLanguage] is + * the current default language). + */ + fun routeAudioLanguageList(audioLanguage: AudioLanguage) } diff --git a/app/src/main/java/org/oppia/android/app/options/RouteToReadingTextSizeListener.kt b/app/src/main/java/org/oppia/android/app/options/RouteToReadingTextSizeListener.kt index c1ad1c9db86..58d3c8a7902 100644 --- a/app/src/main/java/org/oppia/android/app/options/RouteToReadingTextSizeListener.kt +++ b/app/src/main/java/org/oppia/android/app/options/RouteToReadingTextSizeListener.kt @@ -1,6 +1,12 @@ package org.oppia.android.app.options +import org.oppia.android.app.model.ReadingTextSize + /** Listener for when an activity should route to a [ReadingTextSizeActivity]. */ interface RouteToReadingTextSizeListener { - fun routeReadingTextSize(readingTextSize: String?) + /** + * Loads a standalone UI for changing the current UI reading text size (with the current text size + * being passed in via [readingTextSize]). + */ + fun routeReadingTextSize(readingTextSize: ReadingTextSize) } diff --git a/app/src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt b/app/src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt index a8e240609c8..1b3fda584af 100644 --- a/app/src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt @@ -15,9 +15,9 @@ private const val EXTRA_LARGE_TEXT_SIZE_SCALE = 1.4f /** Text Size item view model for the recycler view in [ReadingTextSizeFragment]. */ class TextSizeItemViewModel( - val resources: Resources, + private val resources: Resources, val readingTextSize: ReadingTextSize, - private val selectedTextSize: LiveData, + private val selectedTextSize: LiveData, val textSizeRadioButtonListener: TextSizeRadioButtonListener, private val resourceHandler: AppLanguageResourceHandler ) : ObservableViewModel() { @@ -44,6 +44,6 @@ class TextSizeItemViewModel( } } val isTextSizeSelected: LiveData by lazy { - Transformations.map(selectedTextSize) { it == textSizeName } + Transformations.map(selectedTextSize) { it == readingTextSize } } } diff --git a/app/src/main/java/org/oppia/android/app/options/TextSizeRadioButtonListener.kt b/app/src/main/java/org/oppia/android/app/options/TextSizeRadioButtonListener.kt index 821bcf3d78d..7f4daeaff10 100644 --- a/app/src/main/java/org/oppia/android/app/options/TextSizeRadioButtonListener.kt +++ b/app/src/main/java/org/oppia/android/app/options/TextSizeRadioButtonListener.kt @@ -1,6 +1,9 @@ package org.oppia.android.app.options +import org.oppia.android.app.model.ReadingTextSize + /** Listener for when the reading text size is selected from the [ReadingTextSizeFragment]. */ interface TextSizeRadioButtonListener { - fun onTextSizeSelected(selectedTextSize: String) + /** Called when the user selects a new [ReadingTextSize]. */ + fun onTextSizeSelected(selectedTextSize: ReadingTextSize) } diff --git a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt index 2de7bc529ad..07c50966429 100755 --- a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt @@ -133,11 +133,12 @@ class AudioFragmentPresenter @Inject constructor( /** Gets language code by [AudioLanguage]. */ private fun getAudioLanguage(audioLanguage: AudioLanguage): String { return when (audioLanguage) { - AudioLanguage.ENGLISH_AUDIO_LANGUAGE -> "en" AudioLanguage.HINDI_AUDIO_LANGUAGE -> "hi" AudioLanguage.FRENCH_AUDIO_LANGUAGE -> "fr" AudioLanguage.CHINESE_AUDIO_LANGUAGE -> "zh" - else -> "en" + AudioLanguage.BRAZILIAN_PORTUGUESE_LANGUAGE -> "pt" + AudioLanguage.NO_AUDIO, AudioLanguage.UNRECOGNIZED, AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED, + AudioLanguage.ENGLISH_AUDIO_LANGUAGE -> "en" } } diff --git a/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt b/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt index 81228fc7243..8de03ce0ca3 100644 --- a/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt @@ -1,12 +1,15 @@ package org.oppia.android.app.player.audio +import androidx.databinding.ObservableBoolean import androidx.databinding.ObservableField import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.State import org.oppia.android.app.model.Voiceover import org.oppia.android.app.model.VoiceoverMapping +import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.audio.AudioPlayerController import org.oppia.android.domain.audio.AudioPlayerController.PlayProgress @@ -14,6 +17,7 @@ import org.oppia.android.domain.audio.AudioPlayerController.PlayStatus import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.locale.OppiaLocale +import java.util.Locale import javax.inject.Inject /** [ObservableViewModel] for audio-player state. */ @@ -21,7 +25,8 @@ import javax.inject.Inject class AudioViewModel @Inject constructor( private val audioPlayerController: AudioPlayerController, @DefaultResourceBucketName private val gcsResource: String, - private val machineLocale: OppiaLocale.MachineLocale + private val machineLocale: OppiaLocale.MachineLocale, + private val resourceHandler: AppLanguageResourceHandler ) : ObservableViewModel() { private lateinit var state: State @@ -29,13 +34,15 @@ class AudioViewModel @Inject constructor( private var voiceoverMap = mapOf() private var currentContentId: String? = null private val defaultLanguage = "en" - private var languageSelectionShown = false private var autoPlay = false private var reloadingMainContent = false private var hasFeedback = false var selectedLanguageCode: String = "" + private var fallbackLanguageCode: String = defaultLanguage var languages = listOf() + var selectedLanguageUnavailable = ObservableBoolean() + var selectedLanguageName = ObservableField("") /** Mirrors PlayStatus in AudioPlayerController except adds LOADING state */ enum class UiAudioPlayStatus { @@ -90,22 +97,28 @@ class AudioViewModel @Inject constructor( voiceoverMap = voiceoverMapping.voiceoverMappingMap currentContentId = targetContentId languages = voiceoverMap.keys.toList().map { machineLocale.run { it.toMachineLowerCase() } } + selectedLanguageUnavailable.set(false) + + val localeLanguageCode = + if (selectedLanguageCode.isEmpty()) defaultLanguage else selectedLanguageCode + // TODO(#3791): Remove this dependency. + val locale = Locale(localeLanguageCode) + selectedLanguageName.set(locale.getDisplayLanguage(locale)) + when { selectedLanguageCode.isEmpty() && languages.any { it == defaultLanguage } -> setAudioLanguageCode(defaultLanguage) - languages.any { it == selectedLanguageCode } -> - setAudioLanguageCode(selectedLanguageCode) + languages.any { it == selectedLanguageCode } -> setAudioLanguageCode(selectedLanguageCode) languages.isNotEmpty() -> { autoPlay = false this.reloadingMainContent = false - languageSelectionShown = true - val languageCode = if (languages.contains("en")) { - "en" - } else { - languages.first() - } - setAudioLanguageCode(languageCode) + selectedLanguageUnavailable.set(true) + val ensuredLanguageCode = if (languages.contains("en")) "en" else languages.first() + fallbackLanguageCode = ensuredLanguageCode + audioPlayerController.changeDataSource( + voiceOverToUri(voiceoverMap[ensuredLanguageCode]), currentContentId + ) } } } @@ -132,6 +145,12 @@ class AudioViewModel @Inject constructor( fun handleSeekTo(position: Int) = audioPlayerController.seekTo(position) fun handleRelease() = audioPlayerController.releaseMediaPlayer() + fun computeAudioUnavailabilityString(languageName: String): String { + return resourceHandler.getStringInLocaleWithWrapping( + R.string.audio_unavailable_in_selected_language, languageName + ) + } + private val playProgressResultLiveData: LiveData> by lazy { audioPlayerController.initializeMediaPlayer() } diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt index 29c46c4c7bf..44f4a3eab2f 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 @@ -12,8 +12,11 @@ import org.oppia.android.app.hintsandsolution.HintsAndSolutionDialogFragment import org.oppia.android.app.hintsandsolution.HintsAndSolutionListener import org.oppia.android.app.hintsandsolution.RevealHintListener import org.oppia.android.app.hintsandsolution.RevealSolutionInterface +import org.oppia.android.app.model.ExplorationActivityParams import org.oppia.android.app.model.HelpIndex +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.ReadingTextSize +import org.oppia.android.app.model.ScreenName.EXPLORATION_ACTIVITY import org.oppia.android.app.model.State import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.audio.AudioButtonListener @@ -21,6 +24,9 @@ import org.oppia.android.app.player.state.listener.RouteToHintsAndSolutionListen import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener import org.oppia.android.app.player.stopplaying.StopStatePlayingSessionWithSavedProgressListener import org.oppia.android.app.topic.conceptcard.ConceptCardListener +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.extensions.putProtoExtra +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject const val TAG_HINTS_AND_SOLUTION_DIALOG = "HINTS_AND_SOLUTION_DIALOG" @@ -39,80 +45,64 @@ class ExplorationActivity : HintsAndSolutionExplorationManagerListener, ConceptCardListener { - @Inject - lateinit var explorationActivityPresenter: ExplorationActivityPresenter - private var internalProfileId: Int = -1 - private lateinit var topicId: String - private lateinit var storyId: String - private lateinit var explorationId: String + @Inject lateinit var explorationActivityPresenter: ExplorationActivityPresenter + private lateinit var state: State private lateinit var writtenTranslationContext: WrittenTranslationContext - private var backflowScreen: Int? = null - private var isCheckpointingEnabled: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) - internalProfileId = intent.getIntExtra(EXPLORATION_ACTIVITY_PROFILE_ID_ARGUMENT_KEY, -1) - topicId = checkNotNull(intent.getStringExtra(EXPLORATION_ACTIVITY_TOPIC_ID_ARGUMENT_KEY)) { - "Expected $EXPLORATION_ACTIVITY_TOPIC_ID_ARGUMENT_KEY to be in intent extras." - } - storyId = checkNotNull(intent.getStringExtra(EXPLORATION_ACTIVITY_STORY_ID_ARGUMENT_KEY)) { - "Expected $EXPLORATION_ACTIVITY_STORY_ID_ARGUMENT_KEY to be in intent extras." - } - explorationId = checkNotNull( - intent.getStringExtra( - EXPLORATION_ACTIVITY_EXPLORATION_ID_ARGUMENT_KEY - ) - ) { - "Expected EXPLORATION_ACTIVITY_EXPLORATION_ID_ARGUMENT_KEY to be in intent extras." - } - backflowScreen = intent.getIntExtra(EXPLORATION_ACTIVITY_BACKFLOW_SCREEN_KEY, -1) - isCheckpointingEnabled = - intent.getBooleanExtra(EXPLORATION_ACTIVITY_IS_CHECKPOINTING_ENABLED_KEY, false) + + val params = intent.getProtoExtra(PARAMS_KEY, ExplorationActivityParams.getDefaultInstance()) explorationActivityPresenter.handleOnCreate( this, - internalProfileId, - topicId, - storyId, - explorationId, - backflowScreen, - isCheckpointingEnabled + params.profileId, + params.topicId, + params.storyId, + params.explorationId, + params.parentScreen, + params.isCheckpointingEnabled ) } // TODO(#1655): Re-restrict access to fields in tests post-Gradle. companion object { - /** Returns a new [Intent] to route to [ExplorationActivity] for a specified exploration. */ - - const val EXPLORATION_ACTIVITY_PROFILE_ID_ARGUMENT_KEY = - "ExplorationActivity.profile_id" - const val EXPLORATION_ACTIVITY_TOPIC_ID_ARGUMENT_KEY = "ExplorationActivity.topic_id" - const val EXPLORATION_ACTIVITY_STORY_ID_ARGUMENT_KEY = "ExplorationActivity.story_id" - const val EXPLORATION_ACTIVITY_EXPLORATION_ID_ARGUMENT_KEY = - "ExplorationActivity.exploration_id" - const val EXPLORATION_ACTIVITY_BACKFLOW_SCREEN_KEY = - "ExplorationActivity.backflow_screen" - const val EXPLORATION_ACTIVITY_IS_CHECKPOINTING_ENABLED_KEY = - "ExplorationActivity.is_checkpointing_enabled_key" + private const val PARAMS_KEY = "ExplorationActivity.params" + /** + * A convenience function for creating a new [ExplorationActivity] intent by prefilling common + * params needed by the activity. + */ fun createExplorationActivityIntent( context: Context, - profileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, isCheckpointingEnabled: Boolean ): Intent { - val intent = Intent(context, ExplorationActivity::class.java) - intent.putExtra(EXPLORATION_ACTIVITY_PROFILE_ID_ARGUMENT_KEY, profileId) - intent.putExtra(EXPLORATION_ACTIVITY_TOPIC_ID_ARGUMENT_KEY, topicId) - intent.putExtra(EXPLORATION_ACTIVITY_STORY_ID_ARGUMENT_KEY, storyId) - intent.putExtra(EXPLORATION_ACTIVITY_EXPLORATION_ID_ARGUMENT_KEY, explorationId) - intent.putExtra(EXPLORATION_ACTIVITY_BACKFLOW_SCREEN_KEY, backflowScreen) - intent.putExtra(EXPLORATION_ACTIVITY_IS_CHECKPOINTING_ENABLED_KEY, isCheckpointingEnabled) - return intent + val params = ExplorationActivityParams.newBuilder().apply { + this.profileId = profileId + this.topicId = topicId + this.storyId = storyId + this.explorationId = explorationId + this.parentScreen = parentScreen + this.isCheckpointingEnabled = isCheckpointingEnabled + }.build() + return createExplorationActivityIntent(context, params) + } + + /** Returns a new [Intent] open an [ExplorationActivity] with the specified [params]. */ + fun createExplorationActivityIntent( + context: Context, + params: ExplorationActivityParams + ): Intent { + return Intent(context, ExplorationActivity::class.java).apply { + putProtoExtra(PARAMS_KEY, params) + decorateWithScreenName(EXPLORATION_ACTIVITY) + } } } 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 23f64e84d87..fd8126dc548 100755 --- 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 @@ -1,7 +1,6 @@ package org.oppia.android.app.player.exploration import android.content.Context -import android.os.Bundle import android.view.MenuItem import android.view.inputmethod.EditorInfo import android.widget.TextView @@ -15,7 +14,8 @@ import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.help.HelpActivity import org.oppia.android.app.model.CheckpointState -import org.oppia.android.app.model.Exploration +import org.oppia.android.app.model.EphemeralExploration +import org.oppia.android.app.model.ExplorationActivityParams import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.options.OptionsActivity @@ -27,6 +27,7 @@ import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ExplorationActivityBinding import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -45,38 +46,34 @@ class ExplorationActivityPresenter @Inject constructor( private val explorationDataController: ExplorationDataController, private val viewModelProvider: ViewModelProvider, private val fontScaleConfigurationUtil: FontScaleConfigurationUtil, + private val translationController: TranslationController, private val oppiaLogger: OppiaLogger ) { private lateinit var explorationToolbar: Toolbar private lateinit var explorationToolbarTitle: TextView - private var internalProfileId: Int = -1 + private lateinit var profileId: ProfileId private lateinit var topicId: String private lateinit var storyId: String private lateinit var explorationId: String private lateinit var context: Context - private var backflowScreen: Int? = null + private lateinit var parentScreen: ExplorationActivityParams.ParentScreen private var isCheckpointingEnabled: Boolean = false private lateinit var oldestCheckpointExplorationId: String private lateinit var oldestCheckpointExplorationTitle: String - enum class ParentActivityForExploration(val value: Int) { - BACKFLOW_SCREEN_LESSONS(0), - BACKFLOW_SCREEN_STORY(1); - } - private val exploreViewModel by lazy { getExplorationViewModel() } fun handleOnCreate( context: Context, - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, isCheckpointingEnabled: Boolean ) { val binding = DataBindingUtil.setContentView( @@ -104,29 +101,22 @@ class ExplorationActivityPresenter @Inject constructor( getExplorationFragment()?.handlePlayAudio() } - updateToolbarTitle(explorationId) - this.internalProfileId = internalProfileId + this.profileId = profileId this.topicId = topicId this.storyId = storyId this.explorationId = explorationId this.context = context - this.backflowScreen = backflowScreen + this.parentScreen = parentScreen this.isCheckpointingEnabled = isCheckpointingEnabled + updateToolbarTitle(explorationId) // Retrieve oldest saved checkpoint details. subscribeToOldestSavedExplorationDetails() if (getExplorationManagerFragment() == null) { - val explorationManagerFragment = ExplorationManagerFragment() - val args = Bundle() - args.putInt( - ExplorationActivity.EXPLORATION_ACTIVITY_PROFILE_ID_ARGUMENT_KEY, - internalProfileId - ) - explorationManagerFragment.arguments = args activity.supportFragmentManager.beginTransaction().add( R.id.exploration_fragment_placeholder, - explorationManagerFragment, + ExplorationManagerFragment.createNewInstance(profileId), TAG_EXPLORATION_MANAGER_FRAGMENT ).commitNow() } @@ -137,11 +127,7 @@ class ExplorationActivityPresenter @Inject constructor( activity.supportFragmentManager.beginTransaction().add( R.id.exploration_fragment_placeholder, ExplorationFragment.newInstance( - topicId = topicId, - internalProfileId = internalProfileId, - storyId = storyId, - readingTextSize = readingTextSize.name, - explorationId = explorationId + profileId, topicId, storyId, explorationId, readingTextSize ), TAG_EXPLORATION_FRAGMENT ).commitNow() @@ -162,19 +148,19 @@ class ExplorationActivityPresenter @Inject constructor( R.id.action_preferences -> { val intent = OptionsActivity.createOptionsActivity( activity, - internalProfileId, + profileId.internalId, /* isFromNavigationDrawer= */ false ) - fontScaleConfigurationUtil.adjustFontScale(activity, ReadingTextSize.MEDIUM_TEXT_SIZE.name) + fontScaleConfigurationUtil.adjustFontScale(activity, ReadingTextSize.MEDIUM_TEXT_SIZE) context.startActivity(intent) true } R.id.action_help -> { val intent = HelpActivity.createHelpActivityIntent( - activity, internalProfileId, + activity, profileId.internalId, /* isFromNavigationDrawer= */false ) - fontScaleConfigurationUtil.adjustFontScale(activity, ReadingTextSize.MEDIUM_TEXT_SIZE.name) + fontScaleConfigurationUtil.adjustFontScale(activity, ReadingTextSize.MEDIUM_TEXT_SIZE) context.startActivity(intent) true } @@ -215,10 +201,7 @@ class ExplorationActivityPresenter @Inject constructor( /** Deletes the saved progress for the current exploration and then stops the exploration. */ fun deleteCurrentProgressAndStopExploration(isCompletion: Boolean) { - explorationDataController.deleteExplorationProgressById( - ProfileId.newBuilder().setInternalId(internalProfileId).build(), - explorationId - ) + explorationDataController.deleteExplorationProgressById(profileId, explorationId) stopExploration(isCompletion) } @@ -229,15 +212,14 @@ class ExplorationActivityPresenter @Inject constructor( // without deleting the any checkpoints. oldestCheckpointExplorationId.let { explorationDataController.deleteExplorationProgressById( - ProfileId.newBuilder().setInternalId(internalProfileId).build(), - oldestCheckpointExplorationId + profileId, oldestCheckpointExplorationId ) } stopExploration(isCompletion = false) } fun stopExploration(isCompletion: Boolean) { - fontScaleConfigurationUtil.adjustFontScale(activity, ReadingTextSize.MEDIUM_TEXT_SIZE.name) + fontScaleConfigurationUtil.adjustFontScale(activity, ReadingTextSize.MEDIUM_TEXT_SIZE) explorationDataController.stopPlayingExploration(isCompletion).toLiveData() .observe( activity, @@ -248,7 +230,7 @@ class ExplorationActivityPresenter @Inject constructor( oppiaLogger.e("ExplorationActivity", "Failed to stop exploration", it.error) is AsyncResult.Success -> { oppiaLogger.d("ExplorationActivity", "Successfully stopped exploration") - backPressActivitySelector(backflowScreen) + backPressActivitySelector() (activity as ExplorationActivity).finish() } } @@ -289,19 +271,21 @@ class ExplorationActivityPresenter @Inject constructor( } private fun updateToolbarTitle(explorationId: String) { - subscribeToExploration(explorationDataController.getExplorationById(explorationId).toLiveData()) + subscribeToExploration( + explorationDataController.getExplorationById(profileId, explorationId).toLiveData() + ) } private fun subscribeToExploration( - explorationResultLiveData: LiveData> + explorationResultLiveData: LiveData> ) { - val explorationLiveData = getExploration(explorationResultLiveData) - explorationLiveData.observe( - activity, - Observer { - explorationToolbarTitle.text = it.title - } - ) + val explorationLiveData = getEphemeralExploration(explorationResultLiveData) + explorationLiveData.observe(activity) { + explorationToolbarTitle.text = + translationController.extractString( + it.exploration.translatableTitle, it.writtenTranslationContext + ) + } } private fun getExplorationViewModel(): ExplorationViewModel { @@ -309,37 +293,39 @@ class ExplorationActivityPresenter @Inject constructor( } /** Helper for subscribeToExploration. */ - private fun getExploration( - exploration: LiveData> - ): LiveData { - return Transformations.map(exploration, ::processExploration) + private fun getEphemeralExploration( + exploration: LiveData> + ): LiveData { + return Transformations.map(exploration, ::processEphemeralExploration) } /** Helper for subscribeToExploration. */ - private fun processExploration(ephemeralStateResult: AsyncResult): Exploration { - return when (ephemeralStateResult) { + private fun processEphemeralExploration( + ephemeralExpResult: AsyncResult + ): EphemeralExploration { + return when (ephemeralExpResult) { is AsyncResult.Failure -> { oppiaLogger.e( - "ExplorationActivity", "Failed to retrieve answer outcome", ephemeralStateResult.error + "ExplorationActivity", "Failed to retrieve answer outcome", ephemeralExpResult.error ) - Exploration.getDefaultInstance() + EphemeralExploration.getDefaultInstance() } - is AsyncResult.Pending -> Exploration.getDefaultInstance() - is AsyncResult.Success -> ephemeralStateResult.value + is AsyncResult.Pending -> EphemeralExploration.getDefaultInstance() + is AsyncResult.Success -> ephemeralExpResult.value } } - private fun backPressActivitySelector(backflowScreen: Int?) { - when (backflowScreen) { - ParentActivityForExploration.BACKFLOW_SCREEN_STORY.value -> activity.finish() - ParentActivityForExploration.BACKFLOW_SCREEN_LESSONS.value -> activity.finish() - else -> activity.startActivity( - TopicActivity.createTopicActivityIntent( - context, - internalProfileId, - topicId + private fun backPressActivitySelector() { + when (parentScreen) { + ExplorationActivityParams.ParentScreen.TOPIC_SCREEN_LESSONS_TAB, + ExplorationActivityParams.ParentScreen.STORY_SCREEN -> activity.finish() + ExplorationActivityParams.ParentScreen.PARENT_SCREEN_UNSPECIFIED, + ExplorationActivityParams.ParentScreen.UNRECOGNIZED -> { + // Default to the topic activity. + activity.startActivity( + TopicActivity.createTopicActivityIntent(context, profileId.internalId, topicId) ) - ) + } } } @@ -415,7 +401,7 @@ class ExplorationActivityPresenter @Inject constructor( */ private fun subscribeToOldestSavedExplorationDetails() { explorationDataController.getOldestExplorationDetailsDataProvider( - ProfileId.newBuilder().setInternalId(internalProfileId).build() + profileId ).toLiveData().observe( activity, Observer { diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragment.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragment.kt index ddf88f38cd1..b72f2f0da61 100755 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragment.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragment.kt @@ -7,8 +7,10 @@ import android.view.View import android.view.ViewGroup import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment -import org.oppia.android.app.utility.FontScaleConfigurationUtil -import org.oppia.android.util.extensions.getStringFromBundle +import org.oppia.android.app.model.ExplorationFragmentArguments +import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.ReadingTextSize +import org.oppia.android.util.extensions.putProto import javax.inject.Inject /** Fragment that contains displays single exploration. */ @@ -16,81 +18,45 @@ class ExplorationFragment : InjectableFragment() { @Inject lateinit var explorationFragmentPresenter: ExplorationFragmentPresenter - @Inject - lateinit var fontScaleConfigurationUtil: FontScaleConfigurationUtil - companion object { - internal const val INTERNAL_PROFILE_ID_ARGUMENT_KEY = - "ExplorationFragment.internal_profile_id" - internal const val TOPIC_ID_ARGUMENT_KEY = "ExplorationFragment.topic_id" - internal const val STORY_ID_ARGUMENT_KEY = "ExplorationFragment.story_id" - internal const val STORY_DEFAULT_FONT_SIZE_ARGUMENT_KEY = - "ExplorationFragment.story_default_font_size" - internal const val EXPLORATION_ID_ARGUMENT_KEY = - "ExplorationFragment.exploration_id" - - /** Returns a new [ExplorationFragment] to pass the profileId, topicId, storyId, readingTextSize and explorationId. */ + /** Returns a new [ExplorationFragment] with the corresponding fragment parameters. */ fun newInstance( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, - readingTextSize: String, - explorationId: String + explorationId: String, + readingTextSize: ReadingTextSize ): ExplorationFragment { - val explorationFragment = ExplorationFragment() - val args = Bundle() - args.putInt( - INTERNAL_PROFILE_ID_ARGUMENT_KEY, - internalProfileId - ) - args.putString(TOPIC_ID_ARGUMENT_KEY, topicId) - args.putString(STORY_ID_ARGUMENT_KEY, storyId) - args.putString( - STORY_DEFAULT_FONT_SIZE_ARGUMENT_KEY, - readingTextSize - ) - args.putString( - EXPLORATION_ID_ARGUMENT_KEY, - explorationId - ) - explorationFragment.arguments = args - return explorationFragment + val args = ExplorationFragmentArguments.newBuilder().apply { + this.profileId = profileId + this.topicId = topicId + this.storyId = storyId + this.explorationId = explorationId + this.readingTextSize = readingTextSize + }.build() + return ExplorationFragment().apply { + arguments = Bundle().apply { + putProto(ExplorationFragmentPresenter.ARGUMENTS_KEY, args) + } + } } } override fun onAttach(context: Context) { super.onAttach(context) (fragmentComponent as FragmentComponentImpl).inject(this) - val readingTextSize = - arguments!!.getStringFromBundle(STORY_DEFAULT_FONT_SIZE_ARGUMENT_KEY) - checkNotNull(readingTextSize) { "ExplorationFragment must be created with a reading text size" } - fontScaleConfigurationUtil.adjustFontScale(context, readingTextSize) + explorationFragmentPresenter.handleAttach(context) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - val profileId = - arguments!!.getInt(INTERNAL_PROFILE_ID_ARGUMENT_KEY, -1) - val topicId = - arguments!!.getStringFromBundle(TOPIC_ID_ARGUMENT_KEY) - checkNotNull(topicId) { "StateFragment must be created with an topic ID" } - val storyId = - arguments!!.getStringFromBundle(STORY_ID_ARGUMENT_KEY) - checkNotNull(storyId) { "StateFragment must be created with an story ID" } - val explorationId = - arguments!!.getStringFromBundle(EXPLORATION_ID_ARGUMENT_KEY) - checkNotNull(explorationId) { "StateFragment must be created with an exploration ID" } - return explorationFragmentPresenter.handleCreateView( - inflater, - container, - profileId, - topicId, - storyId, - explorationId - ) + ): View = explorationFragmentPresenter.handleCreateView(inflater, container) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + explorationFragmentPresenter.handleViewCreated() } fun handlePlayAudio() = explorationFragmentPresenter.handlePlayAudio() diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragmentPresenter.kt index 1085a5536d5..99f12480320 100755 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragmentPresenter.kt @@ -1,34 +1,48 @@ package org.oppia.android.app.player.exploration +import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.ExplorationFragmentArguments +import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.player.state.StateFragment +import org.oppia.android.app.utility.FontScaleConfigurationUtil import org.oppia.android.databinding.ExplorationFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import org.oppia.android.util.extensions.getProto +import org.oppia.android.util.extensions.putProto import javax.inject.Inject /** The presenter for [ExplorationFragment]. */ @FragmentScope class ExplorationFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val oppiaLogger: OppiaLogger + private val oppiaLogger: OppiaLogger, + private val fontScaleConfigurationUtil: FontScaleConfigurationUtil, + private val profileManagementController: ProfileManagementController ) { - fun handleCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - profileId: Int, - topicId: String, - storyId: String, - explorationId: String - ): View? { + /** Handles the [Fragment.onAttach] portion of [ExplorationFragment]'s lifecycle. */ + fun handleAttach(context: Context) { + fontScaleConfigurationUtil.adjustFontScale(context, retrieveArguments().readingTextSize) + } + + /** Handles the [Fragment.onCreateView] portion of [ExplorationFragment]'s lifecycle. */ + fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View { + val args = retrieveArguments() val binding = ExplorationFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false).root - val stateFragment = StateFragment.newInstance(profileId, topicId, storyId, explorationId) - logPracticeFragmentEvent(topicId, storyId, explorationId) + val stateFragment = + StateFragment.newInstance( + args.profileId.internalId, args.topicId, args.storyId, args.explorationId + ) + logPracticeFragmentEvent(args.topicId, args.storyId, args.explorationId) if (getStateFragment() == null) { fragment.childFragmentManager.beginTransaction().add( R.id.state_fragment_placeholder, @@ -38,6 +52,24 @@ class ExplorationFragmentPresenter @Inject constructor( return binding } + /** Handles the [Fragment.onViewCreated] portion of [ExplorationFragment]'s lifecycle. */ + fun handleViewCreated() { + val profileDataProvider = profileManagementController.getProfile(retrieveArguments().profileId) + profileDataProvider.toLiveData().observe( + fragment, + { result -> + val readingTextSize = retrieveArguments().readingTextSize + if (result is AsyncResult.Success && result.value.readingTextSize != readingTextSize) { + selectNewReadingTextSize(result.value.readingTextSize) + + // Since text views are based on sp for sizing, the activity needs to be recreated so that + // sp can be correctly recomputed. + fragment.requireActivity().recreate() + } + } + ) + } + fun handlePlayAudio() { getStateFragment()?.handlePlayAudio() } @@ -76,4 +108,28 @@ class ExplorationFragmentPresenter @Inject constructor( oppiaLogger.createOpenExplorationActivityContext(topicId, storyId, explorationId) ) } + + private fun selectNewReadingTextSize(readingTextSize: ReadingTextSize) { + updateArguments( + retrieveArguments().toBuilder().apply { + this.readingTextSize = readingTextSize + }.build() + ) + fontScaleConfigurationUtil.adjustFontScale(fragment.requireActivity(), readingTextSize) + } + + private fun retrieveArguments(): ExplorationFragmentArguments { + return fragment.requireArguments().getProto( + ARGUMENTS_KEY, ExplorationFragmentArguments.getDefaultInstance() + ) + } + + private fun updateArguments(updatedArgs: ExplorationFragmentArguments) { + fragment.requireArguments().putProto(ARGUMENTS_KEY, updatedArgs) + } + + companion object { + /** The fragment arguments key for all proto-held arguments for [ExplorationFragment]. */ + const val ARGUMENTS_KEY = "ExplorationFragment.arguments" + } } diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragment.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragment.kt index 5697b9eed99..a7775707373 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragment.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragment.kt @@ -4,6 +4,9 @@ import android.content.Context import android.os.Bundle import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment +import org.oppia.android.app.model.ProfileId +import org.oppia.android.util.extensions.getProto +import org.oppia.android.util.extensions.putProto import javax.inject.Inject /** @@ -21,11 +24,25 @@ class ExplorationManagerFragment : InjectableFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val internalProfileId = - arguments!!.getInt( - ExplorationActivity.EXPLORATION_ACTIVITY_PROFILE_ID_ARGUMENT_KEY, - /* defaultValue= */ -1 - ) - explorationManagerFragmentPresenter.handleCreate(internalProfileId) + val profileId = checkNotNull(arguments) { + "Expected arguments to be provided for fragment." + }.getProto(PROFILE_ID_ARGUMENT_KEY, ProfileId.getDefaultInstance()) + explorationManagerFragmentPresenter.handleCreate(profileId) + } + + companion object { + private const val PROFILE_ID_ARGUMENT_KEY = "ExplorationManagerFragment.profile_id" + + /** + * Returns a new instance of [ExplorationManagerFragment] corresponding to the specified + * [profileId]. + */ + fun createNewInstance(profileId: ProfileId): ExplorationManagerFragment { + return ExplorationManagerFragment().apply { + arguments = Bundle().apply { + putProto(PROFILE_ID_ARGUMENT_KEY, profileId) + } + } + } } } diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt index 4df78475693..86dd9d370e5 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt @@ -3,7 +3,6 @@ package org.oppia.android.app.player.exploration import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.lifecycle.Transformations import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.Profile @@ -25,11 +24,11 @@ class ExplorationManagerFragmentPresenter @Inject constructor( ) { private lateinit var profileId: ProfileId - fun handleCreate(internalProfileId: Int) { - this.profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() + fun handleCreate(profileId: ProfileId) { + this.profileId = profileId retrieveReadingTextSize().observe( fragment, - Observer { result -> + { result -> (activity as DefaultFontSizeStateListener).onDefaultFontSizeLoaded(result) } ) diff --git a/app/src/main/java/org/oppia/android/app/player/state/ConfettiConfig.kt b/app/src/main/java/org/oppia/android/app/player/state/ConfettiConfig.kt index 93f7efccb38..54f6e20bfa5 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/ConfettiConfig.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/ConfettiConfig.kt @@ -70,9 +70,9 @@ enum class ConfettiConfig( companion object { /** Primary colors to use for the confetti. */ val primaryColors: List = listOf( - R.color.confetti_red, - R.color.confetti_yellow, - R.color.confetti_blue + R.color.component_color_confetti_red_color, + R.color.component_color_confetti_yellow_color, + R.color.component_color_confetti_blue_color ) } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index f0d22cecad7..1f9a6116436 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -4,6 +4,9 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.AnimationUtils +import android.view.animation.BounceInterpolator +import android.view.animation.Interpolator import android.view.inputmethod.InputMethodManager import android.widget.TextView import androidx.appcompat.app.AppCompatActivity @@ -32,6 +35,7 @@ import org.oppia.android.app.player.state.ConfettiConfig.MINI_CONFETTI_BURST import org.oppia.android.app.player.state.listener.RouteToHintsAndSolutionListener import org.oppia.android.app.player.stopplaying.StopStatePlayingSessionWithSavedProgressListener import org.oppia.android.app.topic.conceptcard.ConceptCardFragment.Companion.CONCEPT_CARD_DIALOG_FRAGMENT_TAG +import org.oppia.android.app.utility.LifecycleSafeTimerFactory import org.oppia.android.app.utility.SplitScreenManager import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.StateFragmentBinding @@ -61,6 +65,7 @@ class StateFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val context: Context, + private val lifecycleSafeTimerFactory: LifecycleSafeTimerFactory, private val viewModelProvider: ViewModelProvider, private val explorationProgressController: ExplorationProgressController, private val storyProgressController: StoryProgressController, @@ -338,7 +343,7 @@ class StateFragmentPresenter @Inject constructor( oppiaLogger.e("StateFragment", "Failed to retrieve hint/solution", result.error) } else { // If the hint/solution, was revealed remove dot and radar. - viewModel.setHintOpenedAndUnRevealedVisibility(false) + setHintOpenedAndUnRevealed(false) } } ) @@ -459,31 +464,75 @@ class StateFragmentPresenter @Inject constructor( private fun showHintsAndSolutions(helpIndex: HelpIndex, isCurrentStatePendingState: Boolean) { if (!isCurrentStatePendingState) { // If current state is not the pending top state, hide the hint bulb. - viewModel.setHintOpenedAndUnRevealedVisibility(false) + setHintOpenedAndUnRevealed(false) viewModel.setHintBulbVisibility(false) } else { when (helpIndex.indexTypeCase) { HelpIndex.IndexTypeCase.NEXT_AVAILABLE_HINT_INDEX -> { viewModel.setHintBulbVisibility(true) - viewModel.setHintOpenedAndUnRevealedVisibility(true) + setHintOpenedAndUnRevealed(true) } HelpIndex.IndexTypeCase.LATEST_REVEALED_HINT_INDEX -> { viewModel.setHintBulbVisibility(true) - viewModel.setHintOpenedAndUnRevealedVisibility(false) + setHintOpenedAndUnRevealed(false) } HelpIndex.IndexTypeCase.SHOW_SOLUTION -> { viewModel.setHintBulbVisibility(true) - viewModel.setHintOpenedAndUnRevealedVisibility(true) + setHintOpenedAndUnRevealed(true) } HelpIndex.IndexTypeCase.EVERYTHING_REVEALED -> { - viewModel.setHintOpenedAndUnRevealedVisibility(false) + setHintOpenedAndUnRevealed(false) viewModel.setHintBulbVisibility(true) } else -> { - viewModel.setHintOpenedAndUnRevealedVisibility(false) + setHintOpenedAndUnRevealed(false) viewModel.setHintBulbVisibility(false) } } } } + + private fun setHintOpenedAndUnRevealed(isHintUnrevealed: Boolean) { + viewModel.setHintOpenedAndUnRevealedVisibility(isHintUnrevealed) + if (isHintUnrevealed) { + val hintBulbAnimation = AnimationUtils.loadAnimation( + context, + R.anim.hint_bulb_animation + ).also { it.interpolator = BounceUpAndDownInterpolator() } + + // The bulb should start bouncing every 30 seconds. Note that an initial delay is used for + // cases like configuration changes, or returning from a saved checkpoint. + lifecycleSafeTimerFactory.run { + activity.runPeriodically(delayMillis = 5_000, periodMillis = 30_000) { + return@runPeriodically viewModel.isHintOpenedAndUnRevealed.get()!!.also { playAnim -> + if (playAnim) binding.hintBulb.startAnimation(hintBulbAnimation) + } + } + } + } else { + binding.hintBulb.clearAnimation() + } + } + + /** + * An [Interpolator] when performs a reversed, then regular bounce interpolation using + * [BounceInterpolator]. + * + * This interpolator maps input time from [0, 0.5] to [1.0, 0.0] and (0.5, 1.0] to (0.0, 1.0], + * allowing a clean continuous reverse bounce animation such that the item being bounced returns + * to its original position (which is expected to be the "final" transformation value). Note the + * start and end of the same time values for output--interpolators in Android normally don't allow + * this which is why modeling this animation behavior any other way is particularly challenging. + */ + private class BounceUpAndDownInterpolator : Interpolator { + private val bounceInterpolator by lazy { BounceInterpolator() } + + override fun getInterpolation(input: Float): Float { + // To get the correct continuous bounce, run the reverse bounce from 100% to 0% for the first + // 50% of time, then run the regular bounce from 0% to 100% for the remaining 50%. + return if (input <= 0.5f) { + bounceInterpolator.getInterpolation(1f - input * 2f) + } else bounceInterpolator.getInterpolation(input * 2f - 1f) + } + } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt index 714335ff4d6..58453675b35 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt @@ -937,7 +937,8 @@ class StatePlayerRecyclerViewAssembler private constructor( entityType, contentViewModel.gcsEntityId, imageCenterAlign = true, - customOppiaTagActionListener = customTagListener + customOppiaTagActionListener = customTagListener, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( contentViewModel.htmlContent.toString(), binding.contentTextView, @@ -971,7 +972,8 @@ class StatePlayerRecyclerViewAssembler private constructor( entityType, feedbackViewModel.gcsEntityId, imageCenterAlign = true, - customOppiaTagActionListener = customTagListener + customOppiaTagActionListener = customTagListener, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( feedbackViewModel.htmlContent.toString(), binding.feedbackTextView, @@ -1079,7 +1081,8 @@ class StatePlayerRecyclerViewAssembler private constructor( entityType, submittedAnswerViewModel.gcsEntityId, imageCenterAlign = false, - customOppiaTagActionListener = customTagListener + customOppiaTagActionListener = customTagListener, + displayLocale = resourceHandler.getDisplayLocale() ) submittedAnswerViewModel.setSubmittedAnswer( htmlParser.parseOppiaHtml( @@ -1154,7 +1157,8 @@ class StatePlayerRecyclerViewAssembler private constructor( entityType, gcsEntityId, imageCenterAlign = false, - customOppiaTagActionListener = customTagListener + customOppiaTagActionListener = customTagListener, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( viewModel, binding.submittedAnswerContentTextView, diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt index 4afebaccc72..9395c23cdb7 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt @@ -4,6 +4,7 @@ import android.text.Editable import android.text.TextWatcher import androidx.databinding.Observable import androidx.databinding.ObservableField +import org.oppia.android.R import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.UserAnswer @@ -11,6 +12,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver +import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.translation.TranslationController import javax.inject.Inject @@ -21,6 +23,7 @@ class TextInputViewModel private constructor( private val interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length val isSplitView: Boolean, private val writtenTranslationContext: WrittenTranslationContext, + private val resourceHandler: AppLanguageResourceHandler, private val translationController: TranslationController ) : StateItemViewModel(ViewType.TEXT_INPUT_INTERACTION), InteractionAnswerHandler { var answerText: CharSequence = "" @@ -84,11 +87,16 @@ class TextInputViewModel private constructor( placeholderUnicodeOption2?.let { unicode -> translationController.extractString(unicode, writtenTranslationContext) } ?: "" // The default placeholder for text input is empty. - return if (placeholder1.isNotEmpty()) placeholder1 else placeholder2 + return when { + placeholder1.isNotEmpty() -> placeholder1 + placeholder2.isNotEmpty() -> placeholder2 + else -> resourceHandler.getStringInLocale(R.string.text_input_default_hint_text) + } } /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ class FactoryImpl @Inject constructor( + private val resourceHandler: AppLanguageResourceHandler, private val translationController: TranslationController ) : InteractionItemFactory { override fun create( @@ -107,6 +115,7 @@ class TextInputViewModel private constructor( answerErrorReceiver, isSplitView, writtenTranslationContext, + resourceHandler, translationController ) } diff --git a/app/src/main/java/org/oppia/android/app/policies/PoliciesActivity.kt b/app/src/main/java/org/oppia/android/app/policies/PoliciesActivity.kt new file mode 100644 index 00000000000..fd965897285 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/policies/PoliciesActivity.kt @@ -0,0 +1,51 @@ +package org.oppia.android.app.policies + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.model.ScreenName.POLICIES_ACTIVITY +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.extensions.putProtoExtra +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName +import javax.inject.Inject + +/** Activity for displaying the app policies. */ +class PoliciesActivity : InjectableAppCompatActivity() { + + @Inject + lateinit var policiesActivityPresenter: PoliciesActivityPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + + policiesActivityPresenter.handleOnCreate( + intent.getProtoExtra( + POLICIES_ACTIVITY_POLICY_PAGE_PARAMS_PROTO, + PoliciesActivityParams.getDefaultInstance() + ) + ) + } + + companion object { + /** Argument key for policy page in [PoliciesActivity]. */ + const val POLICIES_ACTIVITY_POLICY_PAGE_PARAMS_PROTO = "PoliciesActivity.policy_page" + + /** Returns the [Intent] for opening [PoliciesActivity] for the specified [policyPage]. */ + fun createPoliciesActivityIntent(context: Context, policyPage: PolicyPage): Intent { + val policiesActivityParams = + PoliciesActivityParams + .newBuilder() + .setPolicyPage(policyPage) + .build() + return Intent(context, PoliciesActivity::class.java).also { + it.putProtoExtra(POLICIES_ACTIVITY_POLICY_PAGE_PARAMS_PROTO, policiesActivityParams) + it.decorateWithScreenName(POLICIES_ACTIVITY) + } + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/policies/PoliciesActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/policies/PoliciesActivityPresenter.kt new file mode 100644 index 00000000000..917fe24f75a --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/policies/PoliciesActivityPresenter.kt @@ -0,0 +1,64 @@ +package org.oppia.android.app.policies + +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import org.oppia.android.R +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PoliciesFragmentArguments +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.translation.AppLanguageResourceHandler +import javax.inject.Inject + +/** The presenter for [PoliciesActivity]. */ +class PoliciesActivityPresenter @Inject constructor( + private val activity: AppCompatActivity, + private val resourceHandler: AppLanguageResourceHandler +) { + + /** Handles onCreate() method of the [PoliciesActivity]. */ + fun handleOnCreate(policiesActivityParams: PoliciesActivityParams) { + activity.setContentView(R.layout.policies_activity) + val toolbar = setUpToolbar(policiesActivityParams.policyPage) + activity.supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + toolbar.setNavigationOnClickListener { + activity.finish() + } + + if (getPoliciesFragment() == null) { + val policiesFragmentArguments = + PoliciesFragmentArguments + .newBuilder() + .setPolicyPage(policiesActivityParams.policyPage) + .build() + activity.supportFragmentManager.beginTransaction().add( + R.id.policies_fragment_placeholder, + PoliciesFragment.newInstance(policiesFragmentArguments) + ).commitNow() + } + } + + private fun setUpToolbar(policyPage: PolicyPage): Toolbar { + val toolbar = activity.findViewById(R.id.policies_activity_toolbar) as Toolbar + + toolbar.title = when (policyPage) { + PolicyPage.PRIVACY_POLICY -> + resourceHandler.getStringInLocale(R.string.privacy_policy_title) + PolicyPage.TERMS_OF_SERVICE -> + resourceHandler.getStringInLocale(R.string.terms_of_service_title) + PolicyPage.POLICY_PAGE_UNSPECIFIED, + PolicyPage.UNRECOGNIZED -> "" + } + activity.setSupportActionBar(toolbar) + return toolbar + } + + private fun getPoliciesFragment(): PoliciesFragment? { + return activity + .supportFragmentManager + .findFragmentById( + R.id.policies_fragment_placeholder + ) as? PoliciesFragment + } +} diff --git a/app/src/main/java/org/oppia/android/app/policies/PoliciesFragment.kt b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragment.kt new file mode 100644 index 00000000000..06cd8bf8b67 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragment.kt @@ -0,0 +1,53 @@ +package org.oppia.android.app.policies + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableFragment +import org.oppia.android.app.model.PoliciesFragmentArguments +import org.oppia.android.util.extensions.getProto +import org.oppia.android.util.extensions.putProto +import javax.inject.Inject + +private const val POLICIES_FRAGMENT_POLICY_PAGE_ARGUMENT_PROTO = "PoliciesFragment.policy_page" + +/** Fragment that contains policies flow of the app. */ +class PoliciesFragment : InjectableFragment() { + @Inject + lateinit var policiesFragmentPresenter: PoliciesFragmentPresenter + + companion object { + /** Returns instance of [PoliciesFragment]. */ + fun newInstance(policiesFragmentArguments: PoliciesFragmentArguments): PoliciesFragment { + val args = Bundle() + args.putProto(POLICIES_FRAGMENT_POLICY_PAGE_ARGUMENT_PROTO, policiesFragmentArguments) + val fragment = PoliciesFragment() + fragment.arguments = args + return fragment + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val args = checkNotNull(arguments) { + "Expected arguments to be passed to PoliciesFragment" + } + val policies = + args.getProto( + POLICIES_FRAGMENT_POLICY_PAGE_ARGUMENT_PROTO, + PoliciesFragmentArguments.getDefaultInstance() + ) + return policiesFragmentPresenter.handleCreateView(inflater, container, policies) + } +} diff --git a/app/src/main/java/org/oppia/android/app/policies/PoliciesFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragmentPresenter.kt new file mode 100644 index 00000000000..e42e38497ae --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragmentPresenter.kt @@ -0,0 +1,83 @@ +package org.oppia.android.app.policies + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.android.R +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.PoliciesFragmentArguments +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.databinding.PoliciesFragmentBinding +import org.oppia.android.util.parser.html.HtmlParser +import javax.inject.Inject + +/** The presenter for [PoliciesFragment]. */ +@FragmentScope +class PoliciesFragmentPresenter @Inject constructor( + private val htmlParserFactory: HtmlParser.Factory, + private val resourceHandler: AppLanguageResourceHandler +) { + + /** Handles onCreate() method of the [PoliciesFragment]. */ + fun handleCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + policiesFragmentArguments: PoliciesFragmentArguments + ): View { + val binding = PoliciesFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + setUpContentForTextViews(policiesFragmentArguments.policyPage, binding) + + return binding.root + } + + private fun setUpContentForTextViews( + policyPage: PolicyPage, + binding: PoliciesFragmentBinding + ) { + var policyDescription = "" + var policyWebLink = "" + + if (policyPage == PolicyPage.PRIVACY_POLICY) { + policyDescription = + resourceHandler.getStringInLocale(R.string.privacy_policy_content) + policyWebLink = resourceHandler.getStringInLocale(R.string.privacy_policy_web_link) + } else if (policyPage == PolicyPage.TERMS_OF_SERVICE) { + policyDescription = + resourceHandler.getStringInLocale(R.string.terms_of_service_content) + policyWebLink = resourceHandler.getStringInLocale(R.string.terms_of_service_web_link) + } + + binding.policyDescriptionTextView.text = htmlParserFactory.create( + gcsResourceName = "", + entityType = "", + entityId = "", + imageCenterAlign = false, + customOppiaTagActionListener = null, + resourceHandler.getDisplayLocale() + ).parseOppiaHtml( + policyDescription, + binding.policyDescriptionTextView, + supportsLinks = true, + supportsConceptCards = false + ) + + binding.policyWebLinkTextView.text = htmlParserFactory.create( + gcsResourceName = "", + entityType = "", + entityId = "", + imageCenterAlign = false, + customOppiaTagActionListener = null, + resourceHandler.getDisplayLocale() + ).parseOppiaHtml( + policyWebLink, + binding.policyWebLinkTextView, + supportsLinks = true, + supportsConceptCards = false + ) + } +} diff --git a/app/src/main/java/org/oppia/android/app/policies/RouteToPoliciesListener.kt b/app/src/main/java/org/oppia/android/app/policies/RouteToPoliciesListener.kt new file mode 100644 index 00000000000..f3ed295ea19 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/policies/RouteToPoliciesListener.kt @@ -0,0 +1,9 @@ +package org.oppia.android.app.policies + +import org.oppia.android.app.model.PolicyPage + +/** Listener for when a selection should result in displaying a policy page (e.g. the Privacy Policy). */ +interface RouteToPoliciesListener { + /** Called when the user wants to view an app policy. */ + fun onRouteToPolicies(policyPage: PolicyPage) +} diff --git a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivity.kt b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivity.kt index 1df10e92c59..e33502df360 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivity.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.ADD_PROFILE_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject const val ADD_PROFILE_COLOR_RGB_EXTRA_KEY = "AddProfileActivity.add_profile_color_rgb" @@ -16,9 +18,10 @@ class AddProfileActivity : InjectableAppCompatActivity() { companion object { fun createAddProfileActivityIntent(context: Context, colorRgb: Int): Intent { - val intent = Intent(context, AddProfileActivity::class.java) - intent.putExtra(ADD_PROFILE_COLOR_RGB_EXTRA_KEY, colorRgb) - return intent + return Intent(context, AddProfileActivity::class.java).apply { + putExtra(ADD_PROFILE_COLOR_RGB_EXTRA_KEY, colorRgb) + decorateWithScreenName(ADD_PROFILE_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/profile/AdminAuthActivity.kt b/app/src/main/java/org/oppia/android/app/profile/AdminAuthActivity.kt index a4832bf2c72..6b783c5781a 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AdminAuthActivity.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AdminAuthActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.ADMIN_AUTH_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject const val ADMIN_AUTH_ADMIN_PIN_EXTRA_KEY = "AdminAuthActivity.admin_auth_admin_pin" @@ -25,12 +27,13 @@ class AdminAuthActivity : InjectableAppCompatActivity() { colorRgb: Int, adminPinEnum: Int ): Intent { - val intent = Intent(context, AdminAuthActivity::class.java) - intent.putExtra(ADMIN_AUTH_ADMIN_PIN_EXTRA_KEY, adminPin) - intent.putExtra(ADMIN_AUTH_PROFILE_ID_EXTRA_KEY, profileId) - intent.putExtra(ADMIN_AUTH_COLOR_RGB_EXTRA_KEY, colorRgb) - intent.putExtra(ADMIN_AUTH_ENUM_EXTRA_KEY, adminPinEnum) - return intent + return Intent(context, AdminAuthActivity::class.java).apply { + putExtra(ADMIN_AUTH_ADMIN_PIN_EXTRA_KEY, adminPin) + putExtra(ADMIN_AUTH_PROFILE_ID_EXTRA_KEY, profileId) + putExtra(ADMIN_AUTH_COLOR_RGB_EXTRA_KEY, colorRgb) + putExtra(ADMIN_AUTH_ENUM_EXTRA_KEY, adminPinEnum) + decorateWithScreenName(ADMIN_AUTH_ACTIVITY) + } } fun getIntentKey(): String { diff --git a/app/src/main/java/org/oppia/android/app/profile/AdminPinActivity.kt b/app/src/main/java/org/oppia/android/app/profile/AdminPinActivity.kt index b32ca1b99d7..b893a6cc19a 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AdminPinActivity.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AdminPinActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.ADMIN_PIN_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject const val ADMIN_PIN_PROFILE_ID_EXTRA_KEY = "AdminPinActivity.admin_pin_profile_id" @@ -23,11 +25,12 @@ class AdminPinActivity : InjectableAppCompatActivity() { colorRgb: Int, adminPinEnum: Int ): Intent { - val intent = Intent(context, AdminPinActivity::class.java) - intent.putExtra(ADMIN_PIN_PROFILE_ID_EXTRA_KEY, profileId) - intent.putExtra(ADMIN_PIN_COLOR_RGB_EXTRA_KEY, colorRgb) - intent.putExtra(ADMIN_PIN_ENUM_EXTRA_KEY, adminPinEnum) - return intent + return Intent(context, AdminPinActivity::class.java).apply { + putExtra(ADMIN_PIN_PROFILE_ID_EXTRA_KEY, profileId) + putExtra(ADMIN_PIN_COLOR_RGB_EXTRA_KEY, colorRgb) + putExtra(ADMIN_PIN_ENUM_EXTRA_KEY, adminPinEnum) + decorateWithScreenName(ADMIN_PIN_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivity.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivity.kt index 54a57da71c7..7be8b64ecaf 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivity.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.PIN_PASSWORD_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject const val PIN_PASSWORD_PROFILE_ID_EXTRA_KEY = "PinPasswordActivity.pin_password_profile_id" @@ -21,10 +23,11 @@ class PinPasswordActivity : InjectableAppCompatActivity(), ProfileRouteDialogInt adminPin: String, profileId: Int ): Intent { - val intent = Intent(context, PinPasswordActivity::class.java) - intent.putExtra(PIN_PASSWORD_PROFILE_ID_EXTRA_KEY, profileId) - intent.putExtra(PIN_PASSWORD_ADMIN_PIN_EXTRA_KEY, adminPin) - return intent + return Intent(context, PinPasswordActivity::class.java).apply { + putExtra(PIN_PASSWORD_PROFILE_ID_EXTRA_KEY, profileId) + putExtra(PIN_PASSWORD_ADMIN_PIN_EXTRA_KEY, adminPin) + decorateWithScreenName(PIN_PASSWORD_ACTIVITY) + } } } @@ -44,6 +47,6 @@ class PinPasswordActivity : InjectableAppCompatActivity(), ProfileRouteDialogInt override fun onDestroy() { super.onDestroy() - pinPasswordActivityPresenter.dismissAlertDialog() + pinPasswordActivityPresenter.handleOnDestroy() } } diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt index 1a7ba8e23dc..b9592c2a5c3 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt @@ -1,8 +1,5 @@ package org.oppia.android.app.profile -import android.content.ActivityNotFoundException -import android.content.Intent -import android.net.Uri import android.text.method.PasswordTransformationMethod import android.view.animation.AnimationUtils import androidx.appcompat.app.AlertDialog @@ -21,6 +18,7 @@ import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import kotlin.system.exitProcess private const val TAG_ADMIN_SETTINGS_DIALOG = "ADMIN_SETTINGS_DIALOG" private const val TAG_RESET_PIN_DIALOG = "RESET_PIN_DIALOG" @@ -38,6 +36,7 @@ class PinPasswordActivityPresenter @Inject constructor( } private var profileId = -1 private lateinit var alertDialog: AlertDialog + private var confirmedDeletion = false fun handleOnCreate() { val adminPin = activity.intent.getStringExtra(PIN_PASSWORD_ADMIN_PIN_EXTRA_KEY) @@ -166,43 +165,71 @@ class PinPasswordActivityPresenter @Inject constructor( private fun showAdminForgotPin() { val appName = resourceHandler.getStringInLocale(R.string.app_name) pinViewModel.showAdminPinForgotPasswordPopUp.set(true) + val resetDataButtonText = + resourceHandler.getStringInLocaleWithWrapping( + R.string.admin_forgot_pin_reset_app_data_button_text, appName + ) alertDialog = AlertDialog.Builder(activity, R.style.OppiaAlertDialogTheme) .setTitle(R.string.pin_password_forgot_title) .setMessage( - resourceHandler.getStringInLocaleWithWrapping(R.string.pin_password_forgot_message, appName) + resourceHandler.getStringInLocaleWithWrapping(R.string.admin_forgot_pin_message, appName) ) .setNegativeButton(R.string.admin_settings_cancel) { dialog, _ -> pinViewModel.showAdminPinForgotPasswordPopUp.set(false) dialog.dismiss() } - .setPositiveButton(R.string.pin_password_play_store) { dialog, _ -> + .setPositiveButton(resetDataButtonText) { dialog, _ -> + // Show a confirmation dialog since this is a permanent action. + dialog.dismiss() + showConfirmAppResetDialog() + }.create() + alertDialog.setCanceledOnTouchOutside(false) + alertDialog.show() + } + + private fun showConfirmAppResetDialog() { + val appName = resourceHandler.getStringInLocale(R.string.app_name) + alertDialog = AlertDialog.Builder(activity, R.style.OppiaAlertDialogTheme) + .setTitle( + resourceHandler.getStringInLocaleWithWrapping( + R.string.admin_confirm_app_wipe_title, appName + ) + ) + .setMessage( + resourceHandler.getStringInLocaleWithWrapping( + R.string.admin_confirm_app_wipe_message, appName + ) + ) + .setNegativeButton(R.string.admin_confirm_app_wipe_negative_button_text) { dialog, _ -> pinViewModel.showAdminPinForgotPasswordPopUp.set(false) - try { - activity.startActivity( - Intent( - Intent.ACTION_VIEW, - Uri.parse("market://details?id=" + activity.packageName) - ) - ) - } catch (e: ActivityNotFoundException) { - activity.startActivity( - Intent( - Intent.ACTION_VIEW, - Uri.parse( - "https://play.google.com/store/apps/details?id=" + activity.packageName - ) - ) - ) - } dialog.dismiss() + } + .setPositiveButton(R.string.admin_confirm_app_wipe_positive_button_text) { dialog, _ -> + profileManagementController.deleteAllProfiles().toLiveData().observe( + activity, + { + // Regardless of the result of the operation, always restart the app. + confirmedDeletion = true + activity.finishAffinity() + } + ) }.create() + alertDialog.setCanceledOnTouchOutside(false) alertDialog.show() } - fun dismissAlertDialog() { + fun handleOnDestroy() { if (::alertDialog.isInitialized && alertDialog.isShowing) { alertDialog.dismiss() } + + if (confirmedDeletion) { + confirmedDeletion = false + + // End the process forcibly since the app is not designed to recover from major on-disk state + // changes that happen from underneath it (like deleting all profiles). + exitProcess(0) + } } private fun showSuccessDialog() { diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivity.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivity.kt index 6415f50f796..cfc698c7c4c 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivity.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.PROFILE_CHOOSER_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity that controls profile creation and selection. */ @@ -14,9 +16,10 @@ class ProfileChooserActivity : InjectableAppCompatActivity() { companion object { fun createProfileChooserActivity(context: Context): Intent { - val intent = Intent(context, ProfileChooserActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - return intent + return Intent(context, ProfileChooserActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + decorateWithScreenName(PROFILE_CHOOSER_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt index d78a40767e7..266ad9983c8 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt @@ -65,7 +65,7 @@ class ProfileChooserViewModel @Inject constructor( machineLocale.run { it.profile.name.toMachineLowerCase() } }.toMutableList() - val adminProfile = sortedProfileList.find { it.profile.isAdmin }!! + val adminProfile = sortedProfileList.find { it.profile.isAdmin } ?: return listOf() sortedProfileList.remove(adminProfile) adminPin = adminProfile.profile.pin diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivity.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivity.kt index 20c8685afc8..3644798777b 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivity.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.PROFILE_PICTURE_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity to display profile picture. */ @@ -27,11 +29,10 @@ class ProfilePictureActivity : InjectableAppCompatActivity() { "ProfilePictureActivity.internal_profile_id" fun createProfilePictureActivityIntent(context: Context, internalProfileId: Int): Intent { - val intent = Intent(context, ProfilePictureActivity::class.java) - intent.putExtra( - PROFILE_PICTURE_ACTIVITY_PROFILE_ID_KEY, internalProfileId - ) - return intent + return Intent(context, ProfilePictureActivity::class.java).apply { + decorateWithScreenName(PROFILE_PICTURE_ACTIVITY) + putExtra(PROFILE_PICTURE_ACTIVITY_PROFILE_ID_KEY, internalProfileId) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivity.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivity.kt index f5f7d914a47..938c4dd4454 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivity.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivity.kt @@ -8,7 +8,9 @@ import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.completedstorylist.CompletedStoryListActivity import org.oppia.android.app.home.RouteToRecentlyPlayedListener import org.oppia.android.app.home.recentlyplayed.RecentlyPlayedActivity +import org.oppia.android.app.model.ScreenName.PROFILE_PROGRESS_ACTIVITY import org.oppia.android.app.ongoingtopiclist.OngoingTopicListActivity +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity to display profile progress. */ @@ -62,9 +64,10 @@ class ProfileProgressActivity : const val PROFILE_ID_EXTRA_KEY = "ProfileProgressActivity.profile_id" fun createProfileProgressActivityIntent(context: Context, internalProfileId: Int): Intent { - val intent = Intent(context, ProfileProgressActivity::class.java) - intent.putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId) - return intent + return Intent(context, ProfileProgressActivity::class.java).apply { + putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId) + decorateWithScreenName(PROFILE_PROGRESS_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt index c6036860a9d..d3844e3ce10 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt @@ -20,6 +20,7 @@ import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.topic.TopicController import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.parser.html.StoryHtmlParserEntityType @@ -36,7 +37,8 @@ class ProfileProgressViewModel @Inject constructor( private val topicListController: TopicListController, private val oppiaLogger: OppiaLogger, @StoryHtmlParserEntityType private val entityType: String, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) { /** [internalProfileId] needs to be set before any of the live data members can be accessed. */ private var internalProfileId: Int = -1 @@ -146,7 +148,13 @@ class ProfileProgressViewModel @Inject constructor( itemViewModelList.addAll( itemList.map { story -> RecentlyPlayedStorySummaryViewModel( - activity, internalProfileId, story, entityType, intentFactoryShim, resourceHandler + activity, + internalProfileId, + story, + entityType, + intentFactoryShim, + resourceHandler, + translationController ) } ) diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/RecentlyPlayedStorySummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/profileprogress/RecentlyPlayedStorySummaryViewModel.kt index 55a33f3ef4e..fabe3eb83dc 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/RecentlyPlayedStorySummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/RecentlyPlayedStorySummaryViewModel.kt @@ -6,6 +6,7 @@ import org.oppia.android.app.home.RouteToTopicPlayStoryListener import org.oppia.android.app.model.PromotedStory import org.oppia.android.app.shim.IntentFactoryShim import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.domain.translation.TranslationController /** Recently played item [ViewModel] for the recycler view in [ProfileProgressFragment]. */ class RecentlyPlayedStorySummaryViewModel( @@ -14,8 +15,24 @@ class RecentlyPlayedStorySummaryViewModel( val promotedStory: PromotedStory, val entityType: String, private val intentFactoryShim: IntentFactoryShim, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + translationController: TranslationController ) : ProfileProgressItemViewModel(), RouteToTopicPlayStoryListener { + val storyTitle by lazy { + translationController.extractString( + promotedStory.storyTitle, promotedStory.storyWrittenTranslationContext + ) + } + val topicTitle by lazy { + translationController.extractString( + promotedStory.topicTitle, promotedStory.topicWrittenTranslationContext + ) + } + val nextChapterTitle by lazy { + translationController.extractString( + promotedStory.nextChapterTitle, promotedStory.nextChapterWrittenTranslationContext + ) + } fun onStoryItemClicked() { routeToTopicPlayStory(internalProfileId, promotedStory.topicId, promotedStory.storyId) @@ -23,7 +40,7 @@ class RecentlyPlayedStorySummaryViewModel( fun computeLessonThumbnailContentDescription(): String { return resourceHandler.getStringInLocaleWithWrapping( - R.string.lesson_thumbnail_content_description, promotedStory.nextChapterName + R.string.lesson_thumbnail_content_description, nextChapterTitle ) } 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 47c11242da2..de195869aa8 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 @@ -6,121 +6,92 @@ import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.home.RouteToExplorationListener +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.ResumeLessonActivityParams +import org.oppia.android.app.model.ScreenName.RESUME_LESSON_ACTIVITY import org.oppia.android.app.player.exploration.ExplorationActivity +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.extensions.putProtoExtra +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity that allows the user to resume a saved exploration. */ class ResumeLessonActivity : InjectableAppCompatActivity(), RouteToExplorationListener { - - @Inject - lateinit var resumeLessonActivityPresenter: ResumeLessonActivityPresenter - private var internalProfileId: Int = -1 - private lateinit var topicId: String - private lateinit var storyId: String - private lateinit var explorationId: String - private lateinit var explorationCheckpoint: ExplorationCheckpoint - private var backflowScreen: Int = -1 + @Inject lateinit var resumeLessonActivityPresenter: ResumeLessonActivityPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) - internalProfileId = - intent.getIntExtra(RESUME_LESSON_ACTIVITY_INTERNAL_PROFILE_ID_ARGUMENT_KEY, -1) - topicId = - checkNotNull(intent.getStringExtra(RESUME_LESSON_ACTIVITY_TOPIC_ID_ARGUMENT_KEY)) { - "Expected topic ID to be included in intent for ResumeLessonActivity." - } - storyId = - checkNotNull(intent.getStringExtra(RESUME_LESSON_ACTIVITY_STORY_ID_ARGUMENT_KEY)) { - "Expected story ID to be included in intent for ResumeLessonActivity." - } - explorationId = - checkNotNull(intent.getStringExtra(RESUME_LESSON_ACTIVITY_EXPLORATION_ID_ARGUMENT_KEY)) { - "Expected exploration ID to be included in intent for ResumeLessonActivity." - } - backflowScreen = intent.getIntExtra(RESUME_LESSON_ACTIVITY_BACKFLOW_SCREEN_KEY, -1) - explorationCheckpoint = ExplorationCheckpoint.parseFrom( - intent.getByteArrayExtra(RESUME_LESSON_ACTIVITY_EXPLORATION_CHECKPOINT_ARGUMENT_KEY) - ) + + val params = intent.getProtoExtra(PARAMS_KEY, ResumeLessonActivityParams.getDefaultInstance()) resumeLessonActivityPresenter.handleOnCreate( - internalProfileId, - topicId, - storyId, - explorationId, - backflowScreen, - explorationCheckpoint + params.profileId, + params.topicId, + params.storyId, + params.explorationId, + params.parentScreen, + params.checkpoint ) } // TODO(#1655): Re-restrict access to fields in tests post-Gradle. companion object { - /** Argument key for internal profile ID in [ResumeLessonActivity] */ - const val RESUME_LESSON_ACTIVITY_INTERNAL_PROFILE_ID_ARGUMENT_KEY = - "ResumeLessonActivity.internal_profile_id" - - /** Argument key for topic ID in [ResumeLessonActivity] */ - const val RESUME_LESSON_ACTIVITY_TOPIC_ID_ARGUMENT_KEY = - "ResumeLessonActivity.topic_id" - - /** Argument key for story ID in [ResumeLessonActivity] */ - const val RESUME_LESSON_ACTIVITY_STORY_ID_ARGUMENT_KEY = - "ResumeLessonActivity.story_id" - - /** Argument key for exploration ID in [ResumeLessonActivity] */ - const val RESUME_LESSON_ACTIVITY_EXPLORATION_ID_ARGUMENT_KEY = - "ResumeLessonActivity.exploration_id" - - /** Argument key for backflow screen in [ResumeLessonActivity] */ - const val RESUME_LESSON_ACTIVITY_BACKFLOW_SCREEN_KEY = - "ResumeLessonActivity.backflow_screen" - - /** Argument key for exploration checkpoint in [ResumeLessonActivity] */ - const val RESUME_LESSON_ACTIVITY_EXPLORATION_CHECKPOINT_ARGUMENT_KEY = - "ResumeLessonActivity.exploration_checkpoint" + private const val PARAMS_KEY = "ResumeLessonActivity.params" /** - * Returns a new [Intent] to route to [ResumeLessonActivity] for a specified exploration. + * A convenience function for creating a new [ResumeLessonActivity] intent by prefilling common + * params needed by the activity. */ fun createResumeLessonActivityIntent( context: Context, - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, - explorationCheckpoint: ExplorationCheckpoint + parentScreen: ExplorationActivityParams.ParentScreen, + checkpoint: ExplorationCheckpoint ): Intent { - val intent = Intent(context, ResumeLessonActivity::class.java) - intent.putExtra(RESUME_LESSON_ACTIVITY_INTERNAL_PROFILE_ID_ARGUMENT_KEY, internalProfileId) - intent.putExtra(RESUME_LESSON_ACTIVITY_TOPIC_ID_ARGUMENT_KEY, topicId) - intent.putExtra(RESUME_LESSON_ACTIVITY_STORY_ID_ARGUMENT_KEY, storyId) - intent.putExtra(RESUME_LESSON_ACTIVITY_EXPLORATION_ID_ARGUMENT_KEY, explorationId) - intent.putExtra(RESUME_LESSON_ACTIVITY_BACKFLOW_SCREEN_KEY, backflowScreen) - intent.putExtra( - RESUME_LESSON_ACTIVITY_EXPLORATION_CHECKPOINT_ARGUMENT_KEY, - explorationCheckpoint.toByteArray() - ) - return intent + val params = ResumeLessonActivityParams.newBuilder().apply { + this.profileId = profileId + this.topicId = topicId + this.storyId = storyId + this.explorationId = explorationId + this.parentScreen = parentScreen + this.checkpoint = checkpoint + }.build() + return createResumeLessonActivityIntent(context, params) + } + + /** Returns a new [Intent] open an [ResumeLessonActivity] with the specified [params]. */ + fun createResumeLessonActivityIntent( + context: Context, + params: ResumeLessonActivityParams + ): Intent { + return Intent(context, ResumeLessonActivity::class.java).apply { + putProtoExtra(PARAMS_KEY, params) + decorateWithScreenName(RESUME_LESSON_ACTIVITY) + } } } override fun routeToExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, isCheckpointingEnabled: Boolean ) { startActivity( ExplorationActivity.createExplorationActivityIntent( this, - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, isCheckpointingEnabled ) ) 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 f05a71dd66d..4064708069e 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 @@ -3,7 +3,9 @@ package org.oppia.android.app.resumelesson import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import org.oppia.android.R +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.databinding.ResumeLessonActivityBinding import javax.inject.Inject @@ -16,11 +18,11 @@ class ResumeLessonActivityPresenter @Inject constructor( /** Handles onCreate() method of the [ResumeLessonActivity] */ fun handleOnCreate( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int, + parentScreen: ExplorationActivityParams.ParentScreen, explorationCheckpoint: ExplorationCheckpoint ) { val binding = DataBindingUtil.setContentView( @@ -36,11 +38,11 @@ class ResumeLessonActivityPresenter @Inject constructor( if (getResumeLessonFragment() == null) { val resumeLessonFragment = ResumeLessonFragment.newInstance( - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, explorationCheckpoint ) activity.supportFragmentManager.beginTransaction().add( diff --git a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragment.kt b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragment.kt index 89cfcc69210..10a05914af6 100644 --- a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragment.kt +++ b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragment.kt @@ -7,53 +7,41 @@ import android.view.View import android.view.ViewGroup import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment +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.ResumeLessonFragmentArguments import org.oppia.android.util.extensions.getProto -import org.oppia.android.util.extensions.getStringFromBundle import org.oppia.android.util.extensions.putProto import javax.inject.Inject /** Fragment that allows the user to resume a saved exploration. */ class ResumeLessonFragment : InjectableFragment() { - companion object { - private const val RESUME_LESSON_FRAGMENT_INTERNAL_PROFILE_ID_KEY = - "ResumeExplorationFragmentPresenter.resume_exploration_fragment_internal_profile_id" - private const val RESUME_LESSON_FRAGMENT_TOPIC_ID_KEY = - "ResumeExplorationFragmentPresenter.resume_exploration_fragment_topic_id" - private const val RESUME_LESSON_FRAGMENT_STORY_ID_KEY = - "ResumeExplorationFragmentPresenter.resume_exploration_fragment_story_id" - private const val RESUME_LESSON_FRAGMENT_EXPLORATION_ID_KEY = - "ResumeExplorationFragmentPresenter.resume_Lesson_fragment_exploration_id" - private const val RESUME_LESSON_FRAGMENT_BACKFLOW_SCREEN_KEY = - "ResumeLessonFragmentPresenter.resume_lesson_fragment_backflow_screen" - private const val RESUME_LESSON_FRAGMENT_EXPLORATION_CHECKPOINT_KEY = - "ResumeExplorationFragmentPresenter.resume_Lesson_fragment_exploration_checkpoint" + private const val ARGUMENTS_KEY = "ResumeExplorationFragment.arguments" - /** - * Creates new instance of [ResumeLessonFragment]. - * - * @param internalProfileId is used by the ResumeLessonFragment to retrieve saved checkpoint - * @param explorationId is used by the ResumeLessonFragment to retrieve saved checkpoint - */ + /** Creates new instance of [ResumeLessonFragment] for the provided parameters. */ fun newInstance( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int, - explorationCheckpoint: ExplorationCheckpoint + parentScreen: ExplorationActivityParams.ParentScreen, + checkpoint: ExplorationCheckpoint ): ResumeLessonFragment { - val resumeLessonFragment = ResumeLessonFragment() - val args = Bundle() - args.putInt(RESUME_LESSON_FRAGMENT_INTERNAL_PROFILE_ID_KEY, internalProfileId) - args.putString(RESUME_LESSON_FRAGMENT_TOPIC_ID_KEY, topicId) - args.putString(RESUME_LESSON_FRAGMENT_STORY_ID_KEY, storyId) - args.putString(RESUME_LESSON_FRAGMENT_EXPLORATION_ID_KEY, explorationId) - args.putInt(RESUME_LESSON_FRAGMENT_BACKFLOW_SCREEN_KEY, backflowScreen) - args.putProto(RESUME_LESSON_FRAGMENT_EXPLORATION_CHECKPOINT_KEY, explorationCheckpoint) - resumeLessonFragment.arguments = args - return resumeLessonFragment + val args = ResumeLessonFragmentArguments.newBuilder().apply { + this.profileId = profileId + this.topicId = topicId + this.storyId = storyId + this.explorationId = explorationId + this.parentScreen = parentScreen + this.checkpoint = checkpoint + }.build() + return ResumeLessonFragment().apply { + arguments = Bundle().apply { + putProto(ARGUMENTS_KEY, args) + } + } } } @@ -70,42 +58,18 @@ class ResumeLessonFragment : InjectableFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - val internalProfileId = - checkNotNull(arguments?.getInt(RESUME_LESSON_FRAGMENT_INTERNAL_PROFILE_ID_KEY, -1)) { - "Expected profile ID to be included in arguments for ResumeLessonFragment." - } - val topicId = - checkNotNull(arguments?.getStringFromBundle(RESUME_LESSON_FRAGMENT_TOPIC_ID_KEY)) { - "Expected topic ID to be included in arguments for ResumeLessonFragment." - } - val storyId = - checkNotNull(arguments?.getStringFromBundle(RESUME_LESSON_FRAGMENT_STORY_ID_KEY)) { - "Expected story ID to be included in arguments for ResumeLessonFragment." - } - val explorationId = - checkNotNull(arguments?.getStringFromBundle(RESUME_LESSON_FRAGMENT_EXPLORATION_ID_KEY)) { - "Expected exploration ID to be included in arguments for ResumeLessonFragment." - } - val backflowScreen = arguments?.getInt(RESUME_LESSON_FRAGMENT_BACKFLOW_SCREEN_KEY, -1) - val explorationCheckpoint = - checkNotNull( - arguments?.getProto( - RESUME_LESSON_FRAGMENT_EXPLORATION_CHECKPOINT_KEY, - ExplorationCheckpoint.getDefaultInstance() - ) - ) { - "Expected exploration checkpoint to be included in arguments for ResumeLessonFragment." - } - + val args = checkNotNull(arguments) { + "Expected arguments to be provided for fragment." + }.getProto(ARGUMENTS_KEY, ResumeLessonFragmentArguments.getDefaultInstance()) return resumeLessonFragmentPresenter.handleOnCreate( inflater, container, - internalProfileId, - topicId, - storyId, - explorationId, - backflowScreen, - explorationCheckpoint + args.profileId, + args.topicId, + args.storyId, + args.explorationId, + args.parentScreen, + args.checkpoint ) } } diff --git a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt index 9c223f25862..89173f4c236 100644 --- a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt @@ -6,17 +6,19 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.lifecycle.Transformations import org.oppia.android.app.home.RouteToExplorationListener -import org.oppia.android.app.model.ChapterSummary +import org.oppia.android.app.model.EphemeralChapterSummary +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.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ResumeLessonFragmentBinding import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.DefaultResourceBucketName @@ -31,7 +33,9 @@ class ResumeLessonFragmentPresenter @Inject constructor( private val topicController: TopicController, private val explorationDataController: ExplorationDataController, private val htmlParserFactory: HtmlParser.Factory, + private val translationController: TranslationController, @DefaultResourceBucketName private val resourceBucketName: String, + private val appLanguageResourceHandler: AppLanguageResourceHandler, private val oppiaLogger: OppiaLogger ) { @@ -44,31 +48,32 @@ class ResumeLessonFragmentPresenter @Inject constructor( private lateinit var storyId: String private lateinit var explorationId: String - private val chapterSummaryResultLiveData: LiveData> by lazy { - topicController.retrieveChapter(topicId, storyId, explorationId).toLiveData() + private val chapterSummaryResultLiveData: LiveData> by lazy { + topicController.retrieveChapter(profileId, topicId, storyId, explorationId).toLiveData() } - private val chapterSummaryLiveData: LiveData by lazy { getChapterSummary() } + private val chapterSummaryLiveData: LiveData by lazy { + getChapterSummary() + } /** Handles onCreateView() method of the [ResumeLessonFragment]. */ fun handleOnCreate( inflater: LayoutInflater, container: ViewGroup?, - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, - explorationCheckpoint: ExplorationCheckpoint + parentScreen: ExplorationActivityParams.ParentScreen, + checkpoint: ExplorationCheckpoint ): View? { - binding = ResumeLessonFragmentBinding.inflate( inflater, container, /* attachToRoot= */ false ) - this.profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() + this.profileId = profileId this.topicId = topicId this.storyId = storyId this.explorationId = explorationId @@ -78,28 +83,28 @@ class ResumeLessonFragmentPresenter @Inject constructor( it.viewModel = resumeLessonViewModel } - resumeLessonViewModel.explorationCheckpoint.set(explorationCheckpoint) + resumeLessonViewModel.explorationCheckpoint.set(checkpoint) subscribeToChapterSummary() binding.resumeLessonContinueButton.setOnClickListener { playExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, resumeLessonViewModel.explorationCheckpoint.get()!!, - backflowScreen + parentScreen ) } binding.resumeLessonStartOverButton.setOnClickListener { playExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, ExplorationCheckpoint.getDefaultInstance(), - backflowScreen + parentScreen ) } @@ -109,65 +114,80 @@ class ResumeLessonFragmentPresenter @Inject constructor( private fun subscribeToChapterSummary() { chapterSummaryLiveData.observe( fragment, - Observer { chapterSummary -> - resumeLessonViewModel.chapterSummary.set(chapterSummary) - updateChapterDescription() + { ephemeralChapterSummary -> + val chapterTitle = + translationController.extractString( + ephemeralChapterSummary.chapterSummary.title, + ephemeralChapterSummary.writtenTranslationContext + ) + val chapterDescription = + translationController.extractString( + ephemeralChapterSummary.chapterSummary.description, + ephemeralChapterSummary.writtenTranslationContext + ) + resumeLessonViewModel.chapterSummary.set(ephemeralChapterSummary.chapterSummary) + resumeLessonViewModel.chapterTitle.set(chapterTitle) + bindChapterDescription(chapterDescription) } ) } - private fun updateChapterDescription() { - binding.resumeLessonChapterDescriptionTextView.text = htmlParserFactory.create( + private fun bindChapterDescription(description: String) { + val chapterDescription = htmlParserFactory.create( resourceBucketName, resumeLessonViewModel.entityType, explorationId, - imageCenterAlign = true - ).parseOppiaHtml( - resumeLessonViewModel.chapterSummary.get()!!.summary, - binding.resumeLessonChapterDescriptionTextView - ) + imageCenterAlign = true, + displayLocale = appLanguageResourceHandler.getDisplayLocale() + ).parseOppiaHtml(description, binding.resumeLessonChapterDescriptionTextView) + if (chapterDescription.isNotBlank()) { + binding.resumeLessonChapterDescriptionTextView.visibility = View.VISIBLE + binding.resumeLessonChapterDescriptionTextView.text = chapterDescription + } else { + binding.resumeLessonChapterDescriptionTextView.visibility = View.GONE + } } private fun getResumeLessonViewModel(): ResumeLessonViewModel { return viewModelProvider.getForFragment(fragment, ResumeLessonViewModel::class.java) } - private fun getChapterSummary(): LiveData { + private fun getChapterSummary(): LiveData { return Transformations.map(chapterSummaryResultLiveData, ::processChapterSummaryResult) } private fun processChapterSummaryResult( - chapterSummaryResult: AsyncResult - ): ChapterSummary { - return when (chapterSummaryResult) { + ephemeralResult: AsyncResult + ): EphemeralChapterSummary { + return when (ephemeralResult) { is AsyncResult.Failure -> { oppiaLogger.e( "ResumeLessonFragment", "Failed to retrieve chapter summary for the explorationId $explorationId: ", - chapterSummaryResult.error + ephemeralResult.error ) - ChapterSummary.getDefaultInstance() + EphemeralChapterSummary.getDefaultInstance() } - is AsyncResult.Pending -> ChapterSummary.getDefaultInstance() - is AsyncResult.Success -> chapterSummaryResult.value + is AsyncResult.Pending -> EphemeralChapterSummary.getDefaultInstance() + is AsyncResult.Success -> ephemeralResult.value } } private fun playExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, checkpoint: ExplorationCheckpoint, - backflowScreen: Int? + parentScreen: ExplorationActivityParams.ParentScreen ) { val startPlayingProvider = if (checkpoint == ExplorationCheckpoint.getDefaultInstance()) { explorationDataController.restartExploration( - internalProfileId, topicId, storyId, explorationId + profileId.internalId, topicId, storyId, explorationId ) } else { explorationDataController.resumeExploration( - internalProfileId, topicId, storyId, explorationId, checkpoint + profileId.internalId, topicId, storyId, explorationId, checkpoint ) } startPlayingProvider.toLiveData().observe(fragment) { result -> @@ -178,11 +198,11 @@ class ResumeLessonFragmentPresenter @Inject constructor( is AsyncResult.Success -> { oppiaLogger.d("ResumeLessonFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, // Checkpointing is enabled be default because stating lesson from // ResumeLessonFragment implies that learner has not completed the lesson. isCheckpointingEnabled = true diff --git a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonViewModel.kt b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonViewModel.kt index 1ce5a872a1a..3b4d95a7017 100644 --- a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonViewModel.kt @@ -20,6 +20,9 @@ class ResumeLessonViewModel @Inject constructor( /** The chapter summary for the exploration that may be resumed. */ val chapterSummary = ObservableField(ChapterSummary.getDefaultInstance()) + /** The title of the chapter/exploration being resumed. */ + val chapterTitle = ObservableField() + /** The [ExplorationCheckpoint] that may be used to resume the exploration. */ val explorationCheckpoint = ObservableField(ExplorationCheckpoint.getDefaultInstance()) } diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditActivity.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditActivity.kt index 7df305d2e07..f53b355b630 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditActivity.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.PROFILE_EDIT_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Argument key for the Profile Id in [ProfileEditActivity]. */ @@ -29,10 +31,11 @@ class ProfileEditActivity : InjectableAppCompatActivity() { profileId: Int, isMultipane: Boolean = false ): Intent { - val intent = Intent(context, ProfileEditActivity::class.java) - intent.putExtra(PROFILE_EDIT_PROFILE_ID_EXTRA_KEY, profileId) - intent.putExtra(IS_MULTIPANE_EXTRA_KEY, isMultipane) - return intent + return Intent(context, ProfileEditActivity::class.java).apply { + putExtra(PROFILE_EDIT_PROFILE_ID_EXTRA_KEY, profileId) + putExtra(IS_MULTIPANE_EXTRA_KEY, isMultipane) + decorateWithScreenName(PROFILE_EDIT_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListActivity.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListActivity.kt index 968f1d56c4e..0855ff14dd4 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListActivity.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.PROFILE_LIST_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity to display all profiles to admin. */ @@ -38,7 +40,9 @@ class ProfileListActivity : companion object { /** Returns a new [Intent] to route to [ProfileListActivity]. */ fun createProfileListActivityIntent(context: Context): Intent { - return Intent(context, ProfileListActivity::class.java) + return Intent(context, ProfileListActivity::class.java).apply { + decorateWithScreenName(PROFILE_LIST_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameActivity.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameActivity.kt index 12d716abeab..53a9a106d51 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameActivity.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.PROFILE_RENAME_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Argument key for the profile id for which the name is going to be changed in [ProfileRenameActivity]. */ @@ -19,9 +21,10 @@ class ProfileRenameActivity : InjectableAppCompatActivity() { /** Returns an [Intent] for opening [ProfileRenameActivity]. */ fun createProfileRenameActivity(context: Context, profileId: Int): Intent { - val intent = Intent(context, ProfileRenameActivity::class.java) - intent.putExtra(PROFILE_RENAME_PROFILE_ID_EXTRA_KEY, profileId) - return intent + return Intent(context, ProfileRenameActivity::class.java).apply { + putExtra(PROFILE_RENAME_PROFILE_ID_EXTRA_KEY, profileId) + decorateWithScreenName(PROFILE_RENAME_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinActivity.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinActivity.kt index 29afa324638..fb35f4ff730 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinActivity.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.PROFILE_RESET_PIN_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Argument key for the ID of the profile resetting their pin. */ @@ -24,10 +26,11 @@ class ProfileResetPinActivity : InjectableAppCompatActivity() { /** Returns [Intent] for opening [ProfileResetPinActivity]. */ fun createProfileResetPinActivity(context: Context, profileId: Int, isAdmin: Boolean): Intent { - val intent = Intent(context, ProfileResetPinActivity::class.java) - intent.putExtra(PROFILE_RESET_PIN_PROFILE_ID_EXTRA_KEY, profileId) - intent.putExtra(PROFILE_RESET_PIN_IS_ADMIN_EXTRA_KEY, isAdmin) - return intent + return Intent(context, ProfileResetPinActivity::class.java).apply { + putExtra(PROFILE_RESET_PIN_PROFILE_ID_EXTRA_KEY, profileId) + putExtra(PROFILE_RESET_PIN_IS_ADMIN_EXTRA_KEY, isAdmin) + decorateWithScreenName(PROFILE_RESET_PIN_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/shim/ViewBindingShimImpl.kt b/app/src/main/java/org/oppia/android/app/shim/ViewBindingShimImpl.kt index fcb28a4b22e..cd577465e07 100644 --- a/app/src/main/java/org/oppia/android/app/shim/ViewBindingShimImpl.kt +++ b/app/src/main/java/org/oppia/android/app/shim/ViewBindingShimImpl.kt @@ -14,6 +14,7 @@ import org.oppia.android.app.home.promotedlist.PromotedStoryViewModel import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.state.itemviewmodel.DragDropInteractionContentViewModel import org.oppia.android.app.player.state.itemviewmodel.SelectionInteractionContentViewModel +import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.databinding.ComingSoonTopicViewBinding import org.oppia.android.databinding.DragDropInteractionItemsBinding import org.oppia.android.databinding.DragDropSingleItemBinding @@ -34,7 +35,8 @@ import javax.inject.Inject */ // TODO(#1619): Remove file post-Gradle class ViewBindingShimImpl @Inject constructor( - private val translationController: TranslationController + private val translationController: TranslationController, + private val appLanguageResourceHandler: AppLanguageResourceHandler ) : ViewBindingShim { override fun providePromotedStoryCardInflatedView( @@ -103,7 +105,8 @@ class ViewBindingShimImpl @Inject constructor( resourceBucketName, entityType, entityId, - false + false, + displayLocale = appLanguageResourceHandler.getDisplayLocale() ).parseOppiaHtml( translationController.extractString(viewModel.htmlContent, writtenTranslationContext), binding.itemSelectionContentsTextView @@ -136,7 +139,8 @@ class ViewBindingShimImpl @Inject constructor( DataBindingUtil.findBinding(view)!! binding.htmlContent = htmlParserFactory.create( - resourceBucketName, entityType, entityId, /* imageCenterAlign= */ false + resourceBucketName, entityType, entityId, /* imageCenterAlign= */ false, + displayLocale = appLanguageResourceHandler.getDisplayLocale() ).parseOppiaHtml( translationController.extractString(viewModel.htmlContent, writtenTranslationContext), binding.multipleChoiceContentTextView @@ -222,7 +226,8 @@ class ViewBindingShimImpl @Inject constructor( resourceBucketName, entityType, entityId, - /* imageCenterAlign= */ false + /* imageCenterAlign= */ false, + displayLocale = appLanguageResourceHandler.getDisplayLocale() ).parseOppiaHtml( viewModel, dragDropSingleItemBinding.dragDropContentTextView ) diff --git a/app/src/main/java/org/oppia/android/app/splash/SplashActivity.kt b/app/src/main/java/org/oppia/android/app/splash/SplashActivity.kt index 83859936edf..1952e72ae3b 100644 --- a/app/src/main/java/org/oppia/android/app/splash/SplashActivity.kt +++ b/app/src/main/java/org/oppia/android/app/splash/SplashActivity.kt @@ -6,10 +6,14 @@ import androidx.fragment.app.Fragment import org.oppia.android.app.activity.ActivityComponent import org.oppia.android.app.activity.ActivityComponentFactory import org.oppia.android.app.activity.ActivityComponentImpl -import org.oppia.android.app.deprecation.DeprecationNoticeExitAppListener import org.oppia.android.app.fragment.FragmentComponent import org.oppia.android.app.fragment.FragmentComponentBuilderInjector import org.oppia.android.app.fragment.FragmentComponentFactory +import org.oppia.android.app.model.ScreenName.SPLASH_ACTIVITY +import org.oppia.android.app.notice.BetaNoticeClosedListener +import org.oppia.android.app.notice.DeprecationNoticeExitAppListener +import org.oppia.android.app.notice.GeneralAvailabilityUpgradeNoticeClosedListener +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** @@ -21,7 +25,12 @@ import javax.inject.Inject * through their intents). */ class SplashActivity : - AppCompatActivity(), FragmentComponentFactory, DeprecationNoticeExitAppListener { + AppCompatActivity(), + FragmentComponentFactory, + DeprecationNoticeExitAppListener, + BetaNoticeClosedListener, + GeneralAvailabilityUpgradeNoticeClosedListener { + private lateinit var activityComponent: ActivityComponent @Inject @@ -33,12 +42,19 @@ class SplashActivity : activityComponent = componentFactory.createActivityComponent(this) (activityComponent as ActivityComponentImpl).inject(this) splashActivityPresenter.handleOnCreate() + intent.decorateWithScreenName(SPLASH_ACTIVITY) } - override fun onCloseAppButtonClicked() = splashActivityPresenter.handleOnCloseAppButtonClicked() - override fun createFragmentComponent(fragment: Fragment): FragmentComponent { val builderInjector = activityComponent as FragmentComponentBuilderInjector return builderInjector.getFragmentComponentBuilderProvider().get().setFragment(fragment).build() } + + override fun onCloseAppButtonClicked() = splashActivityPresenter.handleOnCloseAppButtonClicked() + + override fun onBetaNoticeOkayButtonClicked(permanentlyDismiss: Boolean) = + splashActivityPresenter.handleOnBetaNoticeOkayButtonClicked(permanentlyDismiss) + + override fun onGaUpgradeNoticeOkayButtonClicked(permanentlyDismiss: Boolean) = + splashActivityPresenter.handleOnGaUpgradeNoticeOkayButtonClicked(permanentlyDismiss) } diff --git a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt index 84fe06a1197..5cbbc7df08c 100644 --- a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt @@ -1,18 +1,23 @@ package org.oppia.android.app.splash -import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.LiveData +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer -import androidx.lifecycle.Transformations import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope -import org.oppia.android.app.deprecation.AutomaticAppDeprecationNoticeDialogFragment import org.oppia.android.app.model.AppStartupState +import org.oppia.android.app.model.AppStartupState.BuildFlavorNoticeMode import org.oppia.android.app.model.AppStartupState.StartupMode +import org.oppia.android.app.model.BuildFlavor +import org.oppia.android.app.notice.AutomaticAppDeprecationNoticeDialogFragment +import org.oppia.android.app.notice.BetaNoticeDialogFragment +import org.oppia.android.app.notice.GeneralAvailabilityUpgradeNoticeDialogFragment import org.oppia.android.app.onboarding.OnboardingActivity import org.oppia.android.app.profile.ProfileChooserActivity import org.oppia.android.app.translation.AppLanguageLocaleHandler +import org.oppia.android.app.utility.LifecycleSafeTimerFactory +import org.oppia.android.databinding.SplashActivityBinding import org.oppia.android.domain.locale.LocaleController import org.oppia.android.domain.onboarding.AppStartupStateController import org.oppia.android.domain.oppialogger.OppiaLogger @@ -26,6 +31,8 @@ import org.oppia.android.util.locale.OppiaLocale import javax.inject.Inject private const val AUTO_DEPRECATION_NOTICE_DIALOG_FRAGMENT_TAG = "auto_deprecation_notice_dialog" +private const val BETA_NOTICE_DIALOG_FRAGMENT_TAG = "beta_notice_dialog" +private const val GA_UPDATE_NOTICE_DIALOG_FRAGMENT_TAG = "general_availability_update_notice_dialog" private const val SPLASH_INIT_STATE_DATA_PROVIDER_ID = "splash_init_state_data_provider" /** The presenter for [SplashActivity]. */ @@ -37,15 +44,21 @@ class SplashActivityPresenter @Inject constructor( private val primeTopicAssetsController: PrimeTopicAssetsController, private val translationController: TranslationController, private val localeController: LocaleController, - private val appLanguageLocaleHandler: AppLanguageLocaleHandler + private val appLanguageLocaleHandler: AppLanguageLocaleHandler, + private val lifecycleSafeTimerFactory: LifecycleSafeTimerFactory, + private val currentBuildFlavor: BuildFlavor ) { + lateinit var startupMode: StartupMode fun handleOnCreate() { - activity.setContentView(R.layout.splash_activity) - activity.window.setFlags( - WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN - ) + DataBindingUtil.setContentView( + activity, R.layout.splash_activity + ).apply { + isOnDeveloperFlavor = currentBuildFlavor == BuildFlavor.DEVELOPER + isOnAlphaFlavor = currentBuildFlavor == BuildFlavor.ALPHA + isOnBetaFlavor = currentBuildFlavor == BuildFlavor.BETA + } + // Initiate download support before any additional processing begins. primeTopicAssetsController.downloadAssets(R.style.OppiaAlertDialogTheme) subscribeToOnboardingFlow() @@ -57,41 +70,49 @@ class SplashActivityPresenter @Inject constructor( activity.finish() } + /** Handles cases when the user dismisses the beta notice dialog. */ + fun handleOnBetaNoticeOkayButtonClicked(permanentlyDismiss: Boolean) { + if (permanentlyDismiss) { + appStartupStateController.dismissBetaNoticesPermanently() + } + processStartupMode() + } + + /** Handles cases when the user dismisses the general availability update notice dialog. */ + fun handleOnGaUpgradeNoticeOkayButtonClicked(permanentlyDismiss: Boolean) { + if (permanentlyDismiss) { + appStartupStateController.dismissGaUpgradeNoticesPermanently() + } + processStartupMode() + } + private fun subscribeToOnboardingFlow() { - val liveData = computeInitStateLiveData() + val liveData = computeInitStateDataProvider().toLiveData() liveData.observe( activity, - object : Observer { - override fun onChanged(initState: SplashInitState) { - // It's possible for the observer to still be active & change due to the next activity - // causing a notification to be posted. That's always invalid to process here: the splash - // activity should never do anything after its initial state since it always finishes (or - // in the case of the deprecation dialog, blocks) the activity. - liveData.removeObserver(this) - - // First, initialize the app's initial locale. - appLanguageLocaleHandler.initializeLocale(initState.displayLocale) - - // Second, route the user to the correct destination. - when (initState.startupMode) { - StartupMode.USER_IS_ONBOARDED -> { - activity.startActivity(ProfileChooserActivity.createProfileChooserActivity(activity)) - activity.finish() - } - StartupMode.APP_IS_DEPRECATED -> { - if (getDeprecationNoticeDialogFragment() == null) { - activity.supportFragmentManager.beginTransaction() - .add( - AutomaticAppDeprecationNoticeDialogFragment.newInstance(), - AUTO_DEPRECATION_NOTICE_DIALOG_FRAGMENT_TAG - ).commitNow() + object : Observer> { + override fun onChanged(initStateResult: AsyncResult) { + when (initStateResult) { + is AsyncResult.Pending -> { + // Ensure that pending states last no longer than 5 seconds. In cases where the app + // enters a bad state, this ensures that the user doesn't become stuck on the splash + // screen. + lifecycleSafeTimerFactory.createTimer(timeoutMillis = 5000).observe(activity) { + processInitState(SplashInitState.computeDefault(localeController)) } } - else -> { - // In all other cases (including errors when the startup state fails to load or is - // defaulted), assume the user needs to be onboarded. - activity.startActivity(OnboardingActivity.createOnboardingActivity(activity)) - activity.finish() + is AsyncResult.Failure -> { + oppiaLogger.e( + "SplashActivity", "Failed to compute initial state", initStateResult.error + ) + } + is AsyncResult.Success -> { + // It's possible for the observer to still be active & change due to the next activity + // causing a notification to be posted. That's always invalid to process here: the + // splash activity should never do anything after its initial state since it always + // finishes (or in the case of the deprecation dialog, blocks) the activity. + liveData.removeObserver(this) + processInitState(initStateResult.value) } } } @@ -99,47 +120,93 @@ class SplashActivityPresenter @Inject constructor( ) } + private fun processInitState(initState: SplashInitState) { + // First, initialize the app's initial locale. Note that since the activity can be + // reopened, it's possible for this to be initialized more than once. + if (!appLanguageLocaleHandler.isInitialized()) { + appLanguageLocaleHandler.initializeLocale(initState.displayLocale) + } + + // Second, prepare to route the user to the correct destination. + startupMode = initState.appStartupState.startupMode + + // Third, show any dismissible notices (if the app isn't deprecated). + if (startupMode != StartupMode.APP_IS_DEPRECATED) { + when (initState.appStartupState.buildFlavorNoticeMode) { + BuildFlavorNoticeMode.FLAVOR_NOTICE_MODE_UNSPECIFIED, BuildFlavorNoticeMode.NO_NOTICE, + BuildFlavorNoticeMode.UNRECOGNIZED, null -> { + // No notice should be shown. However, when a pre-release version of the app is active + // that changes the splash screen have it wait a bit longer so that the build flavor can + // be clearly seen. The developer build isn't part of the wait to ensure fast startup + // times (for development purposes). + when (currentBuildFlavor) { + BuildFlavor.BUILD_FLAVOR_UNSPECIFIED, BuildFlavor.UNRECOGNIZED, + BuildFlavor.TESTING, BuildFlavor.DEVELOPER, BuildFlavor.GENERAL_AVAILABILITY -> + processStartupMode() + BuildFlavor.ALPHA, BuildFlavor.BETA -> { + lifecycleSafeTimerFactory.createTimer(timeoutMillis = 2000).observe(activity) { + processStartupMode() + } + } + } + } + BuildFlavorNoticeMode.SHOW_BETA_NOTICE -> + showDialog(BETA_NOTICE_DIALOG_FRAGMENT_TAG, BetaNoticeDialogFragment::newInstance) + BuildFlavorNoticeMode.SHOW_UPGRADE_TO_GENERAL_AVAILABILITY_NOTICE -> { + showDialog( + GA_UPDATE_NOTICE_DIALOG_FRAGMENT_TAG, + GeneralAvailabilityUpgradeNoticeDialogFragment::newInstance + ) + } + } + } else processStartupMode() + } + + private fun processStartupMode() { + when (startupMode) { + StartupMode.USER_IS_ONBOARDED -> { + activity.startActivity(ProfileChooserActivity.createProfileChooserActivity(activity)) + activity.finish() + } + StartupMode.APP_IS_DEPRECATED -> { + showDialog( + AUTO_DEPRECATION_NOTICE_DIALOG_FRAGMENT_TAG, + AutomaticAppDeprecationNoticeDialogFragment::newInstance + ) + } + else -> { + // In all other cases (including errors when the startup state fails to load or is + // defaulted), assume the user needs to be onboarded. + activity.startActivity(OnboardingActivity.createOnboardingActivity(activity)) + activity.finish() + } + } + } + private fun computeInitStateDataProvider(): DataProvider { val startupStateDataProvider = appStartupStateController.getAppStartupState() val systemAppLanguageLocaleDataProvider = translationController.getSystemLanguageLocale() return startupStateDataProvider.combineWith( systemAppLanguageLocaleDataProvider, SPLASH_INIT_STATE_DATA_PROVIDER_ID ) { startupState, systemAppLanguageLocale -> - SplashInitState(startupState.startupMode, systemAppLanguageLocale) + SplashInitState(startupState, systemAppLanguageLocale) } } - private fun computeInitStateLiveData(): LiveData = - Transformations.map(computeInitStateDataProvider().toLiveData(), ::processInitState) - - private fun processInitState( - initStateResult: AsyncResult - ): SplashInitState { - // If there's an error loading the data, assume the default. - return when (initStateResult) { - is AsyncResult.Failure -> { - oppiaLogger.e("SplashActivity", "Failed to compute initial state", initStateResult.error) - SplashInitState.computeDefault(localeController) - } - is AsyncResult.Pending -> SplashInitState.computeDefault(localeController) - is AsyncResult.Success -> initStateResult.value + private inline fun showDialog(tag: String, createFragment: () -> T) { + if (activity.supportFragmentManager.findFragmentByTag(tag) as? T == null) { + activity.supportFragmentManager.beginTransaction().add(createFragment(), tag).commitNow() } } - private fun getDeprecationNoticeDialogFragment(): AutomaticAppDeprecationNoticeDialogFragment? { - return activity.supportFragmentManager.findFragmentByTag( - AUTO_DEPRECATION_NOTICE_DIALOG_FRAGMENT_TAG - ) as? AutomaticAppDeprecationNoticeDialogFragment - } - private data class SplashInitState( - val startupMode: StartupMode, + val appStartupState: AppStartupState, val displayLocale: OppiaLocale.DisplayLocale ) { companion object { fun computeDefault(localeController: LocaleController): SplashInitState { return SplashInitState( - startupMode = AppStartupState.getDefaultInstance().startupMode, + appStartupState = AppStartupState.getDefaultInstance(), displayLocale = localeController.reconstituteDisplayLocale( localeController.getLikelyDefaultAppStringLocaleContext() ) diff --git a/app/src/main/java/org/oppia/android/app/story/ExplorationSelectionListener.kt b/app/src/main/java/org/oppia/android/app/story/ExplorationSelectionListener.kt index c8667d2a992..8f42436a7c0 100644 --- a/app/src/main/java/org/oppia/android/app/story/ExplorationSelectionListener.kt +++ b/app/src/main/java/org/oppia/android/app/story/ExplorationSelectionListener.kt @@ -1,18 +1,20 @@ package org.oppia.android.app.story +import org.oppia.android.app.model.ExplorationActivityParams import org.oppia.android.app.model.ExplorationCheckpoint +import org.oppia.android.app.model.ProfileId /** Listener for cases when the user taps on a specific chapter/exploration to play. */ interface ExplorationSelectionListener { /** Called when an exploration has been selected by the user. */ fun selectExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, canExplorationBeResumed: Boolean, canHavePartialProgressSaved: Boolean, - backflowId: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, explorationCheckpoint: ExplorationCheckpoint ) } diff --git a/app/src/main/java/org/oppia/android/app/story/StoryActivity.kt b/app/src/main/java/org/oppia/android/app/story/StoryActivity.kt index 65fc4f809da..24a31412b1b 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryActivity.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryActivity.kt @@ -6,10 +6,14 @@ import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.home.RouteToExplorationListener +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.ScreenName.STORY_ACTIVITY import org.oppia.android.app.player.exploration.ExplorationActivity import org.oppia.android.app.resumelesson.ResumeLessonActivity import org.oppia.android.app.topic.RouteToResumeLessonListener +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for stories. */ @@ -37,42 +41,42 @@ class StoryActivity : } override fun routeToExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, isCheckpointingEnabled: Boolean ) { startActivity( ExplorationActivity.createExplorationActivityIntent( this, - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, isCheckpointingEnabled ) ) } override fun routeToResumeLesson( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, explorationCheckpoint: ExplorationCheckpoint ) { startActivity( ResumeLessonActivity.createResumeLessonActivityIntent( this, - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, explorationCheckpoint ) ) @@ -94,11 +98,12 @@ class StoryActivity : topicId: String, storyId: String ): Intent { - val intent = Intent(context, StoryActivity::class.java) - intent.putExtra(STORY_ACTIVITY_INTENT_EXTRA_INTERNAL_PROFILE_ID, internalProfileId) - intent.putExtra(STORY_ACTIVITY_INTENT_EXTRA_TOPIC_ID, topicId) - intent.putExtra(STORY_ACTIVITY_INTENT_EXTRA_STORY_ID, storyId) - return intent + return Intent(context, StoryActivity::class.java).apply { + putExtra(STORY_ACTIVITY_INTENT_EXTRA_INTERNAL_PROFILE_ID, internalProfileId) + putExtra(STORY_ACTIVITY_INTENT_EXTRA_TOPIC_ID, topicId) + putExtra(STORY_ACTIVITY_INTENT_EXTRA_STORY_ID, storyId) + decorateWithScreenName(STORY_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/story/StoryFragment.kt b/app/src/main/java/org/oppia/android/app/story/StoryFragment.kt index 9d059264948..0d4187b132b 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryFragment.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryFragment.kt @@ -7,7 +7,9 @@ import android.view.View import android.view.ViewGroup import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment +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.util.extensions.getStringFromBundle import javax.inject.Inject @@ -65,23 +67,23 @@ class StoryFragment : InjectableFragment(), ExplorationSelectionListener, StoryF } override fun selectExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, canExplorationBeResumed: Boolean, canHavePartialProgressSaved: Boolean, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, explorationCheckpoint: ExplorationCheckpoint ) { storyFragmentPresenter.handleSelectExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, canExplorationBeResumed, canHavePartialProgressSaved, - backflowScreen, + parentScreen, explorationCheckpoint ) } diff --git a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt index baa1efc5b09..24795c0024c 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt @@ -21,7 +21,9 @@ import androidx.recyclerview.widget.RecyclerView import org.oppia.android.R import org.oppia.android.app.home.RouteToExplorationListener import org.oppia.android.app.model.ChapterPlayState +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.recyclerview.BindableAdapter import org.oppia.android.app.story.storyitemviewmodel.StoryChapterSummaryViewModel import org.oppia.android.app.story.storyitemviewmodel.StoryHeaderViewModel @@ -33,6 +35,7 @@ import org.oppia.android.databinding.StoryFragmentBinding import org.oppia.android.databinding.StoryHeaderViewBinding import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.util.accessibility.AccessibilityService import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.DefaultResourceBucketName @@ -61,6 +64,9 @@ class StoryFragmentPresenter @Inject constructor( @Inject lateinit var storyViewModel: StoryViewModel + @Inject + lateinit var accessibilityService: AccessibilityService + fun handleCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -104,32 +110,32 @@ class StoryFragmentPresenter @Inject constructor( } fun handleSelectExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, canExplorationBeResumed: Boolean, canHavePartialProgressSaved: Boolean, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, explorationCheckpoint: ExplorationCheckpoint ) { if (canExplorationBeResumed) { routeToResumeLessonListener.routeToResumeLesson( - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, explorationCheckpoint ) } else { playExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, canHavePartialProgressSaved, - backflowScreen + parentScreen ) } } @@ -167,41 +173,42 @@ class StoryFragmentPresenter @Inject constructor( resourceBucketName, entityType, storyItemViewModel.storyId, - imageCenterAlign = true - ).parseOppiaHtml( - storyItemViewModel.summary, binding.chapterSummary - ) + imageCenterAlign = true, + displayLocale = resourceHandler.getDisplayLocale() + ).parseOppiaHtml(storyItemViewModel.description, binding.chapterSummary) if (storyItemViewModel.chapterSummary.chapterPlayState == ChapterPlayState.NOT_PLAYABLE_MISSING_PREREQUISITES ) { val missingPrerequisiteSummary = resourceHandler.getStringInLocaleWithWrapping( R.string.chapter_prerequisite_title_label, storyItemViewModel.index.toString(), - storyItemViewModel.missingPrerequisiteChapter.name + storyItemViewModel.missingPrerequisiteChapterTitle ) val chapterLockedSpannable = SpannableString(missingPrerequisiteSummary) - val clickableSpan = object : ClickableSpan() { - override fun onClick(widget: View) { - smoothScrollToPosition(storyItemViewModel.index - 1) - } + if (!accessibilityService.isScreenReaderEnabled()) { + val clickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + smoothScrollToPosition(storyItemViewModel.index - 1) + } - override fun updateDrawState(ds: TextPaint) { - super.updateDrawState(ds) - ds.isUnderlineText = false + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.isUnderlineText = false + } } + chapterLockedSpannable.setSpan( + clickableSpan, + /* start= */ LOCKED_CARD_PREFIX_LENGTH, + /* end= */ chapterLockedSpannable.length - LOCKED_CARD_SUFFIX_LENGTH, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + chapterLockedSpannable.setSpan( + TypefaceSpan("sans-serif-medium"), + /* start= */ LOCKED_CARD_PREFIX_LENGTH, + /* end= */ chapterLockedSpannable.length - LOCKED_CARD_SUFFIX_LENGTH, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } - chapterLockedSpannable.setSpan( - clickableSpan, - /* start= */ LOCKED_CARD_PREFIX_LENGTH, - /* end= */ chapterLockedSpannable.length - LOCKED_CARD_SUFFIX_LENGTH, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) - chapterLockedSpannable.setSpan( - TypefaceSpan("sans-serif-medium"), - /* start= */ LOCKED_CARD_PREFIX_LENGTH, - /* end= */ chapterLockedSpannable.length - LOCKED_CARD_SUFFIX_LENGTH, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) binding.htmlContent = chapterLockedSpannable binding.chapterSummary.movementMethod = LinkMovementMethod.getInstance() } @@ -252,22 +259,22 @@ class StoryFragmentPresenter @Inject constructor( } private fun playExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, canHavePartialProgressSaved: Boolean, - backflowScreen: Int? + parentScreen: ExplorationActivityParams.ParentScreen ) { // If there's no existing progress, this is either playing a new exploration or replaying an old // one. val startPlayingProvider = if (canHavePartialProgressSaved) { explorationDataController.startPlayingNewExploration( - internalProfileId, topicId, storyId, explorationId + profileId.internalId, topicId, storyId, explorationId ) } else { explorationDataController.replayExploration( - internalProfileId, topicId, storyId, explorationId + profileId.internalId, topicId, storyId, explorationId ) } startPlayingProvider.toLiveData().observe(fragment) { result -> @@ -278,11 +285,11 @@ class StoryFragmentPresenter @Inject constructor( is AsyncResult.Success -> { oppiaLogger.d("Story Fragment", "Successfully loaded exploration: $explorationId") routeToExplorationListener.routeToExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, isCheckpointingEnabled = canHavePartialProgressSaved ) } diff --git a/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt b/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt index c127eae3707..7fee629b2e4 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt @@ -5,9 +5,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ChapterPlayState -import org.oppia.android.app.model.ChapterSummary +import org.oppia.android.app.model.EphemeralStorySummary import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.StorySummary import org.oppia.android.app.story.storyitemviewmodel.StoryChapterSummaryViewModel import org.oppia.android.app.story.storyitemviewmodel.StoryHeaderViewModel import org.oppia.android.app.story.storyitemviewmodel.StoryItemViewModel @@ -15,6 +14,7 @@ import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.parser.html.StoryHtmlParserEntityType @@ -28,7 +28,8 @@ class StoryViewModel @Inject constructor( private val explorationCheckpointController: ExplorationCheckpointController, private val oppiaLogger: OppiaLogger, @StoryHtmlParserEntityType val entityType: String, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) { private var internalProfileId: Int = -1 private lateinit var topicId: String @@ -37,7 +38,7 @@ class StoryViewModel @Inject constructor( private lateinit var storyId: String private val explorationSelectionListener = fragment as ExplorationSelectionListener - private val storyResultLiveData: LiveData> by lazy { + private val storyResultLiveData: LiveData> by lazy { topicController.getStory( ProfileId.newBuilder().setInternalId(internalProfileId).build(), topicId, @@ -45,12 +46,12 @@ class StoryViewModel @Inject constructor( ).toLiveData() } - private val storyLiveData: LiveData by lazy { + private val storyLiveData: LiveData by lazy { Transformations.map(storyResultLiveData, ::processStoryResult) } val storyNameLiveData: LiveData by lazy { - Transformations.map(storyLiveData, StorySummary::getStoryName) + Transformations.map(storyLiveData, ::processStoryTitle) } val storyChapterLiveData: LiveData> by lazy { @@ -69,19 +70,30 @@ class StoryViewModel @Inject constructor( this.storyId = storyId } - private fun processStoryResult(storyResult: AsyncResult): StorySummary { - return when (storyResult) { + private fun processStoryResult( + ephemeralResult: AsyncResult + ): EphemeralStorySummary { + return when (ephemeralResult) { is AsyncResult.Failure -> { - oppiaLogger.e("StoryFragment", "Failed to retrieve Story: ", storyResult.error) - StorySummary.getDefaultInstance() + oppiaLogger.e("StoryFragment", "Failed to retrieve Story: ", ephemeralResult.error) + EphemeralStorySummary.getDefaultInstance() } - is AsyncResult.Pending -> StorySummary.getDefaultInstance() - is AsyncResult.Success -> storyResult.value + is AsyncResult.Pending -> EphemeralStorySummary.getDefaultInstance() + is AsyncResult.Success -> ephemeralResult.value } } - private fun processStoryChapterList(storySummary: StorySummary): List { - val chapterList: List = storySummary.chapterList + private fun processStoryTitle(ephemeralStorySummary: EphemeralStorySummary): String { + return translationController.extractString( + ephemeralStorySummary.storySummary.storyTitle, ephemeralStorySummary.writtenTranslationContext + ) + } + + private fun processStoryChapterList( + ephemeralStorySummary: EphemeralStorySummary + ): List { + val storySummary = ephemeralStorySummary.storySummary + val chapterList = ephemeralStorySummary.chaptersList for (position in chapterList.indices) { if (storySummary.chapterList[position].chapterPlayState == ChapterPlayState.NOT_STARTED) { (fragment as StoryFragmentScroller).smoothScrollToPosition(position + 1) @@ -90,7 +102,9 @@ class StoryViewModel @Inject constructor( } val completedCount = - chapterList.filter { chapter -> chapter.chapterPlayState == ChapterPlayState.COMPLETED }.size + chapterList.filter { ephemeralChapterSummary -> + ephemeralChapterSummary.chapterSummary.chapterPlayState == ChapterPlayState.COMPLETED + }.size // List with only the header val itemViewModelList: MutableList = mutableListOf( @@ -99,7 +113,7 @@ class StoryViewModel @Inject constructor( // Add the rest of the list itemViewModelList.addAll( - chapterList.mapIndexed { index, chapter -> + chapterList.mapIndexed { index, ephemeralChapterSummary -> StoryChapterSummaryViewModel( index, chapterList.size, @@ -109,9 +123,10 @@ class StoryViewModel @Inject constructor( internalProfileId, topicId, storyId, - chapter, + ephemeralChapterSummary, entityType, - resourceHandler + resourceHandler, + translationController ) } ) diff --git a/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt index 4acbaecddea..746af71c90b 100644 --- a/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt @@ -4,13 +4,15 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import org.oppia.android.R import org.oppia.android.app.model.ChapterPlayState -import org.oppia.android.app.model.ChapterSummary +import org.oppia.android.app.model.EphemeralChapterSummary +import org.oppia.android.app.model.ExplorationActivityParams import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.LessonThumbnail import org.oppia.android.app.model.ProfileId import org.oppia.android.app.story.ExplorationSelectionListener import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData @@ -24,17 +26,29 @@ class StoryChapterSummaryViewModel( val internalProfileId: Int, val topicId: String, val storyId: String, - val chapterSummary: ChapterSummary, + private val ephemeralChapterSummary: EphemeralChapterSummary, val entityType: String, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : StoryItemViewModel() { - + val chapterSummary = ephemeralChapterSummary.chapterSummary val explorationId: String = chapterSummary.explorationId - private val name: String = chapterSummary.name - val summary: String = chapterSummary.summary + val description: String by lazy { + translationController.extractString( + chapterSummary.description, ephemeralChapterSummary.writtenTranslationContext + ) + } val chapterThumbnail: LessonThumbnail = chapterSummary.chapterThumbnail - val missingPrerequisiteChapter: ChapterSummary = chapterSummary.missingPrerequisiteChapter + val missingPrerequisiteChapterTitle by lazy { + translationController.extractString( + ephemeralChapterSummary.missingPrerequisiteChapter.chapterSummary.title, + ephemeralChapterSummary.missingPrerequisiteChapter.writtenTranslationContext + ) + } val chapterPlayState: ChapterPlayState = chapterSummary.chapterPlayState + private val profileId by lazy { + ProfileId.newBuilder().apply { internalId = internalProfileId }.build() + } fun onExplorationClicked() { val canHavePartialProgressSaved = @@ -61,25 +75,25 @@ class StoryChapterSummaryViewModel( if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) explorationSelectionListener.selectExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, canExplorationBeResumed = true, canHavePartialProgressSaved, - backflowId = 1, + parentScreen = ExplorationActivityParams.ParentScreen.STORY_SCREEN, explorationCheckpoint = it.value ) } else if (it is AsyncResult.Failure) { explorationCheckpointLiveData.removeObserver(this) explorationSelectionListener.selectExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, canExplorationBeResumed = false, canHavePartialProgressSaved, - backflowId = 1, + parentScreen = ExplorationActivityParams.ParentScreen.STORY_SCREEN, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() ) } @@ -88,21 +102,25 @@ class StoryChapterSummaryViewModel( ) } else { explorationSelectionListener.selectExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, canExplorationBeResumed = false, canHavePartialProgressSaved, - backflowId = 1, + parentScreen = ExplorationActivityParams.ParentScreen.STORY_SCREEN, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() ) } } fun computeChapterTitleText(): String { + val title = + translationController.extractString( + chapterSummary.title, ephemeralChapterSummary.writtenTranslationContext + ) return resourceHandler.getStringInLocaleWithWrapping( - R.string.chapter_name, (index + 1).toString(), name + R.string.chapter_name, (index + 1).toString(), title ) } } diff --git a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivity.kt index 0eab42a60a2..8c47b60e056 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivity.kt @@ -4,6 +4,8 @@ import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.home.RouteToExplorationListener +import org.oppia.android.app.model.ExplorationActivityParams +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.player.exploration.ExplorationActivity import org.oppia.android.app.topic.TopicFragment import org.oppia.android.app.utility.SplitScreenManager @@ -28,21 +30,21 @@ class ExplorationTestActivity : InjectableAppCompatActivity(), RouteToExploratio } override fun routeToExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, isCheckpointingEnabled: Boolean ) { startActivity( ExplorationActivity.createExplorationActivityIntent( this, - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, isCheckpointingEnabled ) ) diff --git a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt index d57623b188e..f4a4285b28d 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt @@ -9,6 +9,8 @@ import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment import org.oppia.android.app.home.RouteToExplorationListener +import org.oppia.android.app.model.ExplorationActivityParams +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.utility.SplitScreenManager import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger @@ -64,11 +66,11 @@ class ExplorationTestActivityPresenter @Inject constructor( is AsyncResult.Success -> { oppiaLogger.d(TAG_EXPLORATION_TEST_ACTIVITY, "Successfully loaded exploration") routeToExplorationListener.routeToExploration( - INTERNAL_PROFILE_ID, + ProfileId.newBuilder().apply { internalId = INTERNAL_PROFILE_ID }.build(), TOPIC_ID, STORY_ID, EXPLORATION_ID, - backflowScreen = null, + parentScreen = ExplorationActivityParams.ParentScreen.PARENT_SCREEN_UNSPECIFIED, isCheckpointingEnabled = false ) } diff --git a/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt index abdcba85aef..058a8334426 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt @@ -29,16 +29,19 @@ class ImageRegionSelectionTestFragmentPresenter @Inject constructor( return listOf( createLabeledRegion( "Region 3", + "You have selected Region 3", createPoint2d(0.24242424242424243f, 0.22400442477876106f) to createPoint2d(0.49242424242424243f, 0.7638274336283186f) ), createLabeledRegion( "Region 1", + "You have selected Region 1", createPoint2d(0.553030303030303f, 0.5470132743362832f) to createPoint2d(0.7613636363636364f, 0.7638274336283186f) ), createLabeledRegion( "Region 2", + "You have selected Region 2", createPoint2d(0.5454545454545454f, 0.22842920353982302f) to createPoint2d(0.7537878787878788f, 0.4540929203539823f) ) @@ -47,9 +50,10 @@ class ImageRegionSelectionTestFragmentPresenter @Inject constructor( private fun createLabeledRegion( label: String, + contentDescription: String, points: Pair ): LabeledRegion { - return LabeledRegion.newBuilder().setLabel(label) + return LabeledRegion.newBuilder().setLabel(label).setContentDescription(contentDescription) .setRegion( LabeledRegion.Region.newBuilder() .setRegionType(LabeledRegion.Region.RegionType.RECTANGLE) diff --git a/app/src/main/java/org/oppia/android/app/testing/ListItemLeadingMarginSpanTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/ListItemLeadingMarginSpanTestActivity.kt new file mode 100644 index 00000000000..91d51d33e52 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/testing/ListItemLeadingMarginSpanTestActivity.kt @@ -0,0 +1,15 @@ +package org.oppia.android.app.testing + +import android.os.Bundle +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAppCompatActivity + +/** This is a dummy activity to test unordered
    and ordered
      lists leading margin span. */ +class ListItemLeadingMarginSpanTestActivity : InjectableAppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + setContentView(R.layout.test_list_item_leading_margin_activity) + } +} diff --git a/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivity.kt new file mode 100644 index 00000000000..e85b14b61f6 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivity.kt @@ -0,0 +1,49 @@ +package org.oppia.android.app.testing + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.extensions.putProtoExtra +import javax.inject.Inject + +/** Test Activity used for testing [PoliciesFragment] */ +class PoliciesFragmentTestActivity : InjectableAppCompatActivity() { + + @Inject + lateinit var policiesFragmentTestActivityPresenter: PoliciesFragmentTestActivityPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + + policiesFragmentTestActivityPresenter.handleOnCreate( + intent.getProtoExtra( + POLICIES_FRAGMENT_TEST_POLICY_PAGE_PARAMS_PROTO, + PoliciesActivityParams.getDefaultInstance() + ) + ) + } + + companion object { + /** Argument key for policy page in [PoliciesFragmentTestActivity]. */ + const val POLICIES_FRAGMENT_TEST_POLICY_PAGE_PARAMS_PROTO = + "PoliciesFragmentTestActivity.policy_page" + + /** Returns the [Intent] for opening [PoliciesFragmentTestActivity] for the specified [policyPage]. */ + fun createPoliciesFragmentTestActivity(context: Context, policyPage: PolicyPage): Intent { + val policiesActivityParams = + PoliciesActivityParams + .newBuilder() + .setPolicyPage(policyPage) + .build() + return Intent(context, PoliciesFragmentTestActivity::class.java).also { + it.putProtoExtra(POLICIES_FRAGMENT_TEST_POLICY_PAGE_PARAMS_PROTO, policiesActivityParams) + } + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivityPresenter.kt new file mode 100644 index 00000000000..acacdf432c0 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivityPresenter.kt @@ -0,0 +1,44 @@ +package org.oppia.android.app.testing + +import androidx.appcompat.app.AppCompatActivity +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PoliciesFragmentArguments +import org.oppia.android.app.policies.PoliciesFragment +import javax.inject.Inject + +/** The presenter for [PoliciesFragmentTestActivity] */ +@ActivityScope +class PoliciesFragmentTestActivityPresenter @Inject constructor( + private val activity: AppCompatActivity +) { + + /** Handles onCreate() method of the [PoliciesFragmentTestActivity]. */ + fun handleOnCreate(policiesActivityParams: PoliciesActivityParams) { + activity.setContentView(R.layout.policies_fragment_test_activity) + if (getPoliciesFragment() == null) { + val policiesFragmentArguments = + PoliciesFragmentArguments + .newBuilder() + .setPolicyPage(policiesActivityParams.policyPage) + .build() + val policiesFragment: PoliciesFragment = + PoliciesFragment.newInstance(policiesFragmentArguments) + + activity + .supportFragmentManager + .beginTransaction() + .add( + R.id.policies_fragment_placeholder, + policiesFragment + ).commitNow() + } + } + + private fun getPoliciesFragment(): PoliciesFragment? { + return activity + .supportFragmentManager + .findFragmentById(R.id.policies_fragment_placeholder) as PoliciesFragment? + } +} diff --git a/app/src/main/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivity.kt b/app/src/main/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivity.kt index b84f46c6765..7b415d9cf20 100644 --- a/app/src/main/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivity.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ReadingTextSize import javax.inject.Inject /** Test activity used for testing font scale. */ @@ -16,9 +17,9 @@ class TestFontScaleConfigurationUtilActivity : InjectableAppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) - val readingTextSize = checkNotNull(intent.getStringExtra(FONT_SCALE_EXTRA_KEY)) { - "Expected $FONT_SCALE_EXTRA_KEY to be in intent extras." - } + val readingTextSize = checkNotNull( + intent.getSerializableExtra(FONT_SCALE_EXTRA_KEY) as? ReadingTextSize + ) { "Expected $FONT_SCALE_EXTRA_KEY to be in intent extras." } configUtilActivityPresenter.handleOnCreate(readingTextSize) } @@ -26,7 +27,7 @@ class TestFontScaleConfigurationUtilActivity : InjectableAppCompatActivity() { private const val FONT_SCALE_EXTRA_KEY = "TestFontScaleConfigurationUtilActivity.font_scale" /** Returns a new [TestFontScaleConfigurationUtilActivity] for context and reading text size. */ - fun createFontScaleTestActivity(context: Context, readingTextSize: String): Intent { + fun createFontScaleTestActivity(context: Context, readingTextSize: ReadingTextSize): Intent { val intent = Intent(context, TestFontScaleConfigurationUtilActivity::class.java) intent.putExtra(FONT_SCALE_EXTRA_KEY, readingTextSize) return intent diff --git a/app/src/main/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityPresenter.kt index c46630eb39e..b09b76d15c0 100644 --- a/app/src/main/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityPresenter.kt @@ -3,6 +3,7 @@ package org.oppia.android.app.testing import androidx.appcompat.app.AppCompatActivity import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.utility.FontScaleConfigurationUtil import javax.inject.Inject @@ -12,7 +13,7 @@ class TestFontScaleConfigurationUtilActivityPresenter @Inject constructor( private val activity: AppCompatActivity, private val fontScaleConfigurationUtil: FontScaleConfigurationUtil ) { - fun handleOnCreate(readingTextSize: String) { + fun handleOnCreate(readingTextSize: ReadingTextSize) { fontScaleConfigurationUtil.adjustFontScale(activity, readingTextSize) activity.setContentView(R.layout.font_scale_test_activity) } diff --git a/app/src/main/java/org/oppia/android/app/testing/TopicTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/TopicTestActivity.kt index eaa6c7a54af..292dbb8138a 100644 --- a/app/src/main/java/org/oppia/android/app/testing/TopicTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/TopicTestActivity.kt @@ -4,6 +4,7 @@ import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.home.RouteToExplorationListener +import org.oppia.android.app.model.ExplorationActivityParams import org.oppia.android.app.model.ProfileId import org.oppia.android.app.player.exploration.ExplorationActivity import org.oppia.android.app.story.StoryActivity @@ -58,21 +59,21 @@ class TopicTestActivity : } override fun routeToExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, isCheckpointingEnabled: Boolean ) { startActivity( ExplorationActivity.createExplorationActivityIntent( this, - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, isCheckpointingEnabled ) ) diff --git a/app/src/main/java/org/oppia/android/app/testing/TopicTestActivityForStory.kt b/app/src/main/java/org/oppia/android/app/testing/TopicTestActivityForStory.kt index b055504b828..a5216c51ec2 100644 --- a/app/src/main/java/org/oppia/android/app/testing/TopicTestActivityForStory.kt +++ b/app/src/main/java/org/oppia/android/app/testing/TopicTestActivityForStory.kt @@ -4,6 +4,7 @@ import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.home.RouteToExplorationListener +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.player.exploration.ExplorationActivity @@ -63,32 +64,32 @@ class TopicTestActivityForStory : } override fun routeToResumeLesson( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, explorationCheckpoint: ExplorationCheckpoint ) { startActivity( ResumeLessonActivity.createResumeLessonActivityIntent( this, - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, explorationCheckpoint ) ) } override fun routeToExploration( - profileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, isCheckpointingEnabled: Boolean ) { startActivity( @@ -98,7 +99,7 @@ class TopicTestActivityForStory : topicId, storyId, explorationId, - backflowScreen, + parentScreen, isCheckpointingEnabled ) ) diff --git a/app/src/main/java/org/oppia/android/app/topic/RouteToResumeLessonListener.kt b/app/src/main/java/org/oppia/android/app/topic/RouteToResumeLessonListener.kt index 5a88a638dc8..babe6ff0d83 100644 --- a/app/src/main/java/org/oppia/android/app/topic/RouteToResumeLessonListener.kt +++ b/app/src/main/java/org/oppia/android/app/topic/RouteToResumeLessonListener.kt @@ -1,16 +1,18 @@ package org.oppia.android.app.topic +import org.oppia.android.app.model.ExplorationActivityParams import org.oppia.android.app.model.ExplorationCheckpoint +import org.oppia.android.app.model.ProfileId /** Listener for when an activity should route to a [ResumeLessonActivity]. */ interface RouteToResumeLessonListener { /** Called selects an exploration that can be resumed. */ fun routeToResumeLesson( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, explorationCheckpoint: ExplorationCheckpoint ) } diff --git a/app/src/main/java/org/oppia/android/app/topic/TopicActivity.kt b/app/src/main/java/org/oppia/android/app/topic/TopicActivity.kt index d3991d3edf8..eab78d074a9 100755 --- a/app/src/main/java/org/oppia/android/app/topic/TopicActivity.kt +++ b/app/src/main/java/org/oppia/android/app/topic/TopicActivity.kt @@ -9,13 +9,16 @@ import org.oppia.android.app.activity.ActivityIntentFactories import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.drawer.NAVIGATION_PROFILE_ID_ARGUMENT_KEY import org.oppia.android.app.home.RouteToExplorationListener +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.ScreenName.TOPIC_ACTIVITY import org.oppia.android.app.player.exploration.ExplorationActivity import org.oppia.android.app.resumelesson.ResumeLessonActivity import org.oppia.android.app.story.StoryActivity import org.oppia.android.app.topic.questionplayer.QuestionPlayerActivity import org.oppia.android.app.topic.revisioncard.RevisionCardActivity +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject private const val TOPIC_ACTIVITY_TOPIC_ID_ARGUMENT_KEY = "TopicActivity.topic_id" @@ -81,42 +84,42 @@ class TopicActivity : } override fun routeToExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, isCheckpointingEnabled: Boolean ) { startActivity( ExplorationActivity.createExplorationActivityIntent( this, - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, isCheckpointingEnabled ) ) } override fun routeToResumeLesson( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, - backflowScreen: Int?, + parentScreen: ExplorationActivityParams.ParentScreen, explorationCheckpoint: ExplorationCheckpoint ) { startActivity( ResumeLessonActivity.createResumeLessonActivityIntent( this, - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen, + parentScreen, explorationCheckpoint ) ) @@ -159,6 +162,7 @@ class TopicActivity : return Intent(context, TopicActivity::class.java).apply { putExtra(NAVIGATION_PROFILE_ID_ARGUMENT_KEY, internalProfileId) putExtra(TOPIC_ACTIVITY_TOPIC_ID_ARGUMENT_KEY, topicId) + decorateWithScreenName(TOPIC_ACTIVITY) } } diff --git a/app/src/main/java/org/oppia/android/app/topic/TopicFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/TopicFragmentPresenter.kt index 9b32a29e1e3..c4389501d25 100644 --- a/app/src/main/java/org/oppia/android/app/topic/TopicFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/TopicFragmentPresenter.kt @@ -12,7 +12,6 @@ import com.google.android.material.tabs.TabLayoutMediator import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.TopicFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import javax.inject.Inject @@ -22,7 +21,7 @@ import javax.inject.Inject class TopicFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider, + private val viewModel: TopicViewModel, private val oppiaLogger: OppiaLogger, @EnablePracticeTab private val enablePracticeTab: Boolean, private val resourceHandler: AppLanguageResourceHandler @@ -61,7 +60,6 @@ class TopicFragmentPresenter @Inject constructor( binding.topicToolbarTitle.isSelected = true } - val viewModel = getTopicViewModel() viewModel.setInternalProfileId(internalProfileId) viewModel.setTopicId(topicId) binding.viewModel = viewModel @@ -89,10 +87,6 @@ class TopicFragmentPresenter @Inject constructor( } } - private fun getTopicViewModel(): TopicViewModel { - return viewModelProvider.getForFragment(fragment, TopicViewModel::class.java) - } - private fun logTopicEvents(tab: TopicTab) { when (tab) { TopicTab.INFO -> logInfoFragmentEvent(topicId) diff --git a/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt index 46d591d347a..4fcfd5a2ed8 100644 --- a/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt @@ -4,12 +4,13 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralTopic import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.Topic import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -19,27 +20,30 @@ import javax.inject.Inject class TopicViewModel @Inject constructor( private val topicController: TopicController, private val oppiaLogger: OppiaLogger, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : ObservableViewModel() { private var internalProfileId: Int = -1 private lateinit var topicId: String - private val topicResultLiveData: LiveData> by lazy { + private val topicResultLiveData: LiveData> by lazy { topicController.getTopic( ProfileId.newBuilder().setInternalId(internalProfileId).build(), topicId ).toLiveData() } - private val topicLiveData: LiveData by lazy { + private val topicLiveData: LiveData by lazy { Transformations.map(topicResultLiveData, ::processTopicResult) } - private val topicNameLiveData by lazy { Transformations.map(topicLiveData, Topic::getName) } - val topicToolbarTitleLiveData: LiveData by lazy { - Transformations.map(topicNameLiveData) { name -> - resourceHandler.getStringInLocaleWithWrapping(R.string.topic_name, name) + Transformations.map(topicLiveData) { ephemeralTopic -> + val topicTitle = + translationController.extractString( + ephemeralTopic.topic.title, ephemeralTopic.writtenTranslationContext + ) + resourceHandler.getStringInLocaleWithWrapping(R.string.topic_name, topicTitle) } } @@ -51,14 +55,14 @@ class TopicViewModel @Inject constructor( this.topicId = topicId } - private fun processTopicResult(topicResult: AsyncResult): Topic { - return when (topicResult) { + private fun processTopicResult(ephemeralResult: AsyncResult): EphemeralTopic { + return when (ephemeralResult) { is AsyncResult.Failure -> { - oppiaLogger.e("TopicFragment", "Failed to retrieve Topic: ", topicResult.error) - Topic.getDefaultInstance() + oppiaLogger.e("TopicFragment", "Failed to retrieve Topic: ", ephemeralResult.error) + EphemeralTopic.getDefaultInstance() } - is AsyncResult.Pending -> Topic.getDefaultInstance() - is AsyncResult.Success -> topicResult.value + is AsyncResult.Pending -> EphemeralTopic.getDefaultInstance() + is AsyncResult.Success -> ephemeralResult.value } } } diff --git a/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt index 28c8d1b8415..4b731ea971f 100644 --- a/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ConceptCardFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger @@ -25,7 +26,8 @@ class ConceptCardFragmentPresenter @Inject constructor( @ConceptCardHtmlParserEntityType private val entityType: String, @DefaultResourceBucketName private val resourceBucketName: String, private val viewModelProvider: ViewModelProvider, - private val translationController: TranslationController + private val translationController: TranslationController, + private val appLanguageResourceHandler: AppLanguageResourceHandler ) { /** * Sets up data binding and toolbar. @@ -70,8 +72,16 @@ class ConceptCardFragmentPresenter @Inject constructor( ephemeralConceptCard.writtenTranslationContext ) view.text = htmlParserFactory - .create(resourceBucketName, entityType, skillId, imageCenterAlign = true) - .parseOppiaHtml(explanationHtml, view) + .create( + resourceBucketName, + entityType, + skillId, + imageCenterAlign = true, + displayLocale = appLanguageResourceHandler.getDisplayLocale() + ) + .parseOppiaHtml( + explanationHtml, view + ) } ) diff --git a/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt index 9195ce8ebb5..a155d6498c2 100644 --- a/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt @@ -8,9 +8,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralTopic import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.Topic -import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.TopicInfoFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController @@ -24,25 +23,15 @@ import javax.inject.Inject @FragmentScope class TopicInfoFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider, + private val viewModel: TopicInfoViewModel, private val oppiaLogger: OppiaLogger, private val topicController: TopicController, private val htmlParserFactory: HtmlParser.Factory, @DefaultResourceBucketName private val resourceBucketName: String ) { private lateinit var binding: TopicInfoFragmentBinding - private val topicInfoViewModel = getTopicInfoViewModel() private var internalProfileId: Int = -1 private lateinit var topicId: String - private val htmlParser: HtmlParser by lazy { - htmlParserFactory - .create( - resourceBucketName, - /* entityType= */ "topic", - topicId, - /* imageCenterAlign= */ true - ) - } fun handleCreateView( inflater: LayoutInflater, @@ -60,48 +49,43 @@ class TopicInfoFragmentPresenter @Inject constructor( subscribeToTopicLiveData() binding.let { it.lifecycleOwner = fragment - it.viewModel = topicInfoViewModel + it.viewModel = viewModel } return binding.root } - private fun getTopicInfoViewModel(): TopicInfoViewModel { - return viewModelProvider.getForFragment(fragment, TopicInfoViewModel::class.java) - } - - private val topicLiveData: LiveData by lazy { getTopicList() } + private val topicLiveData: LiveData by lazy { getTopicList() } private fun subscribeToTopicLiveData() { topicLiveData.observe( fragment, - { topic -> - topicInfoViewModel.setTopic(topic) - topicInfoViewModel.topicDescription.set(topic.description) - topicInfoViewModel.calculateTopicSizeWithUnit() + { ephemeralTopic -> + viewModel.setTopic(ephemeralTopic) + viewModel.calculateTopicSizeWithUnit() controlSeeMoreTextVisibility() } ) } - private val topicResultLiveData: LiveData> by lazy { + private val topicResultLiveData: LiveData> by lazy { topicController.getTopic( ProfileId.newBuilder().setInternalId(internalProfileId).build(), topicId ).toLiveData() } - private fun getTopicList(): LiveData { + private fun getTopicList(): LiveData { return Transformations.map(topicResultLiveData, ::processTopicResult) } - private fun processTopicResult(topic: AsyncResult): Topic { - return when (topic) { + private fun processTopicResult(ephemeralResult: AsyncResult): EphemeralTopic { + return when (ephemeralResult) { is AsyncResult.Failure -> { - oppiaLogger.e("TopicInfoFragment", "Failed to retrieve topic", topic.error) - Topic.getDefaultInstance() + oppiaLogger.e("TopicInfoFragment", "Failed to retrieve topic", ephemeralResult.error) + EphemeralTopic.getDefaultInstance() } - is AsyncResult.Pending -> Topic.getDefaultInstance() - is AsyncResult.Success -> topic.value + is AsyncResult.Pending -> EphemeralTopic.getDefaultInstance() + is AsyncResult.Success -> ephemeralResult.value } } @@ -109,11 +93,11 @@ class TopicInfoFragmentPresenter @Inject constructor( val minimumNumberOfLines = fragment.resources.getInteger(R.integer.topic_description_collapsed) binding.topicDescriptionTextView.post { if (binding.topicDescriptionTextView.lineCount > minimumNumberOfLines) { - getTopicInfoViewModel().isDescriptionExpanded.set(false) - getTopicInfoViewModel().isSeeMoreVisible.set(true) + viewModel.isDescriptionExpanded.set(false) + viewModel.isSeeMoreVisible.set(true) } else { - getTopicInfoViewModel().isDescriptionExpanded.set(true) - getTopicInfoViewModel().isSeeMoreVisible.set(false) + viewModel.isDescriptionExpanded.set(true) + viewModel.isSeeMoreVisible.set(false) } } } diff --git a/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoViewModel.kt index f382b098f41..edc1f4f38c2 100644 --- a/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoViewModel.kt @@ -1,37 +1,49 @@ package org.oppia.android.app.topic.info -import android.content.Context import androidx.databinding.ObservableField import androidx.lifecycle.ViewModel import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralTopic import org.oppia.android.app.model.Topic import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.parser.html.TopicHtmlParserEntityType import javax.inject.Inject /** [ViewModel] for showing topic info details. */ @FragmentScope class TopicInfoViewModel @Inject constructor( - private val context: Context, @TopicHtmlParserEntityType val entityType: String, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : ObservableViewModel() { val topic = ObservableField(DEFAULT_TOPIC) val storyCountText: ObservableField = ObservableField(computeStoryCountText(DEFAULT_TOPIC)) val topicSizeText: ObservableField = ObservableField("") - val topicDescription = ObservableField("") + val topicTitle = ObservableField(DEFAULT_TOPIC.title.html) + val topicDescription = ObservableField(DEFAULT_TOPIC.description.html) var downloadStatusIndicatorDrawableResourceId = ObservableField(R.drawable.ic_available_offline_primary_24dp) val isDescriptionExpanded = ObservableField(true) val isSeeMoreVisible = ObservableField(true) - fun setTopic(topic: Topic) { - this.topic.set(topic) - storyCountText.set(computeStoryCountText(topic)) + fun setTopic(ephemeralTopic: EphemeralTopic) { + this.topic.set(ephemeralTopic.topic) + topicTitle.set( + translationController.extractString( + ephemeralTopic.topic.title, ephemeralTopic.writtenTranslationContext + ) + ) + topicDescription.set( + translationController.extractString( + ephemeralTopic.topic.description, ephemeralTopic.writtenTranslationContext + ) + ) + storyCountText.set(computeStoryCountText(ephemeralTopic.topic)) } fun calculateTopicSizeWithUnit() { diff --git a/app/src/main/java/org/oppia/android/app/topic/lessons/ChapterSummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/lessons/ChapterSummaryViewModel.kt index 7ad2aa9f97b..62a499626e3 100644 --- a/app/src/main/java/org/oppia/android/app/topic/lessons/ChapterSummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/lessons/ChapterSummaryViewModel.kt @@ -10,7 +10,7 @@ import org.oppia.android.app.viewmodel.ObservableViewModel class ChapterSummaryViewModel( val chapterPlayState: ChapterPlayState, val explorationId: String, - val chapterName: String, + val chapterTitle: String, val storyId: String, private val index: Int, private val chapterSummarySelector: ChapterSummarySelector, @@ -24,11 +24,11 @@ class ChapterSummaryViewModel( fun computeChapterPlayStateIconContentDescription(): String { return if (chapterPlayState == ChapterPlayState.COMPLETED) { resourceHandler.getStringInLocaleWithWrapping( - R.string.chapter_completed, (index + 1).toString(), chapterName + R.string.chapter_completed, (index + 1).toString(), chapterTitle ) } else { resourceHandler.getStringInLocaleWithWrapping( - R.string.chapter_in_progress, (index + 1).toString(), chapterName + R.string.chapter_in_progress, (index + 1).toString(), chapterTitle ) } } diff --git a/app/src/main/java/org/oppia/android/app/topic/lessons/StorySummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/lessons/StorySummaryViewModel.kt index d7b0832e002..a2d90d1134a 100644 --- a/app/src/main/java/org/oppia/android/app/topic/lessons/StorySummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/lessons/StorySummaryViewModel.kt @@ -3,18 +3,26 @@ package org.oppia.android.app.topic.lessons import androidx.databinding.ObservableField import androidx.lifecycle.ViewModel import org.oppia.android.R -import org.oppia.android.app.model.StorySummary +import org.oppia.android.app.model.EphemeralStorySummary import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.domain.translation.TranslationController private const val DEFAULT_STORY_PERCENTAGE = 0 /** [ViewModel] for displaying a story summary. */ class StorySummaryViewModel( - val storySummary: StorySummary, + private val ephemeralStorySummary: EphemeralStorySummary, private val storySummarySelector: StorySummarySelector, private val chapterSummarySelector: ChapterSummarySelector, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : TopicLessonsItemViewModel() { + val storySummary = ephemeralStorySummary.storySummary + val storyTitle by lazy { + translationController.extractString( + storySummary.storyTitle, ephemeralStorySummary.writtenTranslationContext + ) + } val storyPercentage: ObservableField = ObservableField(DEFAULT_STORY_PERCENTAGE) val storyProgressPercentageText: ObservableField = ObservableField(computeStoryProgressPercentageText(DEFAULT_STORY_PERCENTAGE)) @@ -39,7 +47,7 @@ class StorySummaryViewModel( R.plurals.chapter_count, storySummary.chapterCount, storySummary.chapterCount.toString() ) return resourceHandler.getStringInLocaleWithWrapping( - R.string.chapter_count_with_story_name, chapterCountText, storySummary.storyName + R.string.chapter_count_with_story_name, chapterCountText, storyTitle ) } @@ -49,6 +57,21 @@ class StorySummaryViewModel( ) } + /* + * Returns content description of progress container based on story percentage. + * + * @return a [String] representing content description for progress container + */ + fun computeProgressContainerContentDescription(): String { + return if (storyPercentage.get()!! < 100) { + "${storyProgressPercentageText.get()} " + + resourceHandler.getStringInLocale(R.string.status_in_progress) + } else { + "${storyProgressPercentageText.get()} " + + resourceHandler.getStringInLocale(R.string.status_completed) + } + } + private fun computeStoryProgressPercentageText(storyPercentage: Int): String { return resourceHandler.getStringInLocaleWithWrapping( R.string.topic_story_progress_percentage, storyPercentage.toString() @@ -56,11 +79,14 @@ class StorySummaryViewModel( } private fun computeChapterSummaryItemList(): List { - return storySummary.chapterList.mapIndexed { index, chapterSummary -> + return ephemeralStorySummary.chaptersList.mapIndexed { index, ephemeralChapterSummary -> ChapterSummaryViewModel( - chapterPlayState = chapterSummary.chapterPlayState, - explorationId = chapterSummary.explorationId, - chapterName = chapterSummary.name, + chapterPlayState = ephemeralChapterSummary.chapterSummary.chapterPlayState, + explorationId = ephemeralChapterSummary.chapterSummary.explorationId, + chapterTitle = translationController.extractString( + ephemeralChapterSummary.chapterSummary.title, + ephemeralChapterSummary.writtenTranslationContext + ), storyId = storySummary.storyId, index = index, chapterSummarySelector = chapterSummarySelector, diff --git a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt index 4bf478275cb..87a50c6d1d9 100644 --- a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt @@ -5,12 +5,13 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralTopic import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.StorySummary -import org.oppia.android.app.model.Topic import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -21,7 +22,8 @@ class TopicLessonViewModel @Inject constructor( private val fragment: Fragment, private val oppiaLogger: OppiaLogger, private val topicController: TopicController, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) { private var internalProfileId: Int = -1 private lateinit var topicId: String @@ -33,42 +35,43 @@ class TopicLessonViewModel @Inject constructor( Transformations.map(topicLiveData, ::processTopic) } - private val topicLiveData: LiveData by lazy { getTopicList() } + private val topicLiveData: LiveData by lazy { getTopicList() } - private fun getTopicList(): LiveData { + private fun getTopicList(): LiveData { return Transformations.map(topicResultLiveData, ::processTopicResult) } - private val topicResultLiveData: LiveData> by lazy { + private val topicResultLiveData: LiveData> by lazy { topicController.getTopic( ProfileId.newBuilder().setInternalId(internalProfileId).build(), topicId ).toLiveData() } - private fun processTopicResult(topic: AsyncResult): Topic { - return when (topic) { + private fun processTopicResult(ephemeralResult: AsyncResult): EphemeralTopic { + return when (ephemeralResult) { is AsyncResult.Failure -> { - oppiaLogger.e("TopicLessonFragment", "Failed to retrieve topic", topic.error) - Topic.getDefaultInstance() + oppiaLogger.e("TopicLessonFragment", "Failed to retrieve topic", ephemeralResult.error) + EphemeralTopic.getDefaultInstance() } - is AsyncResult.Pending -> Topic.getDefaultInstance() - is AsyncResult.Success -> topic.value + is AsyncResult.Pending -> EphemeralTopic.getDefaultInstance() + is AsyncResult.Success -> ephemeralResult.value } } - private fun processTopic(topic: Topic): List { - if (topic.storyList.isNotEmpty()) { - topicStoryList = topic.storyList + private fun processTopic(ephemeralTopic: EphemeralTopic): List { + if (ephemeralTopic.storiesList.isNotEmpty()) { + topicStoryList = ephemeralTopic.topic.storyList itemList.clear() itemList.add(TopicLessonsTitleViewModel()) - for (storySummary in topic.storyList) { + for (ephemeralStorySummary in ephemeralTopic.storiesList) { itemList.add( StorySummaryViewModel( - storySummary, + ephemeralStorySummary, fragment as StorySummarySelector, fragment as ChapterSummarySelector, - resourceHandler + resourceHandler, + translationController ) ) } diff --git a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt index 5bb654e2550..55347f05b0e 100644 --- a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt @@ -10,6 +10,7 @@ import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.home.RouteToExplorationListener import org.oppia.android.app.model.ChapterPlayState import org.oppia.android.app.model.ChapterSummary +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.StorySummary @@ -23,6 +24,7 @@ import org.oppia.android.databinding.TopicLessonsTitleBinding import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.util.accessibility.AccessibilityService import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -34,16 +36,15 @@ class TopicLessonsFragmentPresenter @Inject constructor( private val fragment: Fragment, private val oppiaLogger: OppiaLogger, private val explorationDataController: ExplorationDataController, - private val explorationCheckpointController: ExplorationCheckpointController + private val explorationCheckpointController: ExplorationCheckpointController, + private val topicLessonViewModel: TopicLessonViewModel, + private val accessibilityService: AccessibilityService ) { private val routeToResumeLessonListener = activity as RouteToResumeLessonListener private val routeToExplorationListener = activity as RouteToExplorationListener private val routeToStoryListener = activity as RouteToStoryListener - @Inject - lateinit var topicLessonViewModel: TopicLessonViewModel - private var currentExpandedChapterListIndex: Int? = null private lateinit var binding: TopicLessonsFragmentBinding @@ -180,28 +181,44 @@ class TopicLessonsFragmentPresenter @Inject constructor( ) binding.chapterRecyclerView.adapter = createChapterRecyclerViewAdapter() + binding.expandListIcon.setOnClickListener { + expandStoryList(position) + } + binding.root.setOnClickListener { - val previousIndex: Int? = currentExpandedChapterListIndex - currentExpandedChapterListIndex = - if (currentExpandedChapterListIndex != null && - currentExpandedChapterListIndex == position - ) { - null - } else { - position - } - expandedChapterListIndexListener.onExpandListIconClicked(currentExpandedChapterListIndex) - if (previousIndex != null && currentExpandedChapterListIndex != null && - previousIndex == currentExpandedChapterListIndex + expandStoryList(position) + } + + if (accessibilityService.isScreenReaderEnabled()) { + binding.root.isClickable = false + binding.expandListIcon.isClickable = true + } else { + binding.root.isClickable = true + binding.expandListIcon.isClickable = false + } + } + + private fun expandStoryList(position: Int) { + val previousIndex: Int? = currentExpandedChapterListIndex + currentExpandedChapterListIndex = + if (currentExpandedChapterListIndex != null && + currentExpandedChapterListIndex == position ) { - bindingAdapter.notifyItemChanged(currentExpandedChapterListIndex!!) + null } else { - previousIndex?.let { - bindingAdapter.notifyItemChanged(previousIndex) - } - currentExpandedChapterListIndex?.let { - bindingAdapter.notifyItemChanged(currentExpandedChapterListIndex!!) - } + position + } + expandedChapterListIndexListener.onExpandListIconClicked(currentExpandedChapterListIndex) + if (previousIndex != null && currentExpandedChapterListIndex != null && + previousIndex == currentExpandedChapterListIndex + ) { + bindingAdapter.notifyItemChanged(currentExpandedChapterListIndex!!) + } else { + previousIndex?.let { + bindingAdapter.notifyItemChanged(previousIndex) + } + currentExpandedChapterListIndex?.let { + bindingAdapter.notifyItemChanged(currentExpandedChapterListIndex!!) } } } @@ -224,6 +241,9 @@ class TopicLessonsFragmentPresenter @Inject constructor( explorationId: String, chapterPlayState: ChapterPlayState ) { + val profileId = ProfileId.newBuilder().apply { + internalId = internalProfileId + }.build() val canHavePartialProgressSaved = when (chapterPlayState) { ChapterPlayState.IN_PROGRESS_SAVED, ChapterPlayState.IN_PROGRESS_NOT_SAVED, @@ -237,10 +257,7 @@ class TopicLessonsFragmentPresenter @Inject constructor( ChapterPlayState.IN_PROGRESS_SAVED -> { val explorationCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - ProfileId.newBuilder().apply { - internalId = internalProfileId - }.build(), - explorationId + profileId, explorationId ).toLiveData() explorationCheckpointLiveData.observe( fragment, @@ -249,17 +266,17 @@ class TopicLessonsFragmentPresenter @Inject constructor( if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) routeToResumeLessonListener.routeToResumeLesson( - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen = 0, + parentScreen = ExplorationActivityParams.ParentScreen.TOPIC_SCREEN_LESSONS_TAB, explorationCheckpoint = it.value ) } else if (it is AsyncResult.Failure) { explorationCheckpointLiveData.removeObserver(this) playExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, @@ -273,7 +290,7 @@ class TopicLessonsFragmentPresenter @Inject constructor( } ChapterPlayState.IN_PROGRESS_NOT_SAVED -> { playExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, @@ -283,7 +300,7 @@ class TopicLessonsFragmentPresenter @Inject constructor( } else -> { playExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, @@ -295,7 +312,7 @@ class TopicLessonsFragmentPresenter @Inject constructor( } private fun playExploration( - internalProfileId: Int, + profileId: ProfileId, topicId: String, storyId: String, explorationId: String, @@ -332,11 +349,11 @@ class TopicLessonsFragmentPresenter @Inject constructor( is AsyncResult.Success -> { oppiaLogger.d("TopicLessonsFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( - internalProfileId, + profileId, topicId, storyId, explorationId, - backflowScreen = 0, + parentScreen = ExplorationActivityParams.ParentScreen.TOPIC_SCREEN_LESSONS_TAB, isCheckpointingEnabled = canHavePartialProgressSaved ) } diff --git a/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentPresenter.kt index edee0861394..3c8390d0afc 100644 --- a/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentPresenter.kt @@ -13,7 +13,6 @@ import org.oppia.android.app.topic.practice.practiceitemviewmodel.TopicPracticeF import org.oppia.android.app.topic.practice.practiceitemviewmodel.TopicPracticeHeaderViewModel import org.oppia.android.app.topic.practice.practiceitemviewmodel.TopicPracticeItemViewModel import org.oppia.android.app.topic.practice.practiceitemviewmodel.TopicPracticeSubtopicViewModel -import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.TopicPracticeFooterViewBinding import org.oppia.android.databinding.TopicPracticeFragmentBinding import org.oppia.android.databinding.TopicPracticeHeaderViewBinding @@ -27,7 +26,7 @@ class TopicPracticeFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val oppiaLogger: OppiaLogger, - private val viewModelProvider: ViewModelProvider + private val viewModel: TopicPracticeViewModel ) : SubtopicSelector { private lateinit var binding: TopicPracticeFragmentBinding private lateinit var linearLayoutManager: LinearLayoutManager @@ -45,7 +44,6 @@ class TopicPracticeFragmentPresenter @Inject constructor( internalProfileId: Int, topicId: String ): View? { - val viewModel = getTopicPracticeViewModel() this.topicId = topicId viewModel.setTopicId(this.topicId) viewModel.setInternalProfileId(internalProfileId) @@ -58,6 +56,11 @@ class TopicPracticeFragmentPresenter @Inject constructor( /* attachToRoot= */ false ) + binding.apply { + this.viewModel = this@TopicPracticeFragmentPresenter.viewModel + lifecycleOwner = fragment + } + linearLayoutManager = LinearLayoutManager(activity.applicationContext) binding.topicPracticeSkillList.apply { @@ -65,10 +68,6 @@ class TopicPracticeFragmentPresenter @Inject constructor( adapter = createRecyclerViewAdapter() } - binding.apply { - this.viewModel = viewModel - lifecycleOwner = fragment - } return binding.root } @@ -134,10 +133,6 @@ class TopicPracticeFragmentPresenter @Inject constructor( } } - private fun getTopicPracticeViewModel(): TopicPracticeViewModel { - return viewModelProvider.getForFragment(fragment, TopicPracticeViewModel::class.java) - } - private enum class ViewType { VIEW_TYPE_HEADER, VIEW_TYPE_SKILL, diff --git a/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt index 16f7e7777ff..bbb73a939ab 100644 --- a/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt @@ -3,8 +3,8 @@ package org.oppia.android.app.topic.practice import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralTopic import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.Topic import org.oppia.android.app.topic.practice.practiceitemviewmodel.TopicPracticeFooterViewModel import org.oppia.android.app.topic.practice.practiceitemviewmodel.TopicPracticeHeaderViewModel import org.oppia.android.app.topic.practice.practiceitemviewmodel.TopicPracticeItemViewModel @@ -12,6 +12,7 @@ import org.oppia.android.app.topic.practice.practiceitemviewmodel.TopicPracticeS import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -20,22 +21,23 @@ import javax.inject.Inject @FragmentScope class TopicPracticeViewModel @Inject constructor( private val oppiaLogger: OppiaLogger, - private val topicController: TopicController + private val topicController: TopicController, + private val translationController: TranslationController ) : ObservableViewModel() { private val itemViewModelList: MutableList = ArrayList() private lateinit var topicId: String private var internalProfileId: Int = -1 - private val topicResultLiveData: LiveData> by lazy { + private val topicResultLiveData: LiveData> by lazy { topicController.getTopic( ProfileId.newBuilder().setInternalId(internalProfileId).build(), topicId ).toLiveData() } - private val topicLiveData: LiveData by lazy { getTopicList() } + private val topicLiveData: LiveData by lazy { getTopicList() } - private fun getTopicList(): LiveData { + private fun getTopicList(): LiveData { return Transformations.map(topicResultLiveData, ::processTopicResult) } @@ -51,24 +53,26 @@ class TopicPracticeViewModel @Inject constructor( this.internalProfileId = internalProfileId } - private fun processTopicResult(topic: AsyncResult): Topic { - return when (topic) { + private fun processTopicResult(ephemeralResult: AsyncResult): EphemeralTopic { + return when (ephemeralResult) { is AsyncResult.Failure -> { - oppiaLogger.e("TopicPracticeFragment", "Failed to retrieve topic", topic.error) - Topic.getDefaultInstance() + oppiaLogger.e("TopicPracticeFragment", "Failed to retrieve topic", ephemeralResult.error) + EphemeralTopic.getDefaultInstance() } - is AsyncResult.Pending -> Topic.getDefaultInstance() - is AsyncResult.Success -> topic.value + is AsyncResult.Pending -> EphemeralTopic.getDefaultInstance() + is AsyncResult.Success -> ephemeralResult.value } } - private fun processTopicPracticeSkillList(topic: Topic): List { + private fun processTopicPracticeSkillList( + ephemeralTopic: EphemeralTopic + ): List { itemViewModelList.clear() itemViewModelList.add(TopicPracticeHeaderViewModel() as TopicPracticeItemViewModel) itemViewModelList.addAll( - topic.subtopicList.map { subtopic -> - TopicPracticeSubtopicViewModel(subtopic) as TopicPracticeItemViewModel + ephemeralTopic.subtopicsList.map { ephemeralSubtopic -> + TopicPracticeSubtopicViewModel(ephemeralSubtopic, translationController) } ) diff --git a/app/src/main/java/org/oppia/android/app/topic/practice/practiceitemviewmodel/TopicPracticeSubtopicViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/practice/practiceitemviewmodel/TopicPracticeSubtopicViewModel.kt index 4774cde269a..d380f3aa79e 100644 --- a/app/src/main/java/org/oppia/android/app/topic/practice/practiceitemviewmodel/TopicPracticeSubtopicViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/practice/practiceitemviewmodel/TopicPracticeSubtopicViewModel.kt @@ -1,6 +1,19 @@ package org.oppia.android.app.topic.practice.practiceitemviewmodel +import org.oppia.android.app.model.EphemeralSubtopic import org.oppia.android.app.model.Subtopic +import org.oppia.android.domain.translation.TranslationController /** Subtopic view model for the recycler view in [TopicPracticeFragment]. */ -class TopicPracticeSubtopicViewModel(val subtopic: Subtopic) : TopicPracticeItemViewModel() +class TopicPracticeSubtopicViewModel( + ephemeralSubtopic: EphemeralSubtopic, + translationController: TranslationController +) : TopicPracticeItemViewModel() { + /** The subtopic being displayed. */ + val subtopic: Subtopic = ephemeralSubtopic.subtopic + + /** The localized title of the subtopic being displayed. */ + val subtopicTitle by lazy { + translationController.extractString(subtopic.title, ephemeralSubtopic.writtenTranslationContext) + } +} diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt index d4153be4460..e586b89ba6b 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivity.kt @@ -11,6 +11,7 @@ import org.oppia.android.app.hintsandsolution.RevealHintListener import org.oppia.android.app.hintsandsolution.RevealSolutionInterface import org.oppia.android.app.model.HelpIndex import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.ScreenName.QUESTION_PLAYER_ACTIVITY import org.oppia.android.app.model.State import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.exploration.TAG_HINTS_AND_SOLUTION_DIALOG @@ -22,6 +23,7 @@ import org.oppia.android.app.player.stopplaying.StopStatePlayingSessionListener import org.oppia.android.app.topic.conceptcard.ConceptCardListener import org.oppia.android.util.extensions.getProtoExtra import org.oppia.android.util.extensions.putProtoExtra +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject private const val QUESTION_PLAYER_ACTIVITY_PROFILE_ID_ARGUMENT_KEY = @@ -93,6 +95,7 @@ class QuestionPlayerActivity : return Intent(context, QuestionPlayerActivity::class.java).apply { putProtoExtra(QUESTION_PLAYER_ACTIVITY_PROFILE_ID_ARGUMENT_KEY, profileId) putExtra(QUESTION_PLAYER_ACTIVITY_SKILL_ID_LIST_ARGUMENT_KEY, skillIdList) + decorateWithScreenName(QUESTION_PLAYER_ACTIVITY) } } } diff --git a/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentPresenter.kt index 91e164c271e..2d8e37227c6 100755 --- a/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentPresenter.kt @@ -12,7 +12,6 @@ import org.oppia.android.app.model.Subtopic import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.app.topic.RouteToRevisionCardListener import org.oppia.android.app.topic.revision.revisionitemviewmodel.TopicRevisionItemViewModel -import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.TopicRevisionFragmentBinding import org.oppia.android.databinding.TopicRevisionSummaryViewBinding import javax.inject.Inject @@ -22,7 +21,7 @@ import javax.inject.Inject class TopicRevisionFragmentPresenter @Inject constructor( activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider + private val viewModel: TopicRevisionViewModel ) : RevisionSubtopicSelector { private lateinit var binding: TopicRevisionFragmentBinding private var internalProfileId: Int = -1 @@ -35,8 +34,6 @@ class TopicRevisionFragmentPresenter @Inject constructor( internalProfileId: Int, topicId: String ): View? { - val viewModel = getTopicRevisionViewModel() - this.internalProfileId = internalProfileId this.topicId = topicId binding = TopicRevisionFragmentBinding.inflate( @@ -55,7 +52,7 @@ class TopicRevisionFragmentPresenter @Inject constructor( layoutManager = GridLayoutManager(context, spanCount) } binding.apply { - this.viewModel = viewModel + this.viewModel = this@TopicRevisionFragmentPresenter.viewModel lifecycleOwner = fragment } return binding.root @@ -65,10 +62,6 @@ class TopicRevisionFragmentPresenter @Inject constructor( routeToReviewListener.routeToRevisionCard(internalProfileId, topicId, subtopic.subtopicId) } - private fun getTopicRevisionViewModel(): TopicRevisionViewModel { - return viewModelProvider.getForFragment(fragment, TopicRevisionViewModel::class.java) - } - private fun createRecyclerViewAdapter(): BindableAdapter { return BindableAdapter.SingleTypeBuilder .newBuilder() diff --git a/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt index a1574b5ca5b..9018a41ea9a 100755 --- a/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt @@ -4,12 +4,13 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralTopic import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.Topic import org.oppia.android.app.topic.revision.revisionitemviewmodel.TopicRevisionItemViewModel import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.parser.html.TopicHtmlParserEntityType @@ -21,6 +22,7 @@ class TopicRevisionViewModel @Inject constructor( private val topicController: TopicController, private val oppiaLogger: OppiaLogger, val fragment: Fragment, + private val translationController: TranslationController, @TopicHtmlParserEntityType private val entityType: String ) : ObservableViewModel() { private lateinit var profileId: ProfileId @@ -29,38 +31,40 @@ class TopicRevisionViewModel @Inject constructor( private val revisionSubtopicSelector: RevisionSubtopicSelector = fragment as RevisionSubtopicSelector - private val topicResultLiveData: LiveData> by lazy { + private val topicResultLiveData: LiveData> by lazy { topicController.getTopic(profileId, topicId).toLiveData() } - private val topicLiveData: LiveData by lazy { getTopicList() } + private val topicLiveData: LiveData by lazy { getTopicList() } val subtopicLiveData: LiveData> by lazy { Transformations.map(topicLiveData, ::processTopic) } - private fun processTopic(topic: Topic): List { + private fun processTopic(ephemeralTopic: EphemeralTopic): List { subtopicList.clear() subtopicList.addAll( - topic.subtopicList.map { - TopicRevisionItemViewModel(topicId, it, entityType, revisionSubtopicSelector) + ephemeralTopic.subtopicsList.map { + TopicRevisionItemViewModel( + topicId, it, entityType, revisionSubtopicSelector, translationController + ) } ) return subtopicList } - private fun getTopicList(): LiveData { + private fun getTopicList(): LiveData { return Transformations.map(topicResultLiveData, ::processTopicResult) } - private fun processTopicResult(topic: AsyncResult): Topic { - return when (topic) { + private fun processTopicResult(ephemeralResult: AsyncResult): EphemeralTopic { + return when (ephemeralResult) { is AsyncResult.Failure -> { - oppiaLogger.e("TopicRevisionFragment", "Failed to retrieve topic", topic.error) - Topic.getDefaultInstance() + oppiaLogger.e("TopicRevisionFragment", "Failed to retrieve topic", ephemeralResult.error) + EphemeralTopic.getDefaultInstance() } - is AsyncResult.Pending -> Topic.getDefaultInstance() - is AsyncResult.Success -> topic.value + is AsyncResult.Pending -> EphemeralTopic.getDefaultInstance() + is AsyncResult.Success -> ephemeralResult.value } } diff --git a/app/src/main/java/org/oppia/android/app/topic/revision/revisionitemviewmodel/TopicRevisionItemViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/revision/revisionitemviewmodel/TopicRevisionItemViewModel.kt index 8b126e61937..ec8b1a5e037 100644 --- a/app/src/main/java/org/oppia/android/app/topic/revision/revisionitemviewmodel/TopicRevisionItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revision/revisionitemviewmodel/TopicRevisionItemViewModel.kt @@ -1,13 +1,23 @@ package org.oppia.android.app.topic.revision.revisionitemviewmodel -import org.oppia.android.app.model.Subtopic +import org.oppia.android.app.model.EphemeralSubtopic import org.oppia.android.app.topic.revision.RevisionSubtopicSelector import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.domain.translation.TranslationController /** [ObservableViewModel] for child views of recycler view present in the [TopicRevisionFragment]. */ class TopicRevisionItemViewModel( val topicId: String, - val subtopic: Subtopic, + ephemeralSubtopic: EphemeralSubtopic, val entityType: String, - val onRevisionItemPressed: RevisionSubtopicSelector -) : ObservableViewModel() + val onRevisionItemPressed: RevisionSubtopicSelector, + translationController: TranslationController +) : ObservableViewModel() { + /** The subtopic being displayed. */ + val subtopic = ephemeralSubtopic.subtopic + + /** The localized title of the subtopic being displayed. */ + val subtopicTitle by lazy { + translationController.extractString(subtopic.title, ephemeralSubtopic.writtenTranslationContext) + } +} diff --git a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivity.kt b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivity.kt index f20936edafb..00dc43b941d 100644 --- a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivity.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivity.kt @@ -8,7 +8,9 @@ import android.view.MenuItem import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.REVISION_CARD_ACTIVITY import org.oppia.android.app.topic.conceptcard.ConceptCardListener +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity for revision card. */ @@ -52,11 +54,12 @@ class RevisionCardActivity : topicId: String, subtopicId: Int ): Intent { - val intent = Intent(context, RevisionCardActivity::class.java) - intent.putExtra(INTERNAL_PROFILE_ID_EXTRA_KEY, internalProfileId) - intent.putExtra(TOPIC_ID_EXTRA_KEY, topicId) - intent.putExtra(SUBTOPIC_ID_EXTRA_KEY, subtopicId) - return intent + return Intent(context, RevisionCardActivity::class.java).apply { + putExtra(INTERNAL_PROFILE_ID_EXTRA_KEY, internalProfileId) + putExtra(TOPIC_ID_EXTRA_KEY, topicId) + putExtra(SUBTOPIC_ID_EXTRA_KEY, subtopicId) + decorateWithScreenName(REVISION_CARD_ACTIVITY) + } } } diff --git a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt index 78ce25bfb86..514c6d066a0 100644 --- a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt @@ -17,6 +17,7 @@ import org.oppia.android.app.options.OptionsActivity import org.oppia.android.databinding.RevisionCardActivityBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -26,7 +27,8 @@ import javax.inject.Inject class RevisionCardActivityPresenter @Inject constructor( private val activity: AppCompatActivity, private val oppiaLogger: OppiaLogger, - private val topicController: TopicController + private val topicController: TopicController, + private val translationController: TranslationController ) { private lateinit var revisionCardToolbar: Toolbar @@ -129,7 +131,10 @@ class RevisionCardActivityPresenter @Inject constructor( is AsyncResult.Pending -> EphemeralRevisionCard.getDefaultInstance() is AsyncResult.Success -> revisionCardResult.value } - return ephemeralRevisionCard.revisionCard.subtopicTitle + return translationController.extractString( + ephemeralRevisionCard.revisionCard.subtopicTitle, + ephemeralRevisionCard.writtenTranslationContext + ) } private fun getReviewCardFragment(): RevisionCardFragment? { diff --git a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentPresenter.kt index f83db2ce970..35d2c496e22 100755 --- a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentPresenter.kt @@ -8,6 +8,7 @@ import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId import org.oppia.android.app.topic.conceptcard.ConceptCardFragment import org.oppia.android.app.topic.conceptcard.ConceptCardFragment.Companion.CONCEPT_CARD_DIALOG_FRAGMENT_TAG +import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.RevisionCardFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger @@ -26,7 +27,8 @@ class RevisionCardFragmentPresenter @Inject constructor( @DefaultResourceBucketName private val resourceBucketName: String, @TopicHtmlParserEntityType private val entityType: String, private val viewModelProvider: ViewModelProvider, - private val translationController: TranslationController + private val translationController: TranslationController, + private val appLanguageResourceHandler: AppLanguageResourceHandler ) : HtmlParser.CustomOppiaTagActionListener { private lateinit var profileId: ProfileId @@ -66,7 +68,8 @@ class RevisionCardFragmentPresenter @Inject constructor( ) view.text = htmlParserFactory.create( resourceBucketName, entityType, topicId, imageCenterAlign = true, - customOppiaTagActionListener = this + customOppiaTagActionListener = this, + displayLocale = appLanguageResourceHandler.getDisplayLocale() ).parseOppiaHtml( pageContentsHtml, view, supportsLinks = true, supportsConceptCards = true ) diff --git a/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt b/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt index 4887f41175a..d659c6997a3 100644 --- a/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt +++ b/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt @@ -4,7 +4,9 @@ import androidx.annotation.ArrayRes import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity +import org.oppia.android.app.model.AudioLanguage import org.oppia.android.util.locale.OppiaLocale +import java.util.Locale import javax.inject.Inject /** @@ -132,6 +134,31 @@ class AppLanguageResourceHandler @Inject constructor( /** See [OppiaLocale.DisplayLocale.getLayoutDirection]. */ fun getLayoutDirection(): Int = getDisplayLocale().getLayoutDirection() - private fun getDisplayLocale(): OppiaLocale.DisplayLocale = - appLanguageLocaleHandler.getDisplayLocale() + /** Returns the current [OppiaLocale.DisplayLocale] used for resource processing. */ + fun getDisplayLocale(): OppiaLocale.DisplayLocale = appLanguageLocaleHandler.getDisplayLocale() + + // TODO(#3793): Remove this once OppiaLanguage is used as the source of truth. + /** + * Returns a human-readable, localized representation of the specified [AudioLanguage]. + * + * Note that the returned string is not expected to be localized to the user's current locale. + * Instead, it will be localized for that specific language (i.e. each language will be + * represented within that language to make it easier to identify when choosing a language). + */ + fun computeLocalizedDisplayName(audioLanguage: AudioLanguage): String { + return when (audioLanguage) { + AudioLanguage.HINDI_AUDIO_LANGUAGE -> getLocalizedDisplayName("hi") + AudioLanguage.FRENCH_AUDIO_LANGUAGE -> getLocalizedDisplayName("fr") + AudioLanguage.CHINESE_AUDIO_LANGUAGE -> getLocalizedDisplayName("zh") + AudioLanguage.BRAZILIAN_PORTUGUESE_LANGUAGE -> getLocalizedDisplayName("pt", "BR") + AudioLanguage.NO_AUDIO, AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED, AudioLanguage.UNRECOGNIZED, + AudioLanguage.ENGLISH_AUDIO_LANGUAGE -> getLocalizedDisplayName("en") + } + } + + private fun getLocalizedDisplayName(languageCode: String, regionCode: String = ""): String { + // TODO(#3791): Remove this dependency. + val locale = Locale(languageCode, regionCode) + return locale.getDisplayLanguage(locale).capitalize(locale) + } } diff --git a/app/src/main/java/org/oppia/android/app/translation/BUILD.bazel b/app/src/main/java/org/oppia/android/app/translation/BUILD.bazel index 3fb8eb46d02..e5bc20b3d81 100644 --- a/app/src/main/java/org/oppia/android/app/translation/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/translation/BUILD.bazel @@ -120,6 +120,7 @@ kt_android_library( ], visibility = [ "//app/src/main/java/org/oppia/android/app/activity:__pkg__", + "//app/src/main/java/org/oppia/android/app/application:__pkg__", ], deps = [ ":app_language_locale_handler", @@ -133,6 +134,7 @@ kt_android_library( ], visibility = [ "//app/src/main/java/org/oppia/android/app/activity:__pkg__", + "//app/src/main/java/org/oppia/android/app/application:__pkg__", ], deps = [ ":app_language_application_injector", diff --git a/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt b/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt index a1793e9656d..30ba360afa7 100644 --- a/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt +++ b/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt @@ -136,6 +136,7 @@ class ClickableAreasImage( showOrHideRegion(newView, clickableArea) } } + newView.contentDescription = clickableArea.contentDescription it.addView(newView) } @@ -158,7 +159,12 @@ class ClickableAreasImage( private fun showOrHideRegion(newView: View, clickableArea: ImageWithRegions.LabeledRegion) { resetRegionSelectionViews() - listener.onClickableAreaTouched(NamedRegionClickedEvent(clickableArea.label)) + listener.onClickableAreaTouched( + NamedRegionClickedEvent( + clickableArea.label, + clickableArea.contentDescription + ) + ) newView.setBackgroundResource(R.drawable.selected_region_background) } } diff --git a/app/src/main/java/org/oppia/android/app/utility/FontScaleConfigurationUtil.kt b/app/src/main/java/org/oppia/android/app/utility/FontScaleConfigurationUtil.kt index f0bcf1bdb9a..78731ab1ce5 100644 --- a/app/src/main/java/org/oppia/android/app/utility/FontScaleConfigurationUtil.kt +++ b/app/src/main/java/org/oppia/android/app/utility/FontScaleConfigurationUtil.kt @@ -9,11 +9,11 @@ import javax.inject.Inject /** Utility to change the scale of font for the entire app. */ class FontScaleConfigurationUtil @Inject constructor() { - /** This method updates font scale. */ - fun adjustFontScale( - context: Context, - readingTextSize: String - ) { + /** + * Updates the specified [context]'s current configuration to scale text size according to the + * provided [readingTextSize]. + */ + fun adjustFontScale(context: Context, readingTextSize: ReadingTextSize) { val configuration = context.resources.configuration configuration.fontScale = getReadingTextSizeConfigurationUtil(readingTextSize) val metrics: DisplayMetrics = context.resources.displayMetrics @@ -24,13 +24,12 @@ class FontScaleConfigurationUtil @Inject constructor() { context.resources.displayMetrics.setTo(metrics) } - /** This method returns font scale by reading text size. */ - private fun getReadingTextSizeConfigurationUtil(readingTextSize: String): Float { + private fun getReadingTextSizeConfigurationUtil(readingTextSize: ReadingTextSize): Float { return when (readingTextSize) { - ReadingTextSize.SMALL_TEXT_SIZE.name -> .8f - ReadingTextSize.MEDIUM_TEXT_SIZE.name -> 1.0f - ReadingTextSize.LARGE_TEXT_SIZE.name -> 1.2f - ReadingTextSize.EXTRA_LARGE_TEXT_SIZE.name -> 1.4f + ReadingTextSize.SMALL_TEXT_SIZE -> .8f + ReadingTextSize.MEDIUM_TEXT_SIZE -> 1.0f + ReadingTextSize.LARGE_TEXT_SIZE -> 1.2f + ReadingTextSize.EXTRA_LARGE_TEXT_SIZE -> 1.4f else -> 1.0f } } diff --git a/app/src/main/java/org/oppia/android/app/utility/LifecycleSafeTimerFactory.kt b/app/src/main/java/org/oppia/android/app/utility/LifecycleSafeTimerFactory.kt index 92ac3d421c0..70c71ec8d81 100644 --- a/app/src/main/java/org/oppia/android/app/utility/LifecycleSafeTimerFactory.kt +++ b/app/src/main/java/org/oppia/android/app/utility/LifecycleSafeTimerFactory.kt @@ -1,7 +1,9 @@ package org.oppia.android.app.utility +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -36,4 +38,48 @@ class LifecycleSafeTimerFactory @Inject constructor( } return liveData } + + /** + * A convenience version of [createTimer] that runs the specified [block] for this + * [LifecycleOwner] with the specified millisecond delay of [delayMillis]. + * + * Note that [block] is guaranteed to run at a lifecycle-safe time, though it will be run on the + * main thread so it should not perform any I/O or expensive operations. + */ + fun LifecycleOwner.runWithDelay(delayMillis: Long, block: () -> Unit) { + val liveData = createTimer(delayMillis) + liveData.observe( + this, + object : Observer { + override fun onChanged(value: Any?) { + liveData.removeObserver(this) + block() + } + } + ) + } + + /** + * Runs a [block] of code with an initial delay of [delayMillis] (default 0) at a periodic rate of + * [periodMillis] milliseconds for this [LifecycleOwner]. + * + * Like [runWithDelay], [block] is run at a lifecycle-safe time on the main thread. The loop will + * continue until either the [LifecycleOwner]'s lifecycle ends, or [block] returns false. + * + * Note that [periodMillis] is how much time should be run between calls to [block]. If [block] + * schedules something that will happen in parallel (such as an animation), then the period time + * will likely represent the time between each animation start, rather than between one animation + * end and the next start. + */ + fun LifecycleOwner.runPeriodically( + delayMillis: Long = 0, + periodMillis: Long, + block: () -> Boolean + ) { + runWithDelay(delayMillis) { + if (block()) { + runPeriodically(periodMillis, periodMillis, block) + } + } + } } diff --git a/app/src/main/java/org/oppia/android/app/utility/RegionClickEvent.kt b/app/src/main/java/org/oppia/android/app/utility/RegionClickEvent.kt index 7df8bad6fbb..59ae1a2445c 100644 --- a/app/src/main/java/org/oppia/android/app/utility/RegionClickEvent.kt +++ b/app/src/main/java/org/oppia/android/app/utility/RegionClickEvent.kt @@ -6,9 +6,12 @@ sealed class RegionClickedEvent /** * Class to be used in case when [OnClickableAreaClickedListener] is called with an specified region. * - * @param regionLabel region name for the which [OnClickableAreaClickedListener] was called for. + * @property regionLabel region name for the which [OnClickableAreaClickedListener] was called for. + * @property contentDescription content description for which [OnClickableAreaClickedListener] was + * called for. */ -data class NamedRegionClickedEvent(val regionLabel: String) : RegionClickedEvent() +data class NamedRegionClickedEvent(val regionLabel: String, val contentDescription: String) : + RegionClickedEvent() /** * Class to be used in case when [OnClickableAreaClickedListener] is called with an unspecified diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/WalkthroughActivity.kt b/app/src/main/java/org/oppia/android/app/walkthrough/WalkthroughActivity.kt index 55ecca58477..475f9e5096c 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/WalkthroughActivity.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/WalkthroughActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.ScreenName.WALKTHROUGH_ACTIVITY +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject /** Activity that contains the walkthrough flow for users. */ @@ -36,9 +38,10 @@ class WalkthroughActivity : InjectableAppCompatActivity(), WalkthroughFragmentCh "WalkthroughActivity.internal_profile_id" fun createWalkthroughActivityIntent(context: Context, internalProfileId: Int): Intent { - val intent = Intent(context, WalkthroughActivity::class.java) - intent.putExtra(WALKTHROUGH_ACTIVITY_INTERNAL_PROFILE_ID_KEY, internalProfileId) - return intent + return Intent(context, WalkthroughActivity::class.java).apply { + putExtra(WALKTHROUGH_ACTIVITY_INTERNAL_PROFILE_ID_KEY, internalProfileId) + decorateWithScreenName(WALKTHROUGH_ACTIVITY) + } } } } diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt index a49ae9378ea..1b2658ba0ff 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt @@ -6,17 +6,17 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.lifecycle.Transformations import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.EphemeralTopic import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.Topic import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.walkthrough.WalkthroughActivity import org.oppia.android.databinding.WalkthroughFinalFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -28,7 +28,8 @@ class WalkthroughFinalFragmentPresenter @Inject constructor( private val fragment: Fragment, private val oppiaLogger: OppiaLogger, private val topicController: TopicController, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : WalkthroughEndPageChanger { private lateinit var binding: WalkthroughFinalFragmentBinding private lateinit var walkthroughFinalViewModel: WalkthroughFinalViewModel @@ -61,13 +62,14 @@ class WalkthroughFinalFragmentPresenter @Inject constructor( return binding.root } - private val topicLiveData: LiveData by lazy { getTopic() } + private val topicLiveData: LiveData by lazy { getTopic() } private fun subscribeToTopicLiveData() { topicLiveData.observe( activity, - Observer { result -> - topicName = result.name + { result -> + topicName = + translationController.extractString(result.topic.title, result.writtenTranslationContext) setTopicName() } ) @@ -84,22 +86,22 @@ class WalkthroughFinalFragmentPresenter @Inject constructor( } } - private val topicResultLiveData: LiveData> by lazy { + private val topicResultLiveData: LiveData> by lazy { topicController.getTopic(profileId, topicId = topicId).toLiveData() } - private fun getTopic(): LiveData { + private fun getTopic(): LiveData { return Transformations.map(topicResultLiveData, ::processTopicResult) } - private fun processTopicResult(topic: AsyncResult): Topic { - return when (topic) { + private fun processTopicResult(ephemeralResult: AsyncResult): EphemeralTopic { + return when (ephemeralResult) { is AsyncResult.Failure -> { - oppiaLogger.e("WalkthroughFinalFragment", "Failed to retrieve topic", topic.error) - Topic.getDefaultInstance() + oppiaLogger.e("WalkthroughFinalFragment", "Failed to retrieve topic", ephemeralResult.error) + EphemeralTopic.getDefaultInstance() } - is AsyncResult.Pending -> Topic.getDefaultInstance() - is AsyncResult.Success -> topic.value + is AsyncResult.Pending -> EphemeralTopic.getDefaultInstance() + is AsyncResult.Success -> ephemeralResult.value } } diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicListFragmentPresenter.kt index deef5d17312..6bae04731dc 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicListFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicListFragmentPresenter.kt @@ -9,9 +9,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.recyclerview.widget.GridLayoutManager import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.TopicSummary import org.oppia.android.app.recyclerview.BindableAdapter -import org.oppia.android.app.viewmodel.ViewModelProvider +import org.oppia.android.app.walkthrough.WalkthroughActivity import org.oppia.android.app.walkthrough.WalkthroughFragmentChangeListener import org.oppia.android.app.walkthrough.WalkthroughPages import org.oppia.android.app.walkthrough.topiclist.topiclistviewmodel.WalkthroughTopicHeaderViewModel @@ -26,14 +27,19 @@ import javax.inject.Inject class WalkthroughTopicListFragmentPresenter @Inject constructor( val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider + private val viewModel: WalkthroughTopicViewModel ) { private lateinit var binding: WalkthroughTopicListFragmentBinding private val routeToNextPage = activity as WalkthroughFragmentChangeListener private val orientation = Resources.getSystem().configuration.orientation fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { - val viewModel = getWalkthroughTopicViewModel() + val profileId = ProfileId.newBuilder().apply { + internalId = activity.intent.getIntExtra( + WalkthroughActivity.WALKTHROUGH_ACTIVITY_INTERNAL_PROFILE_ID_KEY, /* defaultValue= */ -1 + ) + }.build() + viewModel.initialize(profileId) binding = WalkthroughTopicListFragmentBinding.inflate( @@ -96,10 +102,6 @@ class WalkthroughTopicListFragmentPresenter @Inject constructor( .build() } - private fun getWalkthroughTopicViewModel(): WalkthroughTopicViewModel { - return viewModelProvider.getForFragment(fragment, WalkthroughTopicViewModel::class.java) - } - private enum class ViewType { VIEW_TYPE_HEADER, VIEW_TYPE_TOPIC diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt index 19eb3fbea61..6090b4681ff 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt @@ -4,6 +4,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.oppia.android.app.home.topiclist.TopicSummaryClickListener +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.TopicList import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableViewModel @@ -11,6 +12,7 @@ import org.oppia.android.app.walkthrough.topiclist.topiclistviewmodel.Walkthroug import org.oppia.android.app.walkthrough.topiclist.topiclistviewmodel.WalkthroughTopicSummaryViewModel import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.parser.html.TopicHtmlParserEntityType @@ -22,20 +24,32 @@ class WalkthroughTopicViewModel @Inject constructor( private val topicListController: TopicListController, private val oppiaLogger: OppiaLogger, @TopicHtmlParserEntityType private val topicEntityType: String, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : ObservableViewModel() { + private lateinit var profileId: ProfileId + val walkthroughTopicViewModelLiveData: LiveData> by lazy { Transformations.map(topicListSummaryLiveData, ::processCompletedTopicList) } private val topicListSummaryResultLiveData: LiveData> by lazy { - topicListController.getTopicList().toLiveData() + topicListController.getTopicList(profileId).toLiveData() } private val topicListSummaryLiveData: LiveData by lazy { Transformations.map(topicListSummaryResultLiveData, ::processTopicListResult) } + /** + * Initializes this view model with the specified [profileId]. + * + * This MUST be called before the view model is interacted with. + */ + fun initialize(profileId: ProfileId) { + this.profileId = profileId + } + private fun processTopicListResult(topicSummaryListResult: AsyncResult): TopicList { return when (topicSummaryListResult) { is AsyncResult.Failure -> { @@ -59,12 +73,13 @@ class WalkthroughTopicViewModel @Inject constructor( // Add the rest of the list itemViewModelList.addAll( - topicList.topicSummaryList.map { topic -> + topicList.topicSummaryList.map { ephemeralTopicSummary -> WalkthroughTopicSummaryViewModel( topicEntityType, - topic, + ephemeralTopicSummary, fragment as TopicSummaryClickListener, - resourceHandler + resourceHandler, + translationController ) } ) diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/topiclistviewmodel/WalkthroughTopicSummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/topiclistviewmodel/WalkthroughTopicSummaryViewModel.kt index f4c3938365d..704a2eb6da7 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/topiclistviewmodel/WalkthroughTopicSummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/topiclistviewmodel/WalkthroughTopicSummaryViewModel.kt @@ -4,18 +4,26 @@ import androidx.annotation.ColorInt import androidx.lifecycle.ViewModel import org.oppia.android.R import org.oppia.android.app.home.topiclist.TopicSummaryClickListener -import org.oppia.android.app.model.TopicSummary +import org.oppia.android.app.model.EphemeralTopicSummary import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.walkthrough.topiclist.WalkthroughTopicItemViewModel +import org.oppia.android.domain.translation.TranslationController /** [ViewModel] corresponding to topic summaries in [WalkthroughTopicListFragment] RecyclerView.. */ class WalkthroughTopicSummaryViewModel( val topicEntityType: String, - val topicSummary: TopicSummary, + ephemeralTopicSummary: EphemeralTopicSummary, private val topicSummaryClickListener: TopicSummaryClickListener, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + translationController: TranslationController ) : WalkthroughTopicItemViewModel() { - val name: String = topicSummary.name + val topicSummary = ephemeralTopicSummary.topicSummary + + val name: String by lazy { + translationController.extractString( + topicSummary.title, ephemeralTopicSummary.writtenTranslationContext + ) + } @ColorInt val backgroundColor: Int = retrieveBackgroundColor() diff --git a/app/src/main/res/anim/hint_bulb_animation.xml b/app/src/main/res/anim/hint_bulb_animation.xml new file mode 100644 index 00000000000..427af92a614 --- /dev/null +++ b/app/src/main/res/anim/hint_bulb_animation.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/color/checkbox_text_color.xml b/app/src/main/res/color/checkbox_text_color.xml index dff1ece24e9..0224c0f546d 100644 --- a/app/src/main/res/color/checkbox_text_color.xml +++ b/app/src/main/res/color/checkbox_text_color.xml @@ -1,12 +1,12 @@ - - - diff --git a/app/src/main/res/color/drawer_item.xml b/app/src/main/res/color/drawer_item.xml index 647f48d5613..c34de39b494 100644 --- a/app/src/main/res/color/drawer_item.xml +++ b/app/src/main/res/color/drawer_item.xml @@ -1,5 +1,5 @@ - - + + diff --git a/app/src/main/res/drawable/audio_background.xml b/app/src/main/res/drawable/audio_background.xml index d20e2216dba..b809bfd7fa6 100644 --- a/app/src/main/res/drawable/audio_background.xml +++ b/app/src/main/res/drawable/audio_background.xml @@ -4,5 +4,5 @@ - + diff --git a/app/src/main/res/drawable/audio_language_availability_background.xml b/app/src/main/res/drawable/audio_language_availability_background.xml new file mode 100644 index 00000000000..5fd51aeab4c --- /dev/null +++ b/app/src/main/res/drawable/audio_language_availability_background.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/color_cursor.xml b/app/src/main/res/drawable/color_cursor.xml index 2f93b37ddcf..4573a4b96a4 100644 --- a/app/src/main/res/drawable/color_cursor.xml +++ b/app/src/main/res/drawable/color_cursor.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/drag_drop_white_background.xml b/app/src/main/res/drawable/drag_drop_white_background.xml index cc1e76f37ad..9fd8b941ae0 100644 --- a/app/src/main/res/drawable/drag_drop_white_background.xml +++ b/app/src/main/res/drawable/drag_drop_white_background.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/res/drawable/edit_text_background_border.xml b/app/src/main/res/drawable/edit_text_background_border.xml index 3dd64225973..4bc3a0fd077 100644 --- a/app/src/main/res/drawable/edit_text_background_border.xml +++ b/app/src/main/res/drawable/edit_text_background_border.xml @@ -2,8 +2,8 @@ - + + android:color="@color/component_color_shared_input_interaction_edit_text_not_selected_border_color" /> diff --git a/app/src/main/res/drawable/edit_text_background_border_blue.xml b/app/src/main/res/drawable/edit_text_background_border_blue.xml index 852ab5a8df3..8291c80cc18 100644 --- a/app/src/main/res/drawable/edit_text_background_border_blue.xml +++ b/app/src/main/res/drawable/edit_text_background_border_blue.xml @@ -2,8 +2,8 @@ - + + android:color="@color/component_color_shared_input_interaction_edit_text_border_color" /> diff --git a/app/src/main/res/drawable/full_oppia_logo.xml b/app/src/main/res/drawable/full_oppia_logo.xml new file mode 100644 index 00000000000..f99bc2df503 --- /dev/null +++ b/app/src/main/res/drawable/full_oppia_logo.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/general_item_background_border_light.xml b/app/src/main/res/drawable/general_item_background_border_light.xml index f24bf5a9194..8805ff234e9 100644 --- a/app/src/main/res/drawable/general_item_background_border_light.xml +++ b/app/src/main/res/drawable/general_item_background_border_light.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/main/res/drawable/ic_arrow_down_grey_24dp.xml b/app/src/main/res/drawable/ic_arrow_down_grey_24dp.xml index 59518af0471..d96f89eef1e 100644 --- a/app/src/main/res/drawable/ic_arrow_down_grey_24dp.xml +++ b/app/src/main/res/drawable/ic_arrow_down_grey_24dp.xml @@ -1,6 +1,7 @@ diff --git a/app/src/main/res/drawable/ic_arrow_right_grey_24dp.xml b/app/src/main/res/drawable/ic_arrow_right_grey_24dp.xml index 9053c17788f..bbdcc03ccde 100644 --- a/app/src/main/res/drawable/ic_arrow_right_grey_24dp.xml +++ b/app/src/main/res/drawable/ic_arrow_right_grey_24dp.xml @@ -1,6 +1,7 @@ - - diff --git a/app/src/main/res/drawable/ic_audio_streaming_on_24dp.xml b/app/src/main/res/drawable/ic_audio_streaming_on_24dp.xml deleted file mode 100644 index 3d1f3cd9db2..00000000000 --- a/app/src/main/res/drawable/ic_audio_streaming_on_24dp.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_baseline_headset_24.xml b/app/src/main/res/drawable/ic_baseline_headset_24.xml new file mode 100644 index 00000000000..2e70e369c18 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_headset_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_headset_off_24.xml b/app/src/main/res/drawable/ic_baseline_headset_off_24.xml new file mode 100644 index 00000000000..57fc8016896 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_headset_off_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_box_checked.xml b/app/src/main/res/drawable/ic_check_box_checked.xml index 85ca608c033..f18efa940af 100644 --- a/app/src/main/res/drawable/ic_check_box_checked.xml +++ b/app/src/main/res/drawable/ic_check_box_checked.xml @@ -1,4 +1,4 @@ - diff --git a/app/src/main/res/drawable/ic_hint_bulb_white_48dp.xml b/app/src/main/res/drawable/ic_hint_bulb_white_48dp.xml index 6fc3005aee2..155f98bf313 100644 --- a/app/src/main/res/drawable/ic_hint_bulb_white_48dp.xml +++ b/app/src/main/res/drawable/ic_hint_bulb_white_48dp.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/rounded_rect_grey_border_gradient_white_background.xml b/app/src/main/res/drawable/rounded_rect_grey_border_gradient_white_background.xml index 68af8bfea69..baa2da01d4c 100644 --- a/app/src/main/res/drawable/rounded_rect_grey_border_gradient_white_background.xml +++ b/app/src/main/res/drawable/rounded_rect_grey_border_gradient_white_background.xml @@ -2,12 +2,12 @@ + android:color="@color/component_color_shared_drag_drop_single_item_stroke_color" /> diff --git a/app/src/main/res/drawable/seekbar_thumb.xml b/app/src/main/res/drawable/seekbar_thumb.xml index 0b3b1481547..a301153cb07 100644 --- a/app/src/main/res/drawable/seekbar_thumb.xml +++ b/app/src/main/res/drawable/seekbar_thumb.xml @@ -17,7 +17,7 @@ android:left="8dp" android:top="4dp"> - + diff --git a/app/src/main/res/drawable/submitted_answer_background.xml b/app/src/main/res/drawable/submitted_answer_background.xml index 67dc05683d9..486748ac486 100644 --- a/app/src/main/res/drawable/submitted_answer_background.xml +++ b/app/src/main/res/drawable/submitted_answer_background.xml @@ -4,9 +4,9 @@ android:shape="rectangle"> + android:color="@color/component_color_shared_submitted_answer_item_stroke_color"/> + android:color="@color/component_color_shared_submitted_answer_item_solid_color"/> diff --git a/app/src/main/res/drawable/thumbnail_gradient.xml b/app/src/main/res/drawable/thumbnail_gradient.xml index 3ff9c58fb90..d3ef4a0fb66 100644 --- a/app/src/main/res/drawable/thumbnail_gradient.xml +++ b/app/src/main/res/drawable/thumbnail_gradient.xml @@ -3,7 +3,7 @@ android:shape="rectangle"> diff --git a/app/src/main/res/layout-land/hints_summary.xml b/app/src/main/res/layout-land/hints_summary.xml deleted file mode 100644 index 6c98ec7257e..00000000000 --- a/app/src/main/res/layout-land/hints_summary.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - -