Skip to content

Commit

Permalink
Fix #4452, #4453, #4454, #4445, #4446, #4447, #4448: Add spotlight fu…
Browse files Browse the repository at this point in the history
…nctionality (#4699)

<!-- READ ME FIRST: Please fill in the explanation section below and
check off every point from the Essential Checklist! -->
## Explanation
<!--
- Explain what your PR does. If this PR fixes an existing bug, please
include
- "Fixes #bugnum:" in the explanation so that GitHub can auto-close the
issue
  - when this PR is merged.
  -->
fixes #4452
fixes #4453
fixes #4454
fixes #4455
fixes #4456
fixes #4457
fixes #4458 
fixes #4459

This PR introduces the SpotlightFragment into the codebase, which is
essentially a powerful, robust API that can highlight or 'spotlight'
certain parts of the UI to create an onboarding experience for the same.
We introduce the [Spotlight
library](https://github.com/TakuSemba/Spotlight) into the codebase
handles the highlighting of certain elements on screen. The work done in
this PR augments the functionality of the library, by dynamically adding
arrows and hints which create an over-all Spotlight onboarding
experience to the app, beautifully surfacing some functionalities of the
app to a new user.

We also work on making sure that the spotlight is only shown to a user
only once - the protocol buffers are used to save which spotlight has
already been seen.

The purpose of this PR is to introduce an API that the future
contributors can use to seamlessly integrate spotlights onto UI elements
as and when needed, with minimal coding.

This PR also implements spotlights for the onboarding, home, topic and
exploration screens that are required at this time.

For accessibility, the content descriptions of all the elements on which
spotlights are required today are updated. If talkback is turned on, the
spotlights will not show up.

The entire spotlight functionality is also gated behind a feature flag.
This entire PR is backed by tests.

## Essential Checklist
<!-- Please tick the relevant boxes by putting an "x" in them. -->
- [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
<!-- Delete these section if this PR does not include UI-related
changes. -->
If your PR includes UI-related changes, then:
- Add screenshots for portrait/landscape for both a tablet & phone of
the before & after UI changes
- For the screenshots above, include both English and pseudo-localized
(RTL) screenshots (see [RTL
guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines))
- Add a video showing the full UX flow with a screen reader enabled (see
[accessibility
guide](https://github.com/oppia/oppia-android/wiki/Accessibility-(A11y)-Guide))
- Add a screenshot demonstrating that you ran affected Espresso tests
locally & that they're passing

Co-authored-by: Ben Henning <[email protected]>
Co-authored-by: madhurgera2 <[email protected]>
Co-authored-by: JishnuGoyal <[email protected]>
  • Loading branch information
4 people authored Nov 22, 2022
1 parent 3059cc7 commit 642a7bd
Show file tree
Hide file tree
Showing 76 changed files with 2,628 additions and 66 deletions.
7 changes: 7 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ git_repository(
shallow_since = "1647295507 -0700",
)

git_repository(
name = "android-spotlight",
commit = "ebde38335bfb56349eae57e705b611ead9addb15",
remote = "https://github.com/oppia/android-spotlight",
shallow_since = "1668824029 -0800",
)

# A custom fork of KotliTeX that removes resources artifacts that break the build, and updates the
# min target SDK version to be compatible with Oppia.
git_repository(
Expand Down
7 changes: 7 additions & 0 deletions app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,10 @@ VIEW_MODELS = [

# keep sorted
VIEWS_WITH_RESOURCE_IMPORTS = [
"src/main/java/org/oppia/android/app/customview/ChapterNotStartedContainerConstraintLayout.kt",
"src/main/java/org/oppia/android/app/customview/ContinueButtonView.kt",
"src/main/java/org/oppia/android/app/customview/LessonThumbnailImageView.kt",
"src/main/java/org/oppia/android/app/customview/PromotedStoryCardView.kt",
"src/main/java/org/oppia/android/app/customview/SegmentedCircularProgressView.kt",
"src/main/java/org/oppia/android/app/customview/VerticalDashedLineView.kt",
"src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt",
Expand Down Expand Up @@ -576,6 +578,7 @@ android_library(
":resources",
":view_models",
":views",
"//app/src/main/java/org/oppia/android/app/spotlight",
"//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider",
"//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler",
"//model/src/main/proto:interaction_object_java_proto_lite",
Expand Down Expand Up @@ -631,6 +634,7 @@ kt_android_library(
":snap_helper",
":view_models",
"//app/src/main/java/org/oppia/android/app/shim:view_binding_shim",
"//app/src/main/java/org/oppia/android/app/spotlight",
"//app/src/main/java/org/oppia/android/app/utility/lifecycle:lifecycle_safe_timer_factory",
"//app/src/main/java/org/oppia/android/app/view:view_component_factory",
"//app/src/main/java/org/oppia/android/app/view:view_scope",
Expand Down Expand Up @@ -746,6 +750,7 @@ kt_android_library(
"//app/src/main/java/org/oppia/android/app/fragment:FragmentComponentImpl.kt",
"//app/src/main/java/org/oppia/android/app/fragment:FragmentModule.kt",
"//app/src/main/java/org/oppia/android/app/view:ViewComponentBuilderModule.kt",
"//app/src/main/java/org/oppia/android/app/spotlight:SpotlightFragment_updated.kt",
],
custom_package = "org.oppia.android.app.ui",
enable_data_binding = 1,
Expand All @@ -769,6 +774,7 @@ kt_android_library(
"//domain/src/main/java/org/oppia/android/domain/onboarding:state_controller",
"//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener",
"//domain/src/main/java/org/oppia/android/domain/profile:profile_management_controller",
"//domain/src/main/java/org/oppia/android/domain/spotlight:spotlight_state_controller",
"//model/src/main/proto:arguments_java_proto_lite",
"//third_party:androidx_databinding_databinding-adapters",
"//third_party:androidx_databinding_databinding-common",
Expand All @@ -780,6 +786,7 @@ kt_android_library(
"//third_party:androidx_viewpager2_viewpager2",
"//third_party:androidx_viewpager_viewpager",
"//third_party:com_caverock_androidsvg",
"//third_party:com_github_takusemba_spotlight",
"//third_party:com_google_android_flexbox_flexbox",
"//third_party:javax_annotation_javax_annotation-api_jar",
"//utility",
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ dependencies {
'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',
'com.github.oppia:android-spotlight:ebde38335bfb56349eae57e705b611ead9addb15'
)
compileOnly(
'jakarta.xml.bind:jakarta.xml.bind-api:2.3.2',
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 @@ -36,6 +36,10 @@
android:name=".app.testing.AdministratorControlsFragmentTestActivity"
android:label="@string/administrator_controls_fragment_test_activity_label"
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity
android:name=".app.testing.SpotlightFragmentTestActivity"
android:label="@string/test_activity_label"
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity
android:name=".app.administratorcontrols.appversion.AppVersionActivity"
android:label="@string/app_version_activity_title"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import org.oppia.android.app.testing.PoliciesFragmentTestActivity
import org.oppia.android.app.testing.ProfileChooserFragmentTestActivity
import org.oppia.android.app.testing.ProfileEditFragmentTestActivity
import org.oppia.android.app.testing.SplashTestActivity
import org.oppia.android.app.testing.SpotlightFragmentTestActivity
import org.oppia.android.app.testing.StateAssemblerMarginBindingAdaptersTestActivity
import org.oppia.android.app.testing.StateAssemblerPaddingBindingAdaptersTestActivity
import org.oppia.android.app.testing.TestFontScaleConfigurationUtilActivity
Expand Down Expand Up @@ -184,6 +185,7 @@ interface ActivityComponentImpl :
StateAssemblerPaddingBindingAdaptersTestActivity
)

fun inject(spotlightFragmentTestActivity: SpotlightFragmentTestActivity)
fun inject(stateFragmentTestActivity: StateFragmentTestActivity)
fun inject(storyActivity: StoryActivity)
fun inject(testFontScaleConfigurationUtilActivity: TestFontScaleConfigurationUtilActivity)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.oppia.android.app.customview

import android.content.Context
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import org.oppia.android.R
import org.oppia.android.app.model.Spotlight
import org.oppia.android.app.spotlight.SpotlightManager
import org.oppia.android.app.spotlight.SpotlightTarget
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.app.view.ViewComponentFactory
import org.oppia.android.app.view.ViewComponentImpl
import javax.inject.Inject

/** Custom view that represents an incomplete chapter. */
class ChapterNotStartedContainerConstraintLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

private var index: Int = -1
private var isSpotlit = false

@Inject
lateinit var fragment: Fragment

@Inject
lateinit var resourceHandler: AppLanguageResourceHandler

/** Sets the index of the story of which this custom view is a part of. */
fun setStoryIndex(index: Int) {
// Only spotlight the first chapter of the "first" story. We know for sure that for a new user,
// the first chapter shall be a type of not started chapter view. The index tells which story
// are we on.
this.index = index
}

private fun getSpotlightFragment(): SpotlightManager? {
return fragment.requireActivity().supportFragmentManager.findFragmentByTag(
SpotlightManager.SPOTLIGHT_FRAGMENT_TAG
) as? SpotlightManager
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()

val viewComponentFactory =
FragmentManager.findFragment<Fragment>(this) as ViewComponentFactory
val viewComponent = viewComponentFactory.createViewComponent(this) as ViewComponentImpl
viewComponent.inject(this)

if (!isSpotlit) {
isSpotlit = true
val spotlightTarget = SpotlightTarget(
this,
resourceHandler.getStringInLocale(R.string.first_chapter_spotlight_hint),
feature = Spotlight.FeatureCase.FIRST_CHAPTER
)
if (index == 0) {
checkNotNull(getSpotlightFragment()).requestSpotlightViewWithDelayedLayout(spotlightTarget)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.oppia.android.app.customview

import android.content.Context
import android.util.AttributeSet
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.google.android.material.card.MaterialCardView
import org.oppia.android.R
import org.oppia.android.app.model.Spotlight
import org.oppia.android.app.spotlight.SpotlightManager
import org.oppia.android.app.spotlight.SpotlightTarget
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.app.view.ViewComponentFactory
import org.oppia.android.app.view.ViewComponentImpl
import javax.inject.Inject

/** [MaterialCardView] that represents stories promoted to the learner. */
class PromotedStoryCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : MaterialCardView(context, attrs, defStyleAttr) {

@Inject
lateinit var fragment: Fragment

@Inject
lateinit var resourceHandler: AppLanguageResourceHandler

private var isSpotlit = false

/** Sets the index at which this custom view is located inside the recycler view. */
fun setPromotedStoryIndex(index: Int) {
// This view can get attached multiple times and we must make sure that the spotlight is
// requested only once. Only spotlight the item at the first index of the recycler view.
if (!isSpotlit && index == 0) {
isSpotlit = true
val spotlightTarget = SpotlightTarget(
this,
resourceHandler.getStringInLocale(R.string.promoted_story_spotlight_hint),
feature = Spotlight.FeatureCase.PROMOTED_STORIES
)
checkNotNull(getSpotlightFragment()).requestSpotlightViewWithDelayedLayout(spotlightTarget)
}
}

private fun getSpotlightFragment(): SpotlightManager? {
return fragment.requireActivity().supportFragmentManager.findFragmentByTag(
SpotlightManager.SPOTLIGHT_FRAGMENT_TAG
) as? SpotlightManager
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
val viewComponentFactory =
FragmentManager.findFragment<Fragment>(this) as ViewComponentFactory
val viewComponent = viewComponentFactory.createViewComponent(this) as ViewComponentImpl
viewComponent.inject(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import org.oppia.android.app.settings.profile.ProfileRenameFragment
import org.oppia.android.app.settings.profile.ProfileResetPinFragment
import org.oppia.android.app.shim.IntentFactoryShimModule
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.spotlight.SpotlightFragment
import org.oppia.android.app.story.StoryFragment
import org.oppia.android.app.testing.DragDropTestFragment
import org.oppia.android.app.testing.ExplorationTestActivityPresenter
Expand Down Expand Up @@ -160,6 +161,7 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto
fun inject(resumeLessonFragment: ResumeLessonFragment)
fun inject(revealSolutionDialogFragment: RevealSolutionDialogFragment)
fun inject(revisionCardFragment: RevisionCardFragment)
fun inject(spotlightFragment: SpotlightFragment)
fun inject(stateFragment: StateFragment)
fun inject(stopExplorationDialogFragment: StopExplorationDialogFragment)
fun inject(storyFragment: StoryFragment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class HomeActivity :
super.onCreate(savedInstanceState)
(activityComponent as ActivityComponentImpl).inject(this)
internalProfileId = intent?.getIntExtra(NAVIGATION_PROFILE_ID_ARGUMENT_KEY, -1)!!
homeActivityPresenter.handleOnCreate()
homeActivityPresenter.handleOnCreate(internalProfileId)
title = resourceHandler.getStringInLocale(R.string.home_activity_title)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import androidx.drawerlayout.widget.DrawerLayout
import org.oppia.android.R
import org.oppia.android.app.activity.ActivityScope
import org.oppia.android.app.drawer.NavigationDrawerFragment
import org.oppia.android.app.spotlight.SpotlightFragment
import org.oppia.android.app.spotlight.SpotlightManager
import javax.inject.Inject

const val TAG_HOME_FRAGMENT = "HOME_FRAGMENT"
Expand All @@ -16,7 +18,7 @@ const val TAG_HOME_FRAGMENT = "HOME_FRAGMENT"
class HomeActivityPresenter @Inject constructor(private val activity: AppCompatActivity) {
private var navigationDrawerFragment: NavigationDrawerFragment? = null

fun handleOnCreate() {
fun handleOnCreate(internalProfileId: Int) {
activity.setContentView(R.layout.home_activity)
setUpNavigationDrawer()
if (getHomeFragment() == null) {
Expand All @@ -26,6 +28,14 @@ class HomeActivityPresenter @Inject constructor(private val activity: AppCompatA
TAG_HOME_FRAGMENT
).commitNow()
}

if (getSpotlightFragment() == null) {
activity.supportFragmentManager.beginTransaction().add(
R.id.home_spotlight_fragment_placeholder,
SpotlightFragment.newInstance(internalProfileId),
SpotlightManager.SPOTLIGHT_FRAGMENT_TAG
).commitNow()
}
}

fun handleOnRestart() {
Expand All @@ -50,4 +60,10 @@ class HomeActivityPresenter @Inject constructor(private val activity: AppCompatA
R.id.home_fragment_placeholder
) as HomeFragment?
}

private fun getSpotlightFragment(): SpotlightFragment? {
return activity.supportFragmentManager.findFragmentById(
R.id.home_spotlight_fragment_placeholder
) as? SpotlightFragment
}
}
5 changes: 3 additions & 2 deletions app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,15 @@ class HomeViewModel(
// completed story topic.
val sortedStoryList = storyList.sortedByDescending { !it.isTopicLearned }
return sortedStoryList.take(promotedStoryListLimit)
.map { promotedStory ->
.mapIndexed { index, promotedStory ->
PromotedStoryViewModel(
activity,
internalProfileId,
sortedStoryList.size,
storyEntityType,
promotedStory,
translationController
translationController,
index
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class PromotedStoryViewModel(
private val totalStoryCount: Int,
val entityType: String,
val promotedStory: PromotedStory,
translationController: TranslationController
translationController: TranslationController,
val index: Int
) : ObservableViewModel() {
val storyTitle by lazy {
translationController.extractString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package org.oppia.android.app.onboarding
import androidx.appcompat.app.AppCompatActivity
import org.oppia.android.R
import org.oppia.android.app.activity.ActivityScope
import org.oppia.android.app.spotlight.SpotlightFragment
import org.oppia.android.app.spotlight.SpotlightManager
import javax.inject.Inject

/** The presenter for [OnboardingActivity]. */
Expand All @@ -16,6 +18,14 @@ class OnboardingActivityPresenter @Inject constructor(private val activity: AppC
OnboardingFragment()
).commitNow()
}

if (getSpotlightFragment() == null) {
activity.supportFragmentManager.beginTransaction().add(
R.id.onboarding_spotlight_fragment_placeholder,
SpotlightFragment.newInstance(internalProfileId = 0),
SpotlightManager.SPOTLIGHT_FRAGMENT_TAG
).commitNow()
}
}

private fun getOnboardingFragment(): OnboardingFragment? {
Expand All @@ -25,4 +35,12 @@ class OnboardingActivityPresenter @Inject constructor(private val activity: AppC
R.id.onboarding_fragment_placeholder
) as OnboardingFragment?
}

private fun getSpotlightFragment(): SpotlightFragment? {
return activity
.supportFragmentManager
.findFragmentById(
R.id.onboarding_spotlight_fragment_placeholder
) as? SpotlightFragment
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@ class OnboardingFragment : InjectableFragment() {
): View? {
return onboardingFragmentPresenter.handleCreateView(inflater, container)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onboardingFragmentPresenter.startSpotlight()
}
}
Loading

0 comments on commit 642a7bd

Please sign in to comment.