Skip to content

Commit

Permalink
Fix #4044: Introduce UI support for math expressions & new interactio…
Browse files Browse the repository at this point in the history
…ns (#2173)

## Explanation
Fixes #4044

This PR introduces the UI interaction views for numeric expressions, algebraic expressions, and algebraic equations, finishing the initial implementation of the broader math expressions project. This work unblocks adding support for 4 additional topics in the app: Multiplication (support was lost after Alpha MR2 due to a change that added numeric expression questions), Addition & Subtraction, Division, and Expressions & Equations.

This PR finishes a long chain of PRs that were needed to provide the domain functionality to support these new interactions, as a significant amount of mathematics functionality needed to be added including:
- Support for representing generic math expressions/equations, polynomials, and reals
- Support for parsing math expressions
- Support for converting between math expressions and:
  - LaTeX (for rendering)
  - Human-readable sentences (for accessibility)
  - Polynomials and real-value evaluation (for equivalence checking)
  - Comparable structures to verify that two expressions/equations match irrespective of associativity or commutativity order
- Robust error detection to support nearly 2 dozen special cased errors with potential for countless more if needed to ensure users receive excellent feedback when inputting expressions

All of the above also required thorough testing to ensure correctness. See the previous PRs corresponding to #4044 for the full context, and thorough PR descriptions explaining past changes.

From a UI perspective, this PR is introducing:
- The new interaction views and relevant view models, including the logic for constructing and displaying errors for invalid answers
- Support for playing a custom content description for math expression answers so that screenreader users can better understand the expression they've entered
- Support for rendering the LaTeX representation of entered expressions
- A new developer options menu to provide support to input arbitrary expressions and equations and convert them to different outputs to test the underlying math engine that's been built (though it can't yet test the classifiers, but it provides enough information to verify the more complex bits behind the classifiers)

From a functional perspective, this PR is introducing:
- Support for loading JSON explorations that include math expressions
- A new test exploration that's been added to test topic 0 to demonstrate all three interactions and each of those interaction's classifiers (9 total states have been added)

The above, and more, are explained in more detail below.

### Background on UI implementation for new interactions

The UI implementation leverages a single new interaction view & model for all three new interaction types since they are very similar to one another, including handling accessibility and LaTeX rendering cases. The customization arguments, however, don't exactly match so there are some inconsistencies there.

### Background on parsing approach
The new interactions rely on custom math infrastructure for parsing math expressions since no suitable libraries were found that were both license compatible and didn't leverage native code. While this significantly increases the scope of code that needs to be maintained in the project, it has the added benefit of leveraging extremely specific functionality to reduce inconsistencies with Oppia web and ensure a very high quality learner experience when it comes to answer classification and error detection.

Expressions are constructed using an LL(1) recursive-descent parser with early-exit error detection (see the [technical specification](https://docs.google.com/document/d/1JMpbjqRqdEpye67HvDoqBo_rtScY9oEaB7SwKBBspss/edit)). Parsing first goes through an LL(1) lazy tokenization process before being parsed lazily into proto-defined math expression/equation structures. Classifier needs are satisified through custom infrastructure that can evaluate numeric expressions to a single real value, compare math expressions/equations with floating point error correction, compare math expressions/equations irrespective of operation associativity and commutativity by transforming it to an intermediary representation, and can compare expressions/equations for equivalence by first converting it to a polynomial using a custom nearly fully feature complete polynomial system. Finally, UI-specific needs are satisified through generators for both LaTeX and human-readable accessibility strings.

The preceding PRs in this project contain the implementations for parsing, conversion, and comparsion and include PR descriptions that explain these systems & other peripheral changes in great detail.

### PR history
This PR contains **all** of the PRs corresponding to both the previous attempt at implementing math expressions, and the latest (since all commits end up being duplicated in PR chains). There's also some early work that preceded the original work in this PR that's included in its history (it goes back quite a bit).

For reference, here's the entire chain of PRs in order that precede this one for implementing math expressions support:
1. #4045
2. #4046
3. #4047
4. #4049
5. #4050
6. #4051
7. #4052
8. #4054
9. #4055
10. #4056
11. #4057
12. #4058
13. #4059
14. #4061
15. #4063
16. #4068
17. #4237
18. #4239

### Refactors, miscellaneous changes, and future work
All interaction view models were refactored to use a factory pattern rather than a function to improve readability, and to make that view model construction consistent with other similar situations (i.e. cases where new instances need to be created with Dagger dependencies).

Split screen supported interaction IDs have been refactored to be a Dagger set to avoid a direct dependency on InteractionViewModelModule to access the IDs (the new solution is more Dagger 2 idiomatic).

This PR doesn't finish all aspects of the project as there are a number of improvements that have been proposed toward the end of development. These have been cataloged in #4254 and will be implemented in a follow-up PR (but will only be started after this PR & its predecessors have been merged). None of those work items block the overall project, and are intentionally being delayed to a future work item to ensure that math expressions can land more quickly.

### Test specifics & exemptions
Changes were needed in ``EditTextInputAction`` to support inputting Unicode text (which is necessary for some UI tests that verify using math symbols such as for times and divide). Espresso itself doesn't support inputting these characters (presumably since it can't easily find them on the keyboard), so the action now exposes an option to directly set the text.

Exemption explanations:
- ``MathExpressionInteractionsViewTest``: this has been enabled to allow parameterized tests to ensure it can verify a broad set of questions and answers. This might actually be a nice pattern to follow for other interaction tests in the future.
- All test exemptions are either can't be obviously tested (listeners & annotations), are tested through other suites (view models & presenters), is especially difficult to test (``ParameterizedAutoAndroidTestRunner``), or is deemed not important enough to add tests for due to a trivial implementation and in the effort to finish this project sooner (``SplitScreenInteractionModule``).

Note that the defaulting cases for accessibility strings & LaTeX (i.e. the scenarios where generation to either fails) can't easily be tested since the scenarios in which generation fails arise from invalid answers that can't be submitted since they trigger submit-time errors. However, the failure cases for both of these utilities have been thoroughly checked in their respective test suites.

This PR also introduces a new addition to ``OppiaParameterizedTestRunner``: ``ParameterizedAutoAndroidTestRunner``. This runner acts like ``AndroidJUnit4`` wherein it will automatically select a Robolectric or Espresso runner depending on the current runtime environment. This ought to only be used by shared tests that are supposed to run on both platforms, otherwise Robolectric or Espresso specific runners should be used (or JUnit for non-Robolectric tests).

## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".)
- [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation.
- [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).

## For UI-specific PRs only
I've verified the interaction with the accessibility scanner locally and it didn't mention any new suggestions (there was one suggestion on the label for the input box when inputting something, and another for the congratulations banner contrast--both of which are existing issues that affect other interactions).

### Screenshots of new UIs
| Device type | Language | Orientation | Input box | Error | Inputted answer |
|-------------|----------|-------------|-----------|-----------------|-------|
| Handset     | English  | Portrait    | ![image](https://user-images.githubusercontent.com/12983742/158960886-b66b5f59-48e9-4005-a05d-60cdc9917cee.png)      | ![image](https://user-images.githubusercontent.com/12983742/158966282-108719be-44cb-42ab-8570-fe2f4dd49cfe.png) | ![image](https://user-images.githubusercontent.com/12983742/158966035-c1db8ee1-01a8-4e2f-bab7-c9622965c5b5.png)        |
| Handset     | English  | Landscape   | ![image](https://user-images.githubusercontent.com/12983742/158966436-ef289ea9-2b69-4bd8-b07a-8e0ac917f30a.png)      | ![image](https://user-images.githubusercontent.com/12983742/158966626-8a19d38c-2cbe-4544-aab1-57da419ca405.png)            | ![image](https://user-images.githubusercontent.com/12983742/158966701-768af358-912c-44c4-8c20-e82db23b39a7.png)  |
| Handset     | Arabic   | Portrait    |  ![image](https://user-images.githubusercontent.com/12983742/158967405-29461acf-d160-46e2-bbaa-233a97b99f91.png)     | ![image](https://user-images.githubusercontent.com/12983742/158967611-d3494512-ff49-4c3e-988d-3c2a0eb48943.png)            | ![image](https://user-images.githubusercontent.com/12983742/158969001-a5d516bb-7db2-49f3-b614-baf298c0ce95.png)  |
| Handset     | Arabic   | Landscape   | ![image](https://user-images.githubusercontent.com/12983742/158969671-f2f92523-6045-4908-9b3a-c0efec525e55.png)      | ![image](https://user-images.githubusercontent.com/12983742/158970091-ba858e7e-deab-4189-b06a-91208049ab79.png)            | ![image](https://user-images.githubusercontent.com/12983742/158971054-c64adfbd-06a1-4cc7-a3df-983793e8bd30.png)  |
| Tablet      | English  | Portrait    | ![image](https://user-images.githubusercontent.com/12983742/158975533-046a1d31-145a-4671-b212-6770f0b09b4a.png)      | ![image](https://user-images.githubusercontent.com/12983742/158975620-e404da29-630b-4a19-be8b-13ff5b853601.png)            | ![image](https://user-images.githubusercontent.com/12983742/158975673-a20f6ddb-1669-4035-a819-c23eccee666b.png)  |
| Tablet      | English  | Landscape   | ![image](https://user-images.githubusercontent.com/12983742/158975794-c36f7bee-4f47-4683-b3a2-7e012ea7039c.png)      | ![image](https://user-images.githubusercontent.com/12983742/158975835-a43957f8-c831-4fe3-a69e-3df7b6be3013.png)            | ![image](https://user-images.githubusercontent.com/12983742/158975869-59a7b989-83c1-4162-9df6-6d07f79c5005.png)  |
| Tablet      | Arabic   | Portrait    |  ![image](https://user-images.githubusercontent.com/12983742/158976398-55014bf8-0146-4507-95a3-733fab00f36f.png)     | ![image](https://user-images.githubusercontent.com/12983742/158976477-0736e4c3-81c0-42fd-8160-6ba0cb528e44.png)            | ![image](https://user-images.githubusercontent.com/12983742/158976579-b0a834fc-793e-4205-a2ad-c4029665430d.png)  |
| Tablet      | Arabic   | Landscape   | ![image](https://user-images.githubusercontent.com/12983742/158976744-7c2fb78b-7338-4688-94a0-afa65126b972.png)      | ![image](https://user-images.githubusercontent.com/12983742/158976832-43aee849-38fe-4fb7-8ebf-af13e0cb5cb0.png)            | ![image](https://user-images.githubusercontent.com/12983742/158976882-4e309290-63ab-4ea3-a3a6-39f64615afe4.png)  |

Note that the interaction's errors haven't yet been translated. Also, some work may need to be done in the future to ensure directionality makes sense both for input and rendering (asking folks yielded that input is typically right-to-left but math expressions are still represented left-to-right, so it's not clear if math input should also be left-to-right). In the case above, the text view actually switches the '+2' to '2+1' once it has another number since the context is more complete. It's possible RTL users are already used to quirks like this. It's also likely the custom math keyboard would be a good means of solving this issue long-term.

Screenshot of the new developer options menu for testing math expressions/equations:
![math_expressions_debug_menu](https://user-images.githubusercontent.com/12983742/158933786-9654c85e-b7b9-49fa-8e5f-8d37e598139f.png)

Note that I didn't bother to screenshot other configurations for the developer options menu since it's only visible to developers, so it's less important to make sure it meets the accessibility, language, device, and orientation requirements that the other screens of the UI are held to.

### Videos
| Flow being demonstrated                                    | Video recording |
|------------------------------------------------------------|-----------------|
| Submitting numeric expressions answers                     | https://user-images.githubusercontent.com/12983742/158933258-74992080-a9ec-46da-b752-c00b406ab3ec.mp4            |
| Submitting algebraic expressions answers                   | https://user-images.githubusercontent.com/12983742/158933296-0ea70704-9e3b-4e0d-9150-680a47f9ece2.mp4            |
| Submitting answers that cause errors                           | https://user-images.githubusercontent.com/12983742/158933391-602356c3-ccad-4199-8c0b-47d42abd7896.mp4            |
| Rendering fractions vs. divisions                          | https://user-images.githubusercontent.com/12983742/158933425-9abc640b-289e-484f-8f30-dab38789c99e.mp4            |
| Submitting numeric expressions with a screenreader enabled | https://user-images.githubusercontent.com/12983742/158933446-21afbcd6-0ea8-4558-8c68-9fa0157a7e3d.mp4            |
| Submitted math equations with a screenreader enabled       | https://user-images.githubusercontent.com/12983742/158933460-ddf39ba3-1fd8-4bfc-82e9-cc1845846646.mp4            |

### Espresso test results
| ![final_math_pr_developeroptionsactivitytest_passing](https://user-images.githubusercontent.com/12983742/158932650-4c55fd70-0c58-4972-b1c9-dda727190f9a.png) | ![final_math_pr_developeroptionsfragmenttest_passing](https://user-images.githubusercontent.com/12983742/158932667-9ab52468-6c7d-4088-8e80-3e71afe3cc29.png) |
|------|------|
| ![final_math_pr_htmlparsertest_passing](https://user-images.githubusercontent.com/12983742/158932678-aadd0ba5-f7c4-49e0-b7a8-0cf0734a269d.png) | ![final_math_pr_mathexpressioninteractionsviewtest_passing](https://user-images.githubusercontent.com/12983742/158932687-e1b5df4a-9a5e-4f5e-a0bb-0b1e60ce562e.png) |
| ![final_math_pr_mathexpressionparseractivitytest_passing](https://user-images.githubusercontent.com/12983742/158932699-e71b0b5a-f4c6-475c-9cc1-db626de9ed51.png) | ![final_math_pr_mathexpressionparserfragmenttest_passing](https://user-images.githubusercontent.com/12983742/158932712-a68cee27-4a2f-49ab-895b-f2d021cbc3d8.png) |
| ![final_math_pr_questionplayeractivitytest_passing](https://user-images.githubusercontent.com/12983742/158932734-e9aefddf-13e9-4cf6-9a22-37588ca527e7.png) | ![final_math_pr_statefragmenttest_passing](https://user-images.githubusercontent.com/12983742/158932752-3bc3d821-b703-46a9-a5ef-29fd568d896f.png) |

Note that in the case of ``MathExpressionInteractionsViewTest`` this is the first PR that demonstrates using parameterized tests with Espresso.

Commit history:

* Treat en-dash as a subtraction symbol.

* Add explicit platform selection for paramerized.

This adds explicit platform selection support rather than it being
automatic based on deps. While less flexible for shared tests, this
offers better control for tests that don't want to to use Robolectric
for local tests.

This also adds a JUnit-only test runner, and updates MathTokenizerTest
to use it (which led to an almost 40x decrease in runtime).

* Exemption fixes.

Also, fix name for the AndroidJUnit4 runner.

* Remove failing test.

* Fix unary expression precedence.

Also, use ParameterizedJunitTestRunner for MathExpressionParserTest.

* Fixes & add more test cases.

* Post-merge fixes & test changes.

Also, update RealExtensionsTest to use the faster JUnit runner.

* Use utility directly in LaTeX tests.

* Post-merge fixes.

Also, update ExpressionToComparableOperationConverterTest to use the
fast JUnit-only runner.

* Post-merge fixes.

Also, update PolynomialExtensionsTest to use fast JUnit-only runner.

* Post-merge fixes.

Also, update float interval per new tests.

* Lint & other check fixes.

* Replace deprecated term.

* Post-merge fixes.

* Add full test suites for alg exp classifiers.

* Lint & static check fixes.

* Fix test on Gradle.

* Fix test for Gradle.

* Add tests for math equations.

And, post-merge fixes.

* Static check & lint fixes.

* Post-merge fixes.

Verified CI checks & all unit tests are passing.

* Split up tests.

Also, adds dedicated BUILD.bazel file for new test.

* Add missing test in Bazel, and fix it.

* Correct order for genrule.

* Add full test suite.

* Clean up + KDocs + exemption.

* Lint fixes.

* Post-merge fix.

* Cache KotliTeX renders.

Directly rendering LaTeX through KotliTeX is way too slow, so this
introduces a custom flow through Glide that computes a PNG for the LaTeX
on a background thread and then caches it as part of Glide's cache to
speed up re-renders of the LaTeX. We may need to manage/prune the cache
over time, but for now we'll just rely on Glide's internal behaviors.

This also documents some new tests that should be added, but it's not
comprehensive.

* Add tests, docs, and exemptions.

* Update to fixed version of KotliTeX.

The newer version correctly computes the bounds for rendered LaTeX.

* Lint fixes.

* Add new dependency licenses.

This isn't done yet (some of the licenses still need to be fixed).

* Fix license links.

Note that the kxml one was tricky since its Maven entry says it's
licensed under BSD and CC0 1.0, and its SourceForge link says the same
plus LGPL 2.0. However, the source code and GitHub version of the
project license it under MIT, and this seems to predate the others so
it seems like the most correct license to use in this case and the one
that we're using to represent the dependency.

* Fix Gradle build.

This uses a version of KotliTeX that builds correctly on Jitpack for Gradle,
and fixes the StaticLayout creation to use an alignment constant that
builds on Gradle (I'm not sure why there's a difference here between
Gradle & Bazel, but the previous constant isn't part of the enum per
official Android docs).

* Create the math drawable synchronously.

This requires exposing new injectors broadly in the app since the math
model loader doesn't have access to the dependency injection graph
directly.

* Remove new deps from Maven list.

They were incorrectly pulled in by KotliTeX.

* Add argument partitioning.

This fixes cases where argument calls may be very large and fail to
execute due to exceeding system limitations.

* Post-merge fixes.

Note that only the following tests are failing (some of which may be due
to the previous branch):
- MarkChaptersCompletedFragmentTest
- MarkStoriesCompletedFragmentTest
- StoryProgressTestHelperTest
- ModifyLessonProgressControllerTest
- TopicListControllerTest
- ComputeAffectedTestsTest
- MarkTopicsCompletedFragmentTest
- HomeActivityTest

* Make allowance for empty cases to fix tests.

These tests correspond to real scenarios.

* Lint fixes.

* Fix broken tests and add reasonable exemptions.

* First pass at adding tests.

Some debugging left, and more tests to add.

This also adds auto-switching support for parameterized tests.

* Fix DeveloperOptionsActivityTest.

Also, fix all other test builds (for Gradle). This will probably require
some reformatting.

* Add & fix remaining tests.

This also fixes a bug where the correct answer a11y label was being
incorrectly assumed to be the correct answer even for incorrect cases.

* Add docs & clean up remaining non-lint parts.

* Lint & small breakage fixes.

* Fix broken tests.

* Address reviewer comment.

Clarifies the documentation in the test runner around parameter
injection.

* Fix broken build.

* Fix broken build post-merge.

* Fix broken post-merge classifier.

* Address reviewer comment.

* Post-merge build fixes.

* Post-merge build fixes for new classifiers.

* Post-merge build fixes.

* Correct reference document link.

* Ensure LaTeX isn't stretched or cut-off.

The comments in-code have much more specifics on the approach.

* Add and fix missing test (was broken on Gradle).

* Fix Gradle tests.

This commit has verified the following tests now pass on Espresso:
- MathExpressionInteractionsViewTest
- HtmlParserTest
- MathExpressionParserActivityTest
- MathExpressionParserFragmentTest
- DeveloperOptionsActivityTest
- DeveloperOptionsFragmentTest

StateFragmentTest is having issues (it ends up hanging), so further
investigation is needed.

* Update to newer version of Kotlin coroutines.

This version includes StateFlow which will be a really useful mechanism
for helping to avoid using critical sections.

* First attempt to fix deadlock.

This uses StateFlows and an actor to avoid the deadlock.

This is missing necessary hint changes that are coming in a later
commit. Tests currently fail, and questions haven't yet been migrated
(and won't until the fixes are verified).

* Attempt to make hint handler multi-threadable.

This is a midway solution that's being committed for posterity, but will
be reverted in favor of a solution that forces hint handler to be unsafe
across multiple threads (since it's much simpler, and works given that
all users of it now synchronize state management using an actor).

* Finish fixing state player.

This includes a fix for accessibility string handling (since the new
flow triggers one of these to fail). It also adds a Bazel file for
StateFragmentTest (I spent some time trying to get Espresso tests
working with Bazel but ran into a compatibility issue).

StateFragmentTest has been verified to pass on Robolectric and Espresso
with this change.

This sets up the project for fixing questions in the next commit.

* First pass on migrating question controller.

This also includes a migration for exploration & question domain tests
to use the test monitor utility.

The question tests currently fail since there's a bug in AsyncResult
where it won't support null values during transformations.

* Refactor AsyncResult into a sealed class.

This also introduces an AsyncResultSubject, and more or less fully fixes
issue #3813 in all tests.

* Refactor AsyncResult into a sealed class.

This also introduces an AsyncResultSubject, and more or less fully fixes
issue #3813 in all tests.

This is a cherry-pick from the fix-progress-controller-deadlock branch
since it ended up being quite large (it made more sense to split it into
a pre-requisite PR).

Conflicts:
	app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt
	domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt
	domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt
	domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/BUILD.bazel
	testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt
	utility/src/main/java/org/oppia/android/util/data/DataProviders.kt

* Post-merge fixes and updates for consistency.

* Post-merge fixes.

* TODO has been addressed.

* Fix documentation & add tests.

* Lint fixes.

* Lint & post-merge fixes.

Questions-related tests still fail and need to be fixed now that
AsyncResult has been refactored to support null values.

* Post-merge test fixes.

The core affected UI/domain tests have now been verified as working on
Robolectric. The full test suite is next.

* Add documentation & tests.

Also, fix a bug in the automatic StateFlow DataProviders wherein they
wouldn't notify correctly on changes. This led to some simplifications
in the exploration & question progress controllers.

* Lint fixes.

* Fix gradle tests.

I've verified in this commit that all Gradle tests build & run locally
(at least on Robolectric).

* Fix Proguard build.

This required bringing kotlinx-coroutines-android up-to-date with
kotlinx-coroutines-core (since that was updated on this branch).

New but reasonable Proguard warning exemptions also needed to be added.

* Post-merge fixes.

* Gradle fixes.

This also fixes an issue wherein answers couldn't be resubmitted for
math expression interactions after an error occurred.

* Fix parameterized runners for Espresso.

This fixes a typo when loading the AndroidJUnit4 runner during
parameterization runer selection, and ensures that test names don't
contain illegal characters that would break the runner.

* Post-merge fix.

* More post-merge fixes.

* Fix TODO comment.

* Post-merge lint fixes.

* Post-merge fix.

* Fix exploration routing issue.

The underlying problem was that the PR inadvertently changed the
behavior of comparing two results wherein results of different times
would be considered different and be re-delivered (which happened to
break exploration routing, and likely a variety of other places).

This introduces a new method for checking equivelance rather than
confusingly assuming that users of AsyncResult don't care about the
result's time during comparison checking.

New tests have been added to verify the functionality works as expected
(and fails when expected), and I manually verified that the exploration
routing issue was fixed. More details on the specific user-facing issue
will be added to the PR as a comment.

* Post-merge fixes.

* Post-merge fixes.

The local repo has been verified to pass nearly all static analysis and
lint tests, as well as all Robolectric tests. The developer and alpha
versions of the app build.

* Update KotliTeX version.

This version doesn't have debug drawing enabled.

* Fix lifecycle breakage.

I noticed downstream that quickly navigating back and forth to enter &
exit an exploration can cause the progress controller to get into a bad
state that either leads to a broken experience (stacked explorations or a
blank state), or a crash.

The issue was coming from DataProviders being shared across sessions
even though the underlying state was different. This change ensures that
providers stay completely independent, similar to the core session
state.

* Address reviewer comment.

* Update play session controllers.

This ensures that the command queue itself is fully isolated to avoid
delayed events potentially leaking across sessions (now that the session
ID barrier has been removed).
  • Loading branch information
BenHenning authored Mar 27, 2022
1 parent df0f064 commit 84e277a
Show file tree
Hide file tree
Showing 220 changed files with 10,737 additions and 949 deletions.
8 changes: 8 additions & 0 deletions app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ LISTENERS = [
"src/main/java/org/oppia/android/app/devoptions/RouteToMarkChaptersCompletedListener.kt",
"src/main/java/org/oppia/android/app/devoptions/RouteToMarkStoriesCompletedListener.kt",
"src/main/java/org/oppia/android/app/devoptions/RouteToMarkTopicsCompletedListener.kt",
"src/main/java/org/oppia/android/app/devoptions/RouteToMathExpressionParserTestListener.kt",
"src/main/java/org/oppia/android/app/devoptions/RouteToViewEventLogsListener.kt",
"src/main/java/org/oppia/android/app/drawer/RouteToProfileProgressListener.kt",
"src/main/java/org/oppia/android/app/help/LoadFaqListFragmentListener.kt",
Expand Down Expand Up @@ -176,6 +177,7 @@ DATABINDING_LAYOUTS = ["src/main/res/layout*/**"]
VIEW_MODELS_WITH_RESOURCE_IMPORTS = [
"src/main/java/org/oppia/android/app/administratorcontrols/appversion/AppVersionViewModel.kt",
"src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeViewModel.kt",
"src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt",
"src/main/java/org/oppia/android/app/drawer/NavigationDrawerHeaderViewModel.kt",
"src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt",
"src/main/java/org/oppia/android/app/help/HelpListViewModel.kt",
Expand All @@ -202,6 +204,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [
"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",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt",
"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",
Expand Down Expand Up @@ -241,6 +244,7 @@ VIEW_MODELS = [
"src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsItemViewModel.kt",
"src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsModifyLessonProgressViewModel.kt",
"src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsOverrideAppBehaviorsViewModel.kt",
"src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsTestParsersViewModel.kt",
"src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsViewLogsViewModel.kt",
"src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt",
"src/main/java/org/oppia/android/app/devoptions/forcenetworktype/NetworkTypeItemViewModel.kt",
Expand Down Expand Up @@ -388,6 +392,7 @@ VIEWS_WITH_RESOURCE_IMPORTS = [
# keep sorted
VIEWS = [
"src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt",
"src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt",
"src/main/java/org/oppia/android/app/customview/interaction/NumericInputInteractionView.kt",
"src/main/java/org/oppia/android/app/customview/interaction/TextInputInteractionView.kt",
"src/main/java/org/oppia/android/app/customview/interaction/RatioInputInteractionView.kt",
Expand Down Expand Up @@ -544,6 +549,7 @@ android_library(
resource_files = glob(DATABINDING_LAYOUTS),
visibility = [
"//app/src/main/java/org/oppia/android/app/shim:__pkg__",
"//app/src/main/java/org/oppia/android/app/testing/activity:__pkg__",
],
deps = [
":annotations",
Expand Down Expand Up @@ -654,6 +660,7 @@ kt_android_library(
"//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/onboarding:state_controller",
Expand All @@ -669,6 +676,7 @@ kt_android_library(
# 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/parser/html:html_parser",
],
)

Expand Down
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ dependencies {
'androidx.test.ext:junit:1.1.1',
'com.github.bumptech.glide:mocks:4.11.0',
'com.google.truth:truth:1.1.3',
'com.google.truth.extensions:truth-liteproto-extension:1.1.3',
'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2',
'org.mockito:mockito-android:2.7.22',
'org.robolectric:annotations:4.5',
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@
<activity
android:name=".app.devoptions.forcenetworktype.testing.ForceNetworkTypeTestActivity"
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity
android:name=".app.devoptions.mathexpressionparser.MathExpressionParserActivity"
android:label="@string/math_expression_parser_activity_title"
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity
android:name=".app.resumelesson.ResumeLessonActivity"
android:label="@string/resume_lesson_activity_title"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.oppia.android.app.devoptions.markstoriescompleted.MarkStoriesComplete
import org.oppia.android.app.devoptions.markstoriescompleted.testing.MarkStoriesCompletedTestActivity
import org.oppia.android.app.devoptions.marktopicscompleted.MarkTopicsCompletedActivity
import org.oppia.android.app.devoptions.marktopicscompleted.testing.MarkTopicsCompletedTestActivity
import org.oppia.android.app.devoptions.mathexpressionparser.MathExpressionParserActivity
import org.oppia.android.app.devoptions.testing.DeveloperOptionsTestActivity
import org.oppia.android.app.devoptions.vieweventlogs.ViewEventLogsActivity
import org.oppia.android.app.devoptions.vieweventlogs.testing.ViewEventLogsTestActivity
Expand Down Expand Up @@ -139,6 +140,7 @@ interface ActivityComponentImpl :
fun inject(markTopicsCompletedActivity: MarkTopicsCompletedActivity)
fun inject(marginBindableAdaptersTestActivity: MarginBindingAdaptersTestActivity)
fun inject(markTopicsCompletedTestActivity: MarkTopicsCompletedTestActivity)
fun inject(mathExpressionParserActivity: MathExpressionParserActivity)
fun inject(myDownloadsActivity: MyDownloadsActivity)
fun inject(navigationDrawerTestActivity: NavigationDrawerTestActivity)
fun inject(onboardingActivity: OnboardingActivity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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
Expand Down Expand Up @@ -97,7 +98,7 @@ import javax.inject.Singleton
NetworkConnectionUtilDebugModule::class, NetworkConfigProdModule::class, AssetModule::class,
LocaleProdModule::class, ActivityRecreatorProdModule::class,
NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class,
MathEquationInputModule::class,
MathEquationInputModule::class, SplitScreenInteractionModule::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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.oppia.android.app.customview.interaction

import android.content.Context
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener
import org.oppia.android.app.utility.KeyboardHelper.Companion.hideSoftKeyboard
import org.oppia.android.app.utility.KeyboardHelper.Companion.showSoftKeyboard

// TODO(#249): These are the attributes which should be defined in XML, that are required for this
// interaction view to work correctly:
// placeholder="Write here."
// inputType="text"
// background="@drawable/edit_text_background"
// maxLength="200".

/**
* The custom [EditText] class for math expression interactions interaction view.
*
* Note that the hint should be set via [setPlaceholder] to ensure that it's properly initialized if
* using databinding, otherwise setting the hint through android:hint should work fine.
*/
class MathExpressionInteractionsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = android.R.attr.editTextStyle
) : EditText(context, attrs, defStyle), View.OnFocusChangeListener {
private var hintText: CharSequence
private val stateKeyboardButtonListener: StateKeyboardButtonListener

init {
onFocusChangeListener = this
hintText = (hint ?: "")
stateKeyboardButtonListener = context as StateKeyboardButtonListener
}

override fun onFocusChange(v: View, hasFocus: Boolean) = if (hasFocus) {
hint = ""
typeface = Typeface.DEFAULT
showSoftKeyboard(v, context)
} else {
hint = hintText
if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC)
hideSoftKeyboard(v, context)
}

override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
clearFocus()
}
return super.onKeyPreIme(keyCode, event)
}

override fun onEditorAction(actionCode: Int) {
if (actionCode == EditorInfo.IME_ACTION_DONE) {
stateKeyboardButtonListener.onEditorAction(EditorInfo.IME_ACTION_DONE)
}
super.onEditorAction(actionCode)
}

/**
* Sets the current placeholder text used by the view to [placeholderText].
*
* See the class's KDoc for caveats on how this relates to the text view's hint.
*/
fun setPlaceholder(placeholderText: CharSequence) {
hintText = placeholderText
if (!hasFocus()) {
hint = placeholderText
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.oppia.android.app.devoptions.forcenetworktype.ForceNetworkTypeActivit
import org.oppia.android.app.devoptions.markchapterscompleted.MarkChaptersCompletedActivity
import org.oppia.android.app.devoptions.markstoriescompleted.MarkStoriesCompletedActivity
import org.oppia.android.app.devoptions.marktopicscompleted.MarkTopicsCompletedActivity
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.translation.AppLanguageResourceHandler
Expand All @@ -23,7 +24,8 @@ class DeveloperOptionsActivity :
RouteToMarkStoriesCompletedListener,
RouteToMarkTopicsCompletedListener,
RouteToViewEventLogsListener,
RouteToForceNetworkTypeListener {
RouteToForceNetworkTypeListener,
RouteToMathExpressionParserTestListener {

@Inject
lateinit var developerOptionsActivityPresenter: DeveloperOptionsActivityPresenter
Expand Down Expand Up @@ -70,6 +72,10 @@ class DeveloperOptionsActivity :
startActivity(ForceNetworkTypeActivity.createForceNetworkTypeActivityIntent(this))
}

override fun routeToMathExpressionParserTest() {
startActivity(MathExpressionParserActivity.createIntent(this))
}

companion object {
/** Function to create intent for DeveloperOptionsActivity */
fun createDeveloperOptionsActivityIntent(context: Context, internalProfileId: Int): Intent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import androidx.recyclerview.widget.LinearLayoutManager
import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsItemViewModel
import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsModifyLessonProgressViewModel
import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsOverrideAppBehaviorsViewModel
import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsTestParsersViewModel
import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsViewLogsViewModel
import org.oppia.android.app.fragment.FragmentScope
import org.oppia.android.app.recyclerview.BindableAdapter
import org.oppia.android.databinding.DeveloperOptionsFragmentBinding
import org.oppia.android.databinding.DeveloperOptionsModifyLessonProgressViewBinding
import org.oppia.android.databinding.DeveloperOptionsOverrideAppBehaviorsViewBinding
import org.oppia.android.databinding.DeveloperOptionsTestParsersViewBinding
import org.oppia.android.databinding.DeveloperOptionsViewLogsViewBinding
import javax.inject.Inject

Expand Down Expand Up @@ -72,6 +74,10 @@ class DeveloperOptionsFragmentPresenter @Inject constructor(
viewModel.itemIndex.set(2)
ViewType.VIEW_TYPE_OVERRIDE_APP_BEHAVIORS
}
is DeveloperOptionsTestParsersViewModel -> {
viewModel.itemIndex.set(3)
ViewType.VIEW_TYPE_TEST_PARSERS
}
else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel")
}
}
Expand All @@ -93,12 +99,19 @@ class DeveloperOptionsFragmentPresenter @Inject constructor(
setViewModel = DeveloperOptionsOverrideAppBehaviorsViewBinding::setViewModel,
transformViewModel = { it as DeveloperOptionsOverrideAppBehaviorsViewModel }
)
.registerViewDataBinder(
viewType = ViewType.VIEW_TYPE_TEST_PARSERS,
inflateDataBinding = DeveloperOptionsTestParsersViewBinding::inflate,
setViewModel = DeveloperOptionsTestParsersViewBinding::setViewModel,
transformViewModel = { it as DeveloperOptionsTestParsersViewModel }
)
.build()
}

private enum class ViewType {
VIEW_TYPE_MODIFY_LESSON_PROGRESS,
VIEW_TYPE_VIEW_LOGS,
VIEW_TYPE_OVERRIDE_APP_BEHAVIORS
VIEW_TYPE_OVERRIDE_APP_BEHAVIORS,
VIEW_TYPE_TEST_PARSERS
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsItemViewModel
import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsModifyLessonProgressViewModel
import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsOverrideAppBehaviorsViewModel
import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsTestParsersViewModel
import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsViewLogsViewModel
import org.oppia.android.app.fragment.FragmentScope
import org.oppia.android.domain.devoptions.ShowAllHintsAndSolutionController
Expand All @@ -28,6 +29,8 @@ class DeveloperOptionsViewModel @Inject constructor(
activity as RouteToMarkTopicsCompletedListener
private val routeToViewEventLogsListener = activity as RouteToViewEventLogsListener
private val routeToForceNetworkTypeListener = activity as RouteToForceNetworkTypeListener
private val routeToMathExpressionParserTestListener =
activity as RouteToMathExpressionParserTestListener

/**
* List of [DeveloperOptionsItemViewModel] used to populate recyclerview of
Expand All @@ -49,7 +52,8 @@ class DeveloperOptionsViewModel @Inject constructor(
forceCrashButtonClickListener,
routeToForceNetworkTypeListener,
showAllHintsAndSolutionController
)
),
DeveloperOptionsTestParsersViewModel(routeToMathExpressionParserTestListener)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.oppia.android.app.devoptions

/** Listener for when the user wants to test math expressions/equations. */
interface RouteToMathExpressionParserTestListener {
/** Called when the user indicates that they want to test math expressions/equations. */
fun routeToMathExpressionParserTest()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.oppia.android.app.devoptions.devoptionsitemviewmodel

import org.oppia.android.app.devoptions.RouteToMathExpressionParserTestListener

/**
* [DeveloperOptionsItemViewModel] to provide features to test and debug math expressions and
* equations.
*/
class DeveloperOptionsTestParsersViewModel(
private val routeToMathExpressionParserTestListener: RouteToMathExpressionParserTestListener
) : DeveloperOptionsItemViewModel() {
/** Routes the user to an activity for testing math expressions & equations. */
fun onMathExpressionsClicked() {
routeToMathExpressionParserTestListener.routeToMathExpressionParserTest()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.oppia.android.app.devoptions.mathexpressionparser

import android.content.Context
import android.content.Intent
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.translation.AppLanguageResourceHandler
import javax.inject.Inject

/** Activity to allow the user to test math expressions/equations. */
class MathExpressionParserActivity : InjectableAppCompatActivity() {
@Inject
lateinit var mathExpressionParserActivityPresenter: MathExpressionParserActivityPresenter

@Inject
lateinit var resourceHandler: AppLanguageResourceHandler

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(activityComponent as ActivityComponentImpl).inject(this)
mathExpressionParserActivityPresenter.handleOnCreate()
title = resourceHandler.getStringInLocale(R.string.math_expression_parser_activity_title)
}

companion object {
/** Returns [Intent] for [MathExpressionParserActivity]. */
fun createIntent(context: Context): Intent {
return Intent(context, MathExpressionParserActivity::class.java)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.oppia.android.app.devoptions.mathexpressionparser

import androidx.appcompat.app.AppCompatActivity
import org.oppia.android.R
import org.oppia.android.app.activity.ActivityScope
import javax.inject.Inject

/** The presenter for [MathExpressionParserActivity]. */
@ActivityScope
class MathExpressionParserActivityPresenter @Inject constructor(
private val activity: AppCompatActivity
) {

/** Called when [MathExpressionParserActivity] is created. Handles UI for the activity. */
fun handleOnCreate() {
activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back_white_24dp)
activity.setContentView(R.layout.math_expression_parser_activity)

if (getMathExpressionParserFragment() == null) {
val forceNetworkTypeFragment = MathExpressionParserFragment.createNewInstance()
activity.supportFragmentManager.beginTransaction().add(
R.id.math_expression_parser_container,
forceNetworkTypeFragment
).commitNow()
}
}

private fun getMathExpressionParserFragment(): MathExpressionParserFragment? {
return activity.supportFragmentManager
.findFragmentById(R.id.force_network_type_container) as? MathExpressionParserFragment
}
}
Loading

0 comments on commit 84e277a

Please sign in to comment.