Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
, #4856: Assorted alpha MR6 fixes (#4846)

## Explanation
Fixes #4833
Fixes #4834
Fixes #4835
Fixes #4838
Fixes #1050
Fixes #4519
Fixes #4522
Fixes #4837
Fixes #4836
Fixes #4855
Fixes #4856

This PR introduces a variety of fixes and user quality-of-life
improvements in preparation for the start of the upcoming multi-week
user research project in Kenya. This PR covers 9 different issues in one
shot to keep the PR management simpler due to the time sensitive nature
of these changes, so it's on the larger side. Please note that:
- These changes have been released to a user study specific release
track since the research project is currently ongoing.
- Many of these changes will impact users outside of the study, as
detailed below.

The specifics of the PR will be split across the different issues being
addressed, in categories below.

### Improvements to solutions

#4833 needed to be addressed because the previous terminology ("reveal")
has been found to be confusing to learners who might not yet have a
strong grasp on English. This string update will, unfortunately, not be
translated for the upcoming study (though the study predominantly uses
English). This change affects all users of the app.

#1050 is a long-standing issue wherein solution answers would only show
up correctly if they were either text or a fraction. This PR fixes the
issue by introducing generic answer support, expanding it to include:
numeric input, numeric expression, algebraic expression, algebraic
equation, and ratio expression. The only missing interaction that web
supports is drag & drop, but this has been found to not be needed for
the current shipped list of topics (as now enforced by an update to the
content import pipeline) and introducing support for it would
nontrivially increase the complexity of that side of the PR. This is
something that, like other interactions, will be added in the future
once needed. This fix will affect all users of the app.

### Improvements to voiceover telemetry

#4834 was addressed by introducing a new event to track explicit
voiceover pauses (i.e. those that require clicking the 'pause' button
and not the ones that are automatic, such as autoplay ending or the user
navigating away from the lesson player). Because of the nature of the
'play voiceover' event (that it is logged both for clicking the 'play'
button and when expanding the audio bar due to autoplay), it's possible
for the play & pause event counts to not be equal. The intent behind
these events is to track explicit user actions, not the occurrences of
voiceovers playing/not playing. This event will be logged for all users,
though sensitive identifiers will only be logged if the user study
feature is enabled.

#4835 was addressed by updating both the existing 'play voiceover' and
new 'pause vocieover' events to include the lesson player-reported
language code (that is, the language code originating from Oppia web).
We may change this to be something more strongly ensured (since Oppia
Android is a bit more careful in its language code management due to
Android complexities), but the code should be generally usable as-is for
data purposes as it's implemented. This change will affect telemetry for
all users of the app.

### Improvements to concept card availability

#4519 and #4522 were addressed by adding support for opening concept
cards both in concept cards themselves, and from hints. As of this PR,
just about all cases of lesson HTML rendering should now include support
for linkifying and opening concept card references. Since both hints and
concept cards are dialogs, the behavior is that the newly opened concept
card dialog "stacks" on top of the existing, open dialog (i.e. closing
the concept card will show the original dialog from which it was
opened). This change affects all users of the app.

### Miscellaneous language improvements

#4837 was addressed to clean up a variety of extraneous spaces &
newlines that were unintentionally added by translators for Swahili
strings. These have a direct user benefit but are otherwise innocuous,
non-semantic changes. This fix technically affects all users of the app,
but only those who use the Swahili translations will actually interact
with the changed strings.

### Study-specific improvements

#4838 was addressed by introducing a new link in the profile edit flow
for administrators to access the previously developer-only flow for
manually marking certain chapters as completed. This newly exposed
mechanism allows study facilitators to set up profiles so that learners
can begin at a specific point (which is particularly helpful if the data
on the device ever needs to be reset, such as in the case where the
administrator forgot their PIN). This change will only affect user study
administrators as it's locked both behind the admin account and the
learner study flag. The functionality *will* be available on
production-optimized builds (that is, it won't be restricted to
developer-only build flavors of the app).

#4836 was addressed by introducing a floating button within the lesson
player for lesson cards whose content have available Swahili
translations to quickly switch the content language between English and
Swahili. This doesn't change app strings, only content strings, and only
temporarily for the lifetime of that specific play session (that is, if
they exit the lesson & return immediately they will still be in the
switched language until the app itself restarts). This button only
appears if the study feature is enabled and has been enabled for that
user by the administrator profile (via a new setting on the "Edit
Profile" screen). This button also has its own new event to track its
usage.

A "share IDs" button was added to make it easier to share the device &
learner IDs by collating the IDs into a single blob of text that can be
shared to any app that accepts text intents. This is meant to help
reduce potential error by study coordinators.

### Content display improvements

#4855 was addressed by changing a lot of how custom LI/OL tags are
handled. Specifically, Android will automatically shift the leading span
of cascading bullets/numbers, but the previous implementation was using
the wrong value for each nested span's parent's leading margin (which is
needed to adjust what leading margin to use for the child span). This
was fixed by introducing more thorough tracking (which also included a
bit of robustness in ensuring larger numbers can be aligned correctly).
This doesn't quite match HTML rendering, but it's much closer. Bullet
lists were also updated to use squares in addition to filled and open
circles (for parity with web HTML), and now uses sizes & spacing that
are a bit cleaner to see.

There's still more work to improve edge cases for bullet list rendering
(see #4859).

#4856 was addressed by introducing a separate SVG "render size" that,
unlike the intrinsic size, is based on the image filename. Since the
image filename is given in pixels, there needs to be a translation to dp
to ensure consistent viewing across devices. This computation has been
implemented with three calculation passes:
1. Convert the image's target pixel size to "Oppia independent pixels"
by using @seanlip's monitor display density as the basis.
2. Convert "Oppia independent pixels" into Android density-independent
pixels.
3. Adjust the final value using a scalar (since the physical size of an
image on @seanlip's display is too large for a small mobile screen, so
all images are consistently further scaled down).

There's still more work to improve image rendering (see #4093).

### General technical details

Design choices & other details of note:
- The chapter completion flow was updated to support configuring a
confirmation dialog (since the action is permanent). This confirmation
only shows for the admin edit profile flow (the developer options menu
version still does not show a confirmation).
- The hints & solution data flow was completely redone in the app layer
since much of what it was doing before isn't actually necessary with
``HelpIndex`` as it greatly reduces the complexity of managing UI-side
hint state. Since solutions were being updated, anyway, this unrelated
complexity has been reduced (and other issues like directly constructing
an injectable view model were addressed).
- Solution titles were previously set to their content ID (similar to
hints before this was recently fixed), so this was addressed by using a
new "Solution" string.
- Displayed solution answers didn't correctly differentiate between
exclusive & example answers. This has been corrected in text (along with
how the app generally concatenates the text as the previous approach
wasn't very RTL friendly).
- Generalizing solutions was done by leveraging an InteractionObject for
Solution's correct_answer field. No migration is being done here since
assets are replaced across releases.
- There are a lot of changes to both json and textproto assets to
accommodate the new correct_answer structure, among other various
changes. A few of these were done manually, but most were generated
(including new Swahili translations to allow local testing of the
in-lesson language switch button).
- A bug was noticed & fixed wherein states with only a solution (and no
hints) would never show the solution.
- Due to the concatenation strategy for solution correct answer text
(see a few points above), the horizontal textual alignment was a bit
off. The new approach addresses this (though LaTeX is still a bit off,
but it's generally off everywhere in the app).
- To make the new in-lesson switch language button a bit more consistent
and aesthetically pleasing, this PR repurposes the "Start over" button
that currently shows on the checkpoint continue screen by making it a
generic "secondary button" style (with corresponding colors) and using
that both for the start over button and this new secondary button. Per
the screenshots below, this seems to work well. The secondary button may
also be used elsewhere in the app in the future.
- Note that some of the work of this PR originates from #4529, so it can
be considered a replacement for that PR.
- Only the first test topic was updated to include Swahili translations
to help decrease the size of the PR (plus, it's convenient to have
non-Swahili lessons to verify cases when the switch button does **not**
show up).
- The PR fixes an incidental issue with the hints & solution dialog
whereby clicking any part of an expanded hint/solution card would
collapse it (rather than just the header or collapse icon). This would
make clicking links more difficult, and is just generally a likely
unexpected behavior from users.
- The new event log & language code property have been manually verified
as logging correctly using the developer Firebase project with debug
view enabled.
- A new event was added to track when users leave revision cards.
- A bug was found & fixed that led to tab switches in the topic activity
not having their corresponding events properly logged.
- All events have been updated to now include app string, content
string, and voiceover languages per-profile for that event (which led to
a fairly large amount of complexity around providing access to those
languages at a low level like ``AnalyticsController``).
- A bug was found & fixed around session ID management: previously, the
session ID was supposed to be reset upon a new lesson being started but
it actually wasn't behaving this way. Per the new needs of analytics,
this behavior was changed & fixed to reset upon profile login (so that
behaviors for a single user session can be properly correlated).

Noteworthy test & exemption changes:
- ``StateRetrieverTest`` is largely not updated since all of the JSON
changes in this PR only affect local development (all Bazel tests &
production builds use textproto or binary proto versions of lessons).
The tests locally passing plus some basic local ad hoc testing is
considered sufficient for the JSON loading pipeline as it will,
eventually, go away in favor of textproto (once Gradle is removed since
textproto -> binary proto conversion is not easily implemented in the
Gradle build environment).
- Some of the question test suites weren't updated since questions
aren't currently enabled, and much of the question pipeline is already
covered under state fragment tests. It's likely that the testing matrix
for questions will need to be largely refined once they're ready to be
enabled, so some of this work was skipped in favor of expediency.
- Some tests in StateFragmentTest & StateFragmentLocalTest are disabled
and can't easily be verified because they would only pass on Espresso
and Bazel (due to Robolectric having limitations in language resource
handling & Gradle builds not supporting the app's language
configurations). Since Espresso tests do not yet work in Bazel, these
tests will be verified later (though they have been manually verified as
part of development).
- A bunch of KDoc validity exemptions were removed since those classes
are now, as of this PR, fully documented.
- The test_file_exemptions updates are due to HintsViewModel being split
into two new view models, and the general convention on the team to not
directly test view models.

## 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

### Screenshots showing some of the new functionality
| **Scenario** | LTR | LTR | LTR | LTR | RTL | RTL | RTL | RTL |

|----------------------------------------------|----------|-----------|----------|-----------|----------|-----------|----------|-----------|
| | Handset | Handset | Tablet | Tablet | Handset | Handset | Tablet |
Tablet |
| | Portrait | Landscape | Portrait | Landscape | Portrait | Landscape |
Portrait | Landscape |
| Fractions solution (exclusive) |
![image](https://user-images.githubusercontent.com/12983742/213827471-4efb3c0d-b4fc-4a7b-a892-7467535ef67a.png)
|
![image](https://user-images.githubusercontent.com/12983742/213827548-ca856160-30e3-4e1a-a423-91e241000fc3.png)
| Skipped until requested | Skipped until requested |
![image](https://user-images.githubusercontent.com/12983742/213828637-dc6a6b78-e36c-4829-a04f-3a69a3035b4b.png)
|
![image](https://user-images.githubusercontent.com/12983742/213828651-23419f3e-64bb-495c-b0b5-27f17f1eb2fc.png)
| Skipped until requested | Skipped until requested |
| Numeric solution (exclusive) |
![image](https://user-images.githubusercontent.com/12983742/213827605-4b767195-a9a2-4790-9156-b5f18edbd4ed.png)
|
![image](https://user-images.githubusercontent.com/12983742/213827632-2e02495d-83e6-48ad-8652-ba97a3e2c3e2.png)
| Skipped until requested | Skipped until requested |
![image](https://user-images.githubusercontent.com/12983742/213828696-7b8367dc-5f56-4a3a-ad00-441610915e66.png)
|
![image](https://user-images.githubusercontent.com/12983742/213828705-aa6651bb-ee77-46e0-a3e2-612e340254a9.png)
| Skipped until requested | Skipped until requested |
| Ratio solution (not exclusive) |
![image](https://user-images.githubusercontent.com/12983742/213827665-bc2c4fc3-f77f-4a3e-9817-c6a4eaf3a435.png)
|
![image](https://user-images.githubusercontent.com/12983742/213827677-05f004d0-c6f1-46ea-bc91-afbe036654a5.png)
| Skipped until requested | Skipped until requested |
![image](https://user-images.githubusercontent.com/12983742/213828734-af173be7-6e88-43b3-aeff-c3da4f7f220d.png)
|
![image](https://user-images.githubusercontent.com/12983742/213828745-03d91530-fcbb-4205-b091-e638b818f056.png)
| Skipped until requested | Skipped until requested |
| Text solution (exclusive) |
![image](https://user-images.githubusercontent.com/12983742/213827705-5d6cd4c5-50a6-46b7-88a2-709e8c6eb981.png)
|
![image](https://user-images.githubusercontent.com/12983742/213827720-2e5982d2-6a41-4ec9-b5f2-26d714e27bff.png)
| Skipped until requested | Skipped until requested |
![image](https://user-images.githubusercontent.com/12983742/213828781-17764229-8a6f-4bb8-a3c3-1dc30207e322.png)
|
![image](https://user-images.githubusercontent.com/12983742/213828793-feeeea82-8475-4bbb-946c-b84c9a0dd988.png)
| Skipped until requested | Skipped until requested |
| Numeric expression solution (not exclusive) |
![image](https://user-images.githubusercontent.com/12983742/213827798-19adb687-ba51-4c53-abf8-6cb2a70f8d6e.png)
|
![image](https://user-images.githubusercontent.com/12983742/213827818-f9108646-2182-459f-a236-f798d6e73814.png)
| Skipped until requested | Skipped until requested |
![image](https://user-images.githubusercontent.com/12983742/213828847-4a4c9433-29bc-4bd9-8439-e922cbb41b4f.png)
|
![image](https://user-images.githubusercontent.com/12983742/213828860-caa1f397-8410-4676-8f22-901ba3614465.png)
| Skipped until requested | Skipped until requested |
| Algebraic expression solution (not exclusive) |
![image](https://user-images.githubusercontent.com/12983742/213827838-42768b22-a5a4-4065-906e-96ced3c4a09d.png)
|
![image](https://user-images.githubusercontent.com/12983742/213827851-98f2c08c-52fc-4eeb-a8dc-3068dc9df160.png)
| Skipped until requested | Skipped until requested |
![image](https://user-images.githubusercontent.com/12983742/213828978-461c2788-1295-4ad7-8a3c-3cba79fa1429.png)
|
![image](https://user-images.githubusercontent.com/12983742/213828991-2bbf31bf-8c41-4d67-94b6-e153943246cb.png)
| Skipped until requested | Skipped until requested |
| Math equation solution (not exclusive) |
![image](https://user-images.githubusercontent.com/12983742/213827886-4feeb3bd-6286-494b-8092-f6e0bc56846a.png)
|
![image](https://user-images.githubusercontent.com/12983742/213827899-1b91d8ca-2bcc-4d53-b17a-304e1a2a2260.png)
| Skipped until requested | Skipped until requested |
![image](https://user-images.githubusercontent.com/12983742/213829068-b0ffa99c-a19f-4682-bbb3-218cd43d9733.png)
|
![image](https://user-images.githubusercontent.com/12983742/213829077-14e7b2ff-4c9a-4402-86bf-785cdf6de95f.png)
| Skipped until requested | Skipped until requested |
| Switch English to Swahili |
![image](https://user-images.githubusercontent.com/12983742/213827944-a3ce6d77-05a4-4ece-8d48-fad1af101ffa.png)
|
![image](https://user-images.githubusercontent.com/12983742/213827953-3082154d-43fd-42c2-9b5c-2ecb5defae27.png)
| Skipped until requested | Skipped until requested |
![image](https://user-images.githubusercontent.com/12983742/213829090-2bef2148-7a98-451b-adfc-bd6fec529153.png)
|
![image](https://user-images.githubusercontent.com/12983742/213829099-3c64b6e7-2aa4-44c9-b4f3-22a30f389fde.png)
| Skipped until requested | Skipped until requested |
| Switch Swahili to English |
![image](https://user-images.githubusercontent.com/12983742/213827966-df757f7b-9148-4391-8768-ac0202a82a32.png)
|
![image](https://user-images.githubusercontent.com/12983742/213827978-6c7a4ffd-1a2c-4a88-be60-90341f24110f.png)
| Skipped until requested | Skipped until requested |
![image](https://user-images.githubusercontent.com/12983742/213829129-d87d7109-7e17-4b7a-81c0-d80b7a7ac3bd.png)
|
![image](https://user-images.githubusercontent.com/12983742/213829143-43b44f5d-1a46-4872-a15e-081fdb7fa440.png)
| Skipped until requested | Skipped until requested |
| Mark chapters as completed (in profile edit) |
![image](https://user-images.githubusercontent.com/12983742/213828020-c6ab793a-6ab6-4fdb-a243-7804f94b7931.png)
|
![image](https://user-images.githubusercontent.com/12983742/213828127-0f9c5015-692f-4b02-81f2-bf2ba2d419b0.png)
|
![image](https://user-images.githubusercontent.com/12983742/213832145-17012205-7e66-4e63-b30a-27cb66b84bc5.png)
|
![image](https://user-images.githubusercontent.com/12983742/213832158-35ce3573-d5fb-46c0-86b1-328ec45f935a.png)
|
![image](https://user-images.githubusercontent.com/12983742/213828513-ff94895f-0d38-4747-9139-e121e44e7d0e.png)
|
![image](https://user-images.githubusercontent.com/12983742/213828526-7a3a0db0-a385-46ed-b6f8-a853329cdf6c.png)
| Skipped until requested | Skipped until requested |

A couple things to note:
- Switching from English to Swahili & back is a bit awkward in the RTL
screenshots (partly because the RTL layout is maintained despite
switching the content language). This isn't actually a realistic
scenario to occur, but it's technically possible so demonstrating it
above still makes sense. There are no plans to refine this flow.
- The landscape screenshots for the profile edit screens clearly show a
broken "Profile Deletion" button placement. This is unfortunately an
issue already on develop, and #4852 has been filed to track this.

### Videos demonstrating different changed/new flows
| Scenario | Video demonstration |

|----------------------------------------------------|---------------------|
| Switch language in-lesson |
https://user-images.githubusercontent.com/12983742/213829720-075cbb64-cc5a-4a08-ab3b-f6d123650fac.mp4
|
| Mark chapters completed |
https://user-images.githubusercontent.com/12983742/213829850-e0e157c1-520b-4ff1-86f8-470eb8c37536.mp4
|
| Open concept card from hints & other concept cards |
https://user-images.githubusercontent.com/12983742/213829894-28228cf9-4c0a-4eca-a617-325676b8d538.mp4
|
| Accessibility flow for solutions (fractions, numeric, ratios, text) |
https://user-images.githubusercontent.com/12983742/213830378-a56235c2-bc78-4c1f-9248-9531ae0f024d.mp4
|
| Accessibility flow for solutions (numeric expressions, algebraic
expressions, math equations) |
https://user-images.githubusercontent.com/12983742/213830403-b964485b-9b49-43b8-9dfd-35f904c0974e.mp4
|

### Espresso test results
The following tests have been modified as a result of this PR:
- MarkChaptersCompletedActivityTest
- MarkChaptersCompletedFragmentTest
- ExplorationActivityTest
- StateFragmentTest
- ProfileEditFragmentTest
- StoryFragmentTest
- NavigationDrawerActivityDebugTest
- ConceptCardFragmentTest

I have **not** run the Espresso tests for these suites. I ran into some
issues getting the suites to kick off locally, and I won't have time to
investigate this for a bit due to other competing priorities. Reviewers:
please let me know if you would like these run and I will prioritize
accordingly (I'm leaning toward not running them since many of the
Espresso tests are already failing on develop, and we have no viable way
to keep them passing until we can run them in CI).
  • Loading branch information
BenHenning committed Mar 10, 2023
1 parent a541783 commit 0290ef8
Show file tree
Hide file tree
Showing 205 changed files with 10,793 additions and 2,855 deletions.
7 changes: 5 additions & 2 deletions app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [
"src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/DeviceIdItemViewModel.kt",
"src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileLearnerIdItemViewModel.kt",
"src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileListViewModel.kt",
"src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ShareIdsViewModel.kt",
"src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/SyncStatusItemViewModel.kt",
"src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeViewModel.kt",
"src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt",
Expand All @@ -200,7 +201,8 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [
"src/main/java/org/oppia/android/app/help/thirdparty/LicenseListViewModel.kt",
"src/main/java/org/oppia/android/app/help/thirdparty/LicenseTextViewModel.kt",
"src/main/java/org/oppia/android/app/help/thirdparty/ThirdPartyDependencyListViewModel.kt",
"src/main/java/org/oppia/android/app/hintsandsolution/HintsViewModel.kt",
"src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionViewModel.kt",
"src/main/java/org/oppia/android/app/hintsandsolution/SolutionViewModel.kt",
"src/main/java/org/oppia/android/app/home/HomeViewModel.kt",
"src/main/java/org/oppia/android/app/home/WelcomeViewModel.kt",
"src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicsViewModel.kt",
Expand Down Expand Up @@ -283,9 +285,9 @@ VIEW_MODELS = [
"src/main/java/org/oppia/android/app/help/HelpItems.kt",
"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/HintViewModel.kt",
"src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionItemViewModel.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",
"src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedItemViewModel.kt",
Expand Down Expand Up @@ -874,6 +876,7 @@ TEST_DEPS = [
"//testing/src/main/java/org/oppia/android/testing/espresso:konfetti_view_matcher",
"//testing/src/main/java/org/oppia/android/testing/espresso:text_input_action",
"//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule",
"//testing/src/main/java/org/oppia/android/testing/logging:event_log_subject",
"//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject",
"//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject",
"//testing/src/main/java/org/oppia/android/testing/mockito",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.oppia.android.app.recyclerview.BindableAdapter
import org.oppia.android.databinding.ProfileAndDeviceIdFragmentBinding
import org.oppia.android.databinding.ProfileListDeviceIdItemBinding
import org.oppia.android.databinding.ProfileListLearnerIdItemBinding
import org.oppia.android.databinding.ProfileListShareIdsItemBinding
import org.oppia.android.databinding.ProfileListSyncStatusItemBinding
import javax.inject.Inject

Expand Down Expand Up @@ -46,27 +47,29 @@ class ProfileAndDeviceIdFragmentPresenter @Inject constructor(
is DeviceIdItemViewModel -> ProfileListItemViewType.DEVICE_ID
is ProfileLearnerIdItemViewModel -> ProfileListItemViewType.LEARNER_ID
is SyncStatusItemViewModel -> ProfileListItemViewType.SYNC_STATUS
is ShareIdsViewModel -> ProfileListItemViewType.SHARE_IDS
else -> error("Encountered unexpected view model: $viewModel")
}
}
.registerViewDataBinder(
viewType = ProfileListItemViewType.DEVICE_ID,
inflateDataBinding = ProfileListDeviceIdItemBinding::inflate,
setViewModel = ProfileListDeviceIdItemBinding::setViewModel,
transformViewModel = { it as DeviceIdItemViewModel }
)
.registerViewDataBinder(
viewType = ProfileListItemViewType.LEARNER_ID,
inflateDataBinding = ProfileListLearnerIdItemBinding::inflate,
setViewModel = ProfileListLearnerIdItemBinding::setViewModel,
transformViewModel = { it as ProfileLearnerIdItemViewModel }
)
.registerViewDataBinder(
viewType = ProfileListItemViewType.SYNC_STATUS,
inflateDataBinding = ProfileListSyncStatusItemBinding::inflate,
setViewModel = ProfileListSyncStatusItemBinding::setViewModel,
transformViewModel = { it as SyncStatusItemViewModel }
)
.build()
}.registerViewDataBinder(
viewType = ProfileListItemViewType.DEVICE_ID,
inflateDataBinding = ProfileListDeviceIdItemBinding::inflate,
setViewModel = ProfileListDeviceIdItemBinding::setViewModel,
transformViewModel = { it as DeviceIdItemViewModel }
).registerViewDataBinder(
viewType = ProfileListItemViewType.LEARNER_ID,
inflateDataBinding = ProfileListLearnerIdItemBinding::inflate,
setViewModel = ProfileListLearnerIdItemBinding::setViewModel,
transformViewModel = { it as ProfileLearnerIdItemViewModel }
).registerViewDataBinder(
viewType = ProfileListItemViewType.SYNC_STATUS,
inflateDataBinding = ProfileListSyncStatusItemBinding::inflate,
setViewModel = ProfileListSyncStatusItemBinding::setViewModel,
transformViewModel = { it as SyncStatusItemViewModel }
).registerViewDataBinder(
viewType = ProfileListItemViewType.SHARE_IDS,
inflateDataBinding = ProfileListShareIdsItemBinding::inflate,
setViewModel = ProfileListShareIdsItemBinding::setViewModel,
transformViewModel = { it as ShareIdsViewModel }
).build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,18 @@ class ProfileListViewModel private constructor(
private val oppiaLogger: OppiaLogger,
private val deviceIdItemViewModelFactory: DeviceIdItemViewModel.Factory,
private val syncStatusItemViewModelFactory: SyncStatusItemViewModel.Factory,
private val profileLearnerIdItemViewModelFactory: ProfileLearnerIdItemViewModel.Factory
private val profileLearnerIdItemViewModelFactory: ProfileLearnerIdItemViewModel.Factory,
private val shareIdsViewModelFactory: ShareIdsViewModel.Factory
) : ObservableViewModel() {
/** The list of [ProfileListItemViewModel] to display. */
val profileModels: LiveData<List<ProfileListItemViewModel>> by lazy {
Transformations.map(
profileManagementController.getProfiles().toLiveData(), ::processProfiles
)
Transformations.map(profileManagementController.getProfiles().toLiveData(), ::processProfiles)
}

private fun processProfiles(
profilesResult: AsyncResult<List<Profile>>
): List<ProfileListItemViewModel> {
val deviceIdViewModel = deviceIdItemViewModelFactory.create()
val syncStatusViewModel = syncStatusItemViewModelFactory.create()

val learnerIdModels = when (profilesResult) {
is AsyncResult.Pending -> listOf()
Expand All @@ -56,7 +54,10 @@ class ProfileListViewModel private constructor(
}
}

return listOf(deviceIdViewModel) + learnerIdModels + listOf(syncStatusViewModel)
val idModels = listOf(deviceIdViewModel) + learnerIdModels
val shareIdsViewModel = shareIdsViewModelFactory.create(idModels)
val syncStatusViewModel = syncStatusItemViewModelFactory.create()
return idModels + listOf(syncStatusViewModel, shareIdsViewModel)
}

/**
Expand All @@ -80,7 +81,10 @@ class ProfileListViewModel private constructor(
LEARNER_ID,

/** Corresponds to [SyncStatusItemViewModel]. */
SYNC_STATUS
SYNC_STATUS,

/** Corresponds to [ShareIdsViewModel]. */
SHARE_IDS
}

/** Factory to create new [ProfileListViewModel]s. */
Expand All @@ -89,7 +93,8 @@ class ProfileListViewModel private constructor(
private val oppiaLogger: OppiaLogger,
private val deviceIdItemViewModelFactory: DeviceIdItemViewModel.Factory,
private val syncStatusItemViewModelFactory: SyncStatusItemViewModel.Factory,
private val profileLearnerIdItemViewModelFactory: ProfileLearnerIdItemViewModel.Factory
private val profileLearnerIdItemViewModelFactory: ProfileLearnerIdItemViewModel.Factory,
private val shareIdsViewModelFactory: ShareIdsViewModel.Factory
) {
/** Returns a new [ProfileListViewModel]. */
fun create(): ProfileListViewModel {
Expand All @@ -98,7 +103,8 @@ class ProfileListViewModel private constructor(
oppiaLogger,
deviceIdItemViewModelFactory,
syncStatusItemViewModelFactory,
profileLearnerIdItemViewModelFactory
profileLearnerIdItemViewModelFactory,
shareIdsViewModelFactory
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.oppia.android.app.administratorcontrols.learneranalytics

import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import org.oppia.android.app.administratorcontrols.learneranalytics.ProfileListViewModel.ProfileListItemViewModel
import org.oppia.android.domain.oppialogger.OppiaLogger
import javax.inject.Inject

/**
* [ProfileListItemViewModel] that represents the portion of the learner analytics admin page which
* provides a button to the user that allows them to share the device's installation ID, along with
* profile IDs, to an external app for record keeping.
*/
@SuppressLint("StaticFieldLeak") // False positive (Android doesn't manage this model's lifecycle).
class ShareIdsViewModel private constructor(
private val oppiaLogger: OppiaLogger,
private val activity: AppCompatActivity,
private val viewModels: List<ProfileListItemViewModel>
) : ProfileListItemViewModel(ProfileListViewModel.ProfileListItemViewType.SHARE_IDS) {
/** Indicates the user wants to share learner & the device IDs with another app. */
fun onShareIdsButtonClicked() {
// Reference: https://developer.android.com/guide/components/intents-common#Email from
// https://stackoverflow.com/a/15022153/3689782.
val sharedText = viewModels.mapNotNull { viewModel ->
when (viewModel) {
is DeviceIdItemViewModel -> "Oppia app installation ID: ${viewModel.deviceId.value}"
is ProfileLearnerIdItemViewModel -> {
val profile = viewModel.profile
"- Profile name: ${profile.name}, learner ID: ${profile.learnerId}"
}
else -> null
}
}.joinToString(separator = "\n")
try {
activity.startActivity(
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(Intent.EXTRA_TEXT, sharedText)
}
)
} catch (e: ActivityNotFoundException) {
oppiaLogger.e("ProfileAndDeviceIdViewModel", "No activity found to receive shared IDs.", e)
}
}

/** Factory for creating new [ShareIdsViewModel]s. */
class Factory @Inject constructor(
private val oppiaLogger: OppiaLogger,
private val activity: AppCompatActivity
) {
/** Returns a new [ShareIdsViewModel]. */
fun create(viewModels: List<ProfileListItemViewModel>): ShareIdsViewModel =
ShareIdsViewModel(oppiaLogger, activity, viewModels)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ class DeveloperOptionsActivity :

override fun routeToMarkChaptersCompleted() {
startActivity(
MarkChaptersCompletedActivity
.createMarkChaptersCompletedIntent(this, internalProfileId)
MarkChaptersCompletedActivity.createMarkChaptersCompletedIntent(
context = this, internalProfileId, showConfirmationNotice = false
)
)
}

Expand Down Expand Up @@ -86,10 +87,6 @@ class DeveloperOptionsActivity :
decorateWithScreenName(DEVELOPER_OPTIONS_ACTIVITY)
}
}

fun getIntentKey(): String {
return NAVIGATION_PROFILE_ID_ARGUMENT_KEY
}
}

override fun forceCrash() {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ class MarkChaptersCompletedActivity : InjectableAppCompatActivity() {
@Inject
lateinit var resourceHandler: AppLanguageResourceHandler

private var internalProfileId = -1

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(activityComponent as ActivityComponentImpl).inject(this)
internalProfileId = intent.getIntExtra(PROFILE_ID_EXTRA_KEY, -1)
markChaptersCompletedActivityPresenter.handleOnCreate(internalProfileId)
val internalProfileId = intent.getIntExtra(PROFILE_ID_EXTRA_KEY, /* defaultValue= */ -1)
val showConfirmationNotice =
intent.getBooleanExtra(SHOW_CONFIRMATION_NOTICE_EXTRA_KEY, /* defaultValue= */ false)
markChaptersCompletedActivityPresenter.handleOnCreate(internalProfileId, showConfirmationNotice)
title = resourceHandler.getStringInLocale(R.string.mark_chapters_completed_activity_title)
}

Expand All @@ -39,11 +39,18 @@ class MarkChaptersCompletedActivity : InjectableAppCompatActivity() {
}

companion object {
const val PROFILE_ID_EXTRA_KEY = "MarkChaptersCompletedActivity.profile_id"

fun createMarkChaptersCompletedIntent(context: Context, internalProfileId: Int): Intent {
private const val PROFILE_ID_EXTRA_KEY = "MarkChaptersCompletedActivity.profile_id"
private const val SHOW_CONFIRMATION_NOTICE_EXTRA_KEY =
"MarkChaptersCompletedActivity.show_confirmation_notice"

fun createMarkChaptersCompletedIntent(
context: Context,
internalProfileId: Int,
showConfirmationNotice: Boolean
): Intent {
return Intent(context, MarkChaptersCompletedActivity::class.java).apply {
putExtra(PROFILE_ID_EXTRA_KEY, internalProfileId)
putExtra(SHOW_CONFIRMATION_NOTICE_EXTRA_KEY, showConfirmationNotice)
decorateWithScreenName(MARK_CHAPTERS_COMPLETED_ACTIVITY)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ class MarkChaptersCompletedActivityPresenter @Inject constructor(
private val activity: AppCompatActivity
) {

fun handleOnCreate(internalProfileId: Int) {
fun handleOnCreate(internalProfileId: Int, showConfirmationNotice: Boolean) {
activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back_white_24dp)
activity.setContentView(R.layout.mark_chapters_completed_activity)

if (getMarkChaptersCompletedFragment() == null) {
val markChaptersCompletedFragment = MarkChaptersCompletedFragment
.newInstance(internalProfileId)
val markChaptersCompletedFragment =
MarkChaptersCompletedFragment.newInstance(internalProfileId, showConfirmationNotice)
activity.supportFragmentManager.beginTransaction().add(
R.id.mark_chapters_completed_container,
markChaptersCompletedFragment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,23 @@ class MarkChaptersCompletedFragment : InjectableFragment() {
lateinit var markChaptersCompletedFragmentPresenter: MarkChaptersCompletedFragmentPresenter

companion object {
internal const val PROFILE_ID_ARGUMENT_KEY =
"MarkChaptersCompletedFragment.profile_id"

private const val PROFILE_ID_ARGUMENT_KEY = "MarkChaptersCompletedFragment.profile_id"
private const val SHOW_CONFIRMATION_NOTICE_ARGUMENT_KEY =
"MarkChaptersCompletedFragment.show_confirmation_notice"
private const val EXPLORATION_ID_LIST_ARGUMENT_KEY =
"MarkChaptersCompletedFragment.exploration_id_list"
private const val EXPLORATION_TITLE_LIST_ARGUMENT_KEY =
"MarkChaptersCompletedFragment.exploration_title_list"

/** Returns a new [MarkChaptersCompletedFragment]. */
fun newInstance(internalProfileId: Int): MarkChaptersCompletedFragment {
fun newInstance(
internalProfileId: Int,
showConfirmationNotice: Boolean
): MarkChaptersCompletedFragment {
val markChaptersCompletedFragment = MarkChaptersCompletedFragment()
val args = Bundle()
args.putInt(PROFILE_ID_ARGUMENT_KEY, internalProfileId)
args.putBoolean(SHOW_CONFIRMATION_NOTICE_ARGUMENT_KEY, showConfirmationNotice)
markChaptersCompletedFragment.arguments = args
return markChaptersCompletedFragment
}
Expand All @@ -43,26 +49,28 @@ class MarkChaptersCompletedFragment : InjectableFragment() {
): View? {
val args =
checkNotNull(arguments) { "Expected arguments to be passed to MarkChaptersCompletedFragment" }
val internalProfileId = args
.getInt(PROFILE_ID_ARGUMENT_KEY, -1)
var selectedExplorationIdList = ArrayList<String>()
if (savedInstanceState != null) {
selectedExplorationIdList =
savedInstanceState.getStringArrayList(EXPLORATION_ID_LIST_ARGUMENT_KEY)!!
}
val internalProfileId = args.getInt(PROFILE_ID_ARGUMENT_KEY, /* defaultValue= */ -1)
val showConfirmationNotice =
args.getBoolean(SHOW_CONFIRMATION_NOTICE_ARGUMENT_KEY, /* defaultValue= */ false)
return markChaptersCompletedFragmentPresenter.handleCreateView(
inflater,
container,
internalProfileId,
selectedExplorationIdList
showConfirmationNotice,
savedInstanceState?.getStringArrayList(EXPLORATION_ID_LIST_ARGUMENT_KEY) ?: listOf(),
savedInstanceState?.getStringArrayList(EXPLORATION_TITLE_LIST_ARGUMENT_KEY) ?: listOf()
)
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putStringArrayList(
EXPLORATION_ID_LIST_ARGUMENT_KEY,
markChaptersCompletedFragmentPresenter.selectedExplorationIdList
markChaptersCompletedFragmentPresenter.serializableSelectedExplorationIds
)
outState.putStringArrayList(
EXPLORATION_TITLE_LIST_ARGUMENT_KEY,
markChaptersCompletedFragmentPresenter.serializableSelectedExplorationTitles
)
}
}
Loading

0 comments on commit 0290ef8

Please sign in to comment.