Skip to content

Commit

Permalink
Fix #3622, #4238, #3861, part of #4044: Fix state player deadlock by …
Browse files Browse the repository at this point in the history
…migrating progress controllers over to a command-queue structure (#4239)

## Explanation

Fix #3622
Fix #4238
Fix #3861
Fix part of #4044 (see below for how this relates to the broader math expressions project)

This PR fixes a deadlock that's possible in both ``QuestionAssessmentProgressController`` and ``ExplorationProgressController`` wherein the internal locks used to synchronize session state can conflate with the locks of the controllers' ``HintHandler``s (which calls back into the controller within the lock). While this seems really difficult to hit when playing a training session or exploration, ``StateFragmentTest`` seems to very consistently hit the deadlock after only 10-15 tests (so it essentially can't be run without the fixes introduced here).

While I could've tried rearranging the locks to be more cooperative, it's actually difficult to guarantee that deadlocks won't happen without rebuilding the internal locking mechanism for controller data. I chose the latter by introducing a command queue-esque pattern using a Kotlin actor with ``StateFlow``s for pushing state back to callers (see https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html; actors are a powerful way to leverage the lightweight nature of coroutines to manage cross-thread state machines). To simplify the implementation, this PR also introduces conversion methods from ``StateFlow``s to ``DataProvider``s (I could see us leveraging these flows more in the future, maybe as a long-term replacement to ``DataProvider``s with some refinement).

One side effect of using a command queue to manage what is effectively a session state is that state can now fairly easily leak between sessions. Efforts have been taken to minimize the chance of this happening (mainly ignoring events that arrive from a previously ended session), but a better long-term solution would be to create new session controllers for each session (which should be something possible with custom scopes once we integrate Dagger Hilt in #1720).

Due to a general push toward using ``DataProvider``s much more heavily in these controllers rather than ``LiveData``, ``AsyncResult`` needed to be refactored in order for some of the internal transformed chains in the controllers to work correctly (which is where #4237 comes in).

This also permanently fixes #3861 and other potential errors that could happen by ensuring that the provider state can continue even if internal errors are encountered rather than entering a permanently broken state (#4230 mitigates the immediate repro steps from the bug by fixing the underlying issue that caused the exception, whereas this PR focuses on preventing those scenarios from causing the bug in question).

### Some explanations for the threading approach
I was originally going to make hint handler also a command queue, but it became a bit complicated. To keep things simpler, I removed the threading restricting on it so that it can only be accessed on a single thread (similarly to the internal progress objects used by both controllers) which helps avoid needing a lock without worrying about data races. Note that this command queue style implementation of ``HintHandler`` is available to view in the past commits in this PR if it's interesting for future development (since it's a simpler command queue than the ones used by the controllers due to having an inherently simpler internal state machine).

This PR demonstrates a general tendency to move the app toward a 'notify-and-pull' model (e.g. ``DataProvider``s) vs. a push-subscribe pattern. The former is easier to synchronize, parallelize, ensure lifecycle safety, and is often more performant since operations only need to execute when their results are needed.

Note that both controllers' internal computation behaviors are completely different with this new approach. Previously, the internal progress state machine would only be fully processed when the current ephemeral state/question was retrieved from the UI layer (i.e. lazily), but now it's computed shortly after the change (i.e. eagerly) since that works a bit better with the ``Flow``s being used for internal cross-thread communication. This technically means the controllers are slightly less efficient since execution may happen in cases when the state/question is no longer being viewed by the user, but it should realistically make a small difference since we have to thread hop for the command queue, anyway, and computing the ephemeral state/question should be cheap. This has the benefit of a potentially snappier load experience in the frontend, though, since the ephemeral state/question will be available sooner.

All operations in the controllers now execute their operations regardless of whether they're observed (which should better match callers' expectations).

Note that the internal state machine complexity can probably be drastically simplified now that the command queue can also act as a state machine, but I didn't want to risk regressions by removing it (I was trying to keep the logical flow as similar as possible to the existing behavior).

### Updates to dependencies

The coroutines core dependency needed to be updated since the older version didn't include ``StateFlow`` which was necessary for providing a sync-back mechanism for the controller command queues.

### Stability checks

I verified that both ``QuestionPlayerActivity`` and ``StateFragmentTest`` pass on Espresso (where the latter deadlocked prior to this PR); see the screenshots below. Further, I tested ``ExplorationProgressControllerTest`` and ``QuestionAssessmentProgressControllerTest`` 100x times to verify both are not flaky (since some flakes were discovered during development). Both test suites passed all 100 runs.

### Bazel app module tests
This PR introduces support for dedicated app module tests in Bazel (rather than bundling them as part of the broader list of app module tests). This required a few changes in .bzl and .bazel files, but the change is otherwise straightforward and extremely beneficial for team members that rely on the Android Studio with Bazel plugin for regular development.

### Miscellaneous
I tried introducing a Bazel emulator test while developing this (as part of #59) but it ran into a desugar issue with Mockito and Robolectric (due to them both being pulled into the test binary that's being built to send to the device) which AGP apparently works around. See bazelbuild/bazel#13684 for a Bazel issue tracking the same issue.

This fixes #3622 since it eliminates the lock entirely in favor of using actors for safe cross-thread communication. The ``MediatorLiveData`` specific part of this issue's TODO comment isn't relevant anymore, either, since we replaced ``MediatorLiveData`` with a custom notifying ``LiveData`` within ``DataProviders`` a while back.

This fixes #4238 fully by ensuring any exceptions caused within the state flow of an exploration or questions session are captured and logged, but don't actually break the internal state machine of the session. This issue was discovered during the development of #2173 due to an exception being thrown in the classifier. While that issue was fixed, a test couldn't actually be added for the #4238 fix since it's difficult to intentionally trigger exceptions during answer classification.

### A note on checkpoints in ``ExplorationProgressController``
The checkpointing behavior in ``ExplorationProgressController`` became more complex as a result of these changes. In particular, checkpointing behaves as follows:
- Something changes in the play session
- A checkpoint recomputation is requested (which results in a new checkpoint being computed and then saved to disk)
- The checkpoint is 'processed' by updating the play state of the exploration (i.e. in ``StoryProgressController``)
- The ephemeral state is recomputed since it contains the checkpoint and the checkpoint has changed

Each of these more or less require a command queue "hop" which means other operations can occur between them. As a result, #3467 can still occur since an exploration can be finished before the process happens (though the behavior has changed: the progress simply won't be processed which means an exploration might not be indicated as played, but the checkpointing should almost always be saved before the exploration session end is processed). Fixing this isn't straightforward since the exploration's play state can't be changed until the checkpoint is confirmed as saved which either requires updating ``StoryProgressController`` to also account for the presence of checkpoints, or to use more flows internally to combine the save-and-process operation into one in a way that doesn't lock up ``ExplorationProgressController`` (I'm currently not exactly sure how one would do this, but I haven't thought about it in depth yet).

## 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
This PR does not intentionally change and user-perceivable behaviors in the questions or state players except potentially fixing deadlocks/ANRs that could occur while playing. There could be regressions in the players, and they may potentially be more performant after this change, but neither can easily be demonstrated via videos or screenshots. There are no accessibility behavior changes to demonstrate, either.

However, two key tests have been run and verified as passing on Espresso to demonstrate that the deadlock has been fixed. Specifically:

| QuestionPlayerActivityTest | StateFragmentTest |
|------|------|
| ![deadlock_fix_pr_question_player_activity_test_passing](https://user-images.githubusercontent.com/12983742/158529405-6e19a602-19b8-48ab-814b-980831c57cb4.png) | ![deadlock_fix_pr_state_fragment_test_passing](https://user-images.githubusercontent.com/12983742/158529650-cc6ac45f-dbee-46b6-a732-d2e4a1a1fbcb.png) |

Commit history:

* Change parameterized method delimiter.

* Use utility directly in test.

* Post-merge fixes.

This adjusts for the removal of ComparableOperationList (i.e. no wrapper
proto).

* Add first round of tests.

This includes fixes to the converter itself as it wasn't distributing
both product inversions and negation correctly in several cases. Tests
should now be covering these cases.

* Finish initial test suite.

Still needs to be cleaned up, but after converter refactoring attempts.

* Simplify operation sorting comparators.

* Remove old tests.

* Add remaining missing tests.

* KDocs & test exemption.

* Renames & lint fixes.

* Post-merge fixes.

* Add tests.

* KDocs + exemptions.

Also, clean up polynomial sorting.

* Lint fixes.

* Post-merge fixes.

Also, mark methods/classes that need tests.

* Add extension tests.

* Add classifier tests.

* Use more intentional epsilons for float comparing.

* 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.

* Make allowance for empty cases to fix tests.

These tests correspond to real scenarios.

* Lint fixes.

* 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).

* 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 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.

* 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.

* 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 cc5ba50 commit df0f064
Show file tree
Hide file tree
Showing 36 changed files with 3,374 additions and 1,689 deletions.
12 changes: 11 additions & 1 deletion app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ package_group(
package_group(
name = "app_testing_visibility",
packages = [
"//app/src/sharedTest/...",
"//app/src/test/...",
],
)
Expand All @@ -46,6 +47,12 @@ filegroup(
visibility = ["//:oppia_testing_visibility"],
)

filegroup(
name = "data_binder_mapper_impl",
srcs = ["src/test/java/DataBinderMapperImpl.java"],
visibility = [":app_testing_visibility"],
)

# Source files for the migrated source files library. The files inside the migrated source files
# library are dependencies in app module that have their own libraries.
# Place your files here if:
Expand Down Expand Up @@ -812,7 +819,10 @@ kt_android_library(
"src/sharedTest/java/org/oppia/android/app/utility/ProgressMatcher.kt",
"src/sharedTest/java/org/oppia/android/app/utility/TabMatcher.kt",
],
visibility = ["//app:__subpackages__"],
visibility = [
":app_testing_visibility",
"//app:__subpackages__",
],
deps = [
":app",
"//testing",
Expand Down
30 changes: 20 additions & 10 deletions app/app_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,39 @@ Macros for app module tests.
load("//:oppia_android_test.bzl", "oppia_android_module_level_test")

# TODO(#1620): Remove module-specific test macros once Gradle is removed
def app_test(name, processed_src, test_path_prefix, filtered_tests, deps, **kwargs):
def app_test(
name,
deps,
processed_src = None,
test_class = None,
test_path_prefix = None,
filtered_tests = [],
**kwargs):
"""
Creates individual tests for test files in the app module.
Args:
name: str. The relative path to the Kotlin test file.
processed_src: str. The source to a processed version of the test that should be used
name: str. The relative path to the Kotlin test file, or the name of the suite.
processed_src: str|None. The source to a processed version of the test that should be used
instead of the original.
test_path_prefix: str. The prefix of the test path (which is used to extract the qualified
class name of the test suite).
filtered_tests: list of str. The test files that should not have tests defined for them.
test_class: str|None. The fully qualified test class that will be run (relative to
src/test/java).
test_path_prefix: str|None. The prefix of the test path (which is used to extract the
qualified class name of the test suite).
deps: list of str. The list of dependencies needed to build and run this test.
filtered_tests: list of str. The test files that should not have tests defined for them.
**kwargs: additional parameters passed in.
"""
oppia_android_module_level_test(
name = name,
processed_src = processed_src,
filtered_tests = filtered_tests,
test_path_prefix = test_path_prefix,
deps = deps,
processed_src = processed_src,
test_class = test_class,
test_path_prefix = test_path_prefix,
custom_package = "org.oppia.android.app.test",
test_manifest = "src/test/AndroidManifest.xml",
additional_srcs = ["src/test/java/DataBinderMapperImpl.java"],
test_manifest = "//app:test_manifest",
additional_srcs = ["//app:data_binder_mapper_impl"],
enable_data_binding = True,
**kwargs
)
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ dependencies {
'de.hdodenhof:circleimageview:3.0.1',
'nl.dionsegijn:konfetti:1.2.5',
"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version",
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1',
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1',
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1',
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1',
'org.mockito:mockito-core:2.7.22',
)
compileOnly(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class SubmittedAnswerViewModel(
)
private var accessibleAnswer: String? = DEFAULT_ACCESSIBLE_ANSWER

fun setSubmittedAnswer(submittedAnswer: CharSequence, accessibleAnswer: String? = null) {
fun setSubmittedAnswer(submittedAnswer: CharSequence, accessibleAnswer: String?) {
this.submittedAnswer.set(submittedAnswer)
this.accessibleAnswer = accessibleAnswer
updateSubmittedAnswerContentDescription()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
Tests for the core state player experience.
"""

load("@dagger//:workspace_defs.bzl", "dagger_rules")
load("//app:app_test.bzl", "app_test")
load("//app:test_with_resources.bzl", "test_with_resources")

app_test(
name = "StateFragmentTest",
processed_src = test_with_resources("StateFragmentTest.kt"),
test_class = "org.oppia.android.app.player.state.StateFragmentTest",
deps = [
":dagger",
"//app",
"//app:test_deps",
"//app/src/main/java/org/oppia/android/app/translation/testing:test_module",
"//data/src/main/java/org/oppia/android/data/backends/gae:prod_module",
"//domain/src/main/java/org/oppia/android/domain/onboarding/testing:retriever_test_module",
"//domain/src/main/java/org/oppia/android/domain/translation:translation_controller",
"//testing",
"//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor",
"//testing/src/main/java/org/oppia/android/testing/espresso:edit_text_input_action",
"//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule",
"//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
"//testing/src/main/java/org/oppia/android/testing/threading:coroutine_executor_service",
"//testing/src/main/java/org/oppia/android/testing/threading:test_module",
"//testing/src/main/java/org/oppia/android/testing/time:test_module",
"//third_party:androidx_test_ext_junit",
"//third_party:com_github_bumptech_glide_mocks",
"//third_party:com_google_truth_truth",
"//third_party:junit_junit",
"//third_party:org_robolectric_robolectric",
"//third_party:robolectric_android-all",
"//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/data:data_providers",
"//utility/src/main/java/org/oppia/android/util/locale/testing:test_module",
"//utility/src/main/java/org/oppia/android/util/logging:prod_module",
"//utility/src/main/java/org/oppia/android/util/networking:debug_module",
"//utility/src/main/java/org/oppia/android/util/networking:debug_util_module",
],
)

dagger_rules()
18 changes: 18 additions & 0 deletions config/proguard/kotlin-proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,21 @@
# run into runtime issues if something is unintentionally removed.
-dontwarn android.support.annotation.Keep
-dontwarn android.support.annotation.VisibleForTesting

# https://github.com/Kotlin/kotlinx.coroutines/issues/2046 describes some of the classes which are
# safe to ignore due to kotlinx.coroutines dependencies. All of the following are sourced from:
# https://github.com/Kotlin/kotlinx.coroutines/blob/bc120a/kotlinx-coroutines-core/jvm/resources/META-INF/com.android.tools/proguard/coroutines.pro
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembers class kotlinx.coroutines.** {
volatile <fields>;
}
-keepclassmembers class kotlin.coroutines.SafeContinuation {
volatile <fields>;
}
-dontwarn java.lang.ClassValue
-dontwarn java.lang.instrument.ClassFileTransformer
-dontwarn java.lang.instrument.Instrumentation
-dontwarn sun.misc.SignalHandler
-dontwarn sun.misc.Signal
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
2 changes: 1 addition & 1 deletion data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ dependencies {
'com.google.protobuf:protobuf-javalite:3.17.3',
'com.squareup.moshi:moshi-kotlin:1.11.0',
'com.squareup.okhttp3:okhttp:4.7.2',
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2',
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1',
)
compileOnly(
'jakarta.xml.bind:jakarta.xml.bind-api:2.3.2',
Expand Down
2 changes: 1 addition & 1 deletion domain/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ dependencies {
'com.squareup.okhttp3:mockwebserver:4.7.2',
'junit:junit:4.12',
"org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version",
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2',
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2',
'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2',
'org.mockito:mockito-core:2.19.0',
'org.robolectric:robolectric:4.5',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,26 @@ class ExplorationDataController @Inject constructor(
/**
* Begins playing an exploration of the specified ID.
*
* This method is not expected to fail.
*
* [ExplorationProgressController] should be used to manage the play state, and monitor the load
* success/failure of the exploration.
*
* This must be called only if no active exploration is being played. The previous exploration
* must have first been stopped using [stopPlayingExploration], otherwise the operation will fail.
* This can be called even if a session is currently active as it will force initiate a new play
* session, resetting any data from the previous session (though any pending unsaved checkpoint
* progress is guaranteed to be saved from the previous session, first).
*
* [stopPlayingExploration] may be optionally called to clean up the session--see the
* documentation for that method for details.
*
* @param internalProfileId the ID corresponding to the profile for which exploration has to be
* played
* @param topicId the ID corresponding to the topic for which exploration has to be played
* @param storyId the ID corresponding to the story for which exploration has to be played
* @param explorationId the ID of the exploration which has to be played
* @param shouldSavePartialProgress the boolean that indicates if partial progress has to be saved
* for the current exploration
* @param shouldSavePartialProgress indicates if partial progress should be saved for the new play
* session
* @param explorationCheckpoint the checkpoint which may be used to resume the exploration
* @return a one-time [DataProvider] to observe whether initiating the play request succeeded.
* The exploration may still fail to load, but this provides early-failure detection.
* @return a [DataProvider] to observe whether initiating the play request, or future play
* requests, succeeded
*/
fun startPlayingExploration(
internalProfileId: Int,
Expand All @@ -68,7 +70,7 @@ class ExplorationDataController @Inject constructor(
explorationCheckpoint: ExplorationCheckpoint
): DataProvider<Any?> {
return explorationProgressController.beginExplorationAsync(
internalProfileId,
ProfileId.newBuilder().apply { internalId = internalProfileId }.build(),
topicId,
storyId,
explorationId,
Expand All @@ -79,10 +81,16 @@ class ExplorationDataController @Inject constructor(

/**
* Finishes the most recent exploration started by [startPlayingExploration], and returns a
* one-off [DataProvider] indicating whether the operation succeeded.
* [DataProvider] indicating whether the operation succeeded.
*
* This method should only be called if an active exploration is being played, otherwise the
* operation will fail.
* resulting provider will fail. Note that this doesn't actually need to be called between
* sessions unless the caller wants to ensure other providers monitored from
* [ExplorationProgressController] are reset to a proper out-of-session state.
*
* Note that the returned provider monitors the long-term stopping state of exploration sessions
* and will be reset to 'pending' when a session is currently active, or before any session has
* started.
*/
fun stopPlayingExploration(): DataProvider<Any?> =
explorationProgressController.finishExplorationAsync()
Expand Down
Loading

0 comments on commit df0f064

Please sign in to comment.