From 8bfbbecad7bc1ca4d831edaae6f7f54dedbf7b17 Mon Sep 17 00:00:00 2001 From: Rajat Talesra Date: Tue, 5 Nov 2019 13:22:34 +0530 Subject: [PATCH] Fix part of #134: Home fragment low fi with TopicList (#289) * HomeFragment xml introduction * Promoted Story ViewModel * PromotedStoryViewModel * Basic promoted story * Promoted story data display * Promoted card display * GridLayoutManager * Functional HomeFragment * Click listeners * Click listeners final * Chapter name issue fixed * Fully functional with click listeners * Introduce ContinuePlayingActivity * Click listener integration * Test for ContinuePlayingActivity * HomeTest cases * Nit changes * Nit suggested changes * Code updated as per suggestions * Added HomeItemViewModel * Nit changes * Orientation Change * EOF * Test fix --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 1 + .../oppia/app/activity/ActivityComponent.kt | 4 +- .../databinding/DrawableBindingAdapters.kt | 15 ++ .../oppia/app/fragment/FragmentComponent.kt | 2 + .../oppia/app/home/ContinuePlayingActivity.kt | 25 +++ .../home/ContinuePlayingActivityPresenter.kt | 24 ++ .../oppia/app/home/ContinuePlayingFragment.kt | 23 ++ .../home/ContinuePlayingFragmentPresenter.kt | 16 ++ .../java/org/oppia/app/home/HomeActivity.kt | 10 +- .../oppia/app/home/HomeActivityPresenter.kt | 2 +- .../java/org/oppia/app/home/HomeFragment.kt | 8 +- .../oppia/app/home/HomeFragmentPresenter.kt | 103 ++++++++- .../org/oppia/app/home/HomeItemViewModel.kt | 6 + .../home/RouteToContinuePlayingListener.kt | 6 + .../oppia/app/home/RouteToTopicListener.kt | 6 + .../app/home/RouteToTopicPlayStoryListener.kt | 6 + .../oppia/app/home/UserAppHistoryViewModel.kt | 28 +-- .../home/topiclist/PromotedStoryViewModel.kt | 53 +++++ .../app/home/topiclist/TopicListAdapter.kt | 115 ++++++++++ .../topiclist/TopicSummaryClickListener.kt | 8 + .../home/topiclist/TopicSummaryViewModel.kt | 47 ++++ .../java/org/oppia/app/topic/TopicActivity.kt | 19 ++ .../res/drawable/rounded_rect_background.xml | 4 + .../res/layout/continue_playing_activity.xml | 7 + .../res/layout/continue_playing_fragment.xml | 19 ++ app/src/main/res/layout/home_activity.xml | 13 +- app/src/main/res/layout/home_fragment.xml | 41 ++-- .../main/res/layout/promoted_story_card.xml | 119 ++++++++++ .../main/res/layout/topic_summary_view.xml | 64 ++++++ app/src/main/res/layout/welcome.xml | 33 +++ app/src/main/res/values/strings.xml | 10 + .../app/home/ContinuePlayingActivityTest.kt | 24 ++ .../org/oppia/app/home/HomeActivityTest.kt | 206 +++++++++++++++++- .../app/utility/OrientationChangeAction.kt | 51 +++++ .../oppia/domain/topic/TopicListController.kt | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++++++++++++ gradlew.bat | 84 +++++++ model/src/main/proto/topic.proto | 9 +- 41 files changed, 1322 insertions(+), 70 deletions(-) create mode 100755 app/src/main/java/org/oppia/app/databinding/DrawableBindingAdapters.kt create mode 100644 app/src/main/java/org/oppia/app/home/ContinuePlayingActivity.kt create mode 100644 app/src/main/java/org/oppia/app/home/ContinuePlayingActivityPresenter.kt create mode 100644 app/src/main/java/org/oppia/app/home/ContinuePlayingFragment.kt create mode 100755 app/src/main/java/org/oppia/app/home/ContinuePlayingFragmentPresenter.kt create mode 100644 app/src/main/java/org/oppia/app/home/HomeItemViewModel.kt create mode 100755 app/src/main/java/org/oppia/app/home/RouteToContinuePlayingListener.kt create mode 100755 app/src/main/java/org/oppia/app/home/RouteToTopicListener.kt create mode 100755 app/src/main/java/org/oppia/app/home/RouteToTopicPlayStoryListener.kt create mode 100755 app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryViewModel.kt create mode 100644 app/src/main/java/org/oppia/app/home/topiclist/TopicListAdapter.kt create mode 100755 app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryClickListener.kt create mode 100755 app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryViewModel.kt create mode 100755 app/src/main/res/drawable/rounded_rect_background.xml create mode 100644 app/src/main/res/layout/continue_playing_activity.xml create mode 100644 app/src/main/res/layout/continue_playing_fragment.xml create mode 100755 app/src/main/res/layout/promoted_story_card.xml create mode 100755 app/src/main/res/layout/topic_summary_view.xml create mode 100644 app/src/main/res/layout/welcome.xml create mode 100644 app/src/sharedTest/java/org/oppia/app/home/ContinuePlayingActivityTest.kt create mode 100644 app/src/sharedTest/java/org/oppia/app/utility/OrientationChangeAction.kt create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat diff --git a/app/build.gradle b/app/build.gradle index 70eaf0c8056..0205e9b8dd0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,6 +73,7 @@ dependencies { ) testImplementation( 'androidx.test:core:1.2.0', + 'androidx.test.espresso:espresso-contrib:3.1.0', 'androidx.test.espresso:espresso-core:3.2.0', 'androidx.test.espresso:espresso-intents:3.1.0', 'androidx.test.ext:junit:1.1.1', @@ -82,6 +83,7 @@ dependencies { ) androidTestImplementation( 'androidx.test:core:1.2.0', + 'androidx.test.espresso:espresso-contrib:3.1.0', 'androidx.test.espresso:espresso-core:3.2.0', 'androidx.test.espresso:espresso-intents:3.1.0', 'androidx.test.ext:junit:1.1.1', diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5128677c47d..63011e02931 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/OppiaTheme"> + diff --git a/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt b/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt index d12e457a66d..d0b75f4ebf9 100644 --- a/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt +++ b/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt @@ -4,6 +4,7 @@ import androidx.appcompat.app.AppCompatActivity import dagger.BindsInstance import dagger.Subcomponent import org.oppia.app.fragment.FragmentComponent +import org.oppia.app.home.ContinuePlayingActivity import org.oppia.app.home.HomeActivity import org.oppia.app.player.audio.testing.AudioFragmentTestActivity import org.oppia.app.player.exploration.ExplorationActivity @@ -34,9 +35,10 @@ interface ActivityComponent { fun inject(bindableAdapterTestActivity: BindableAdapterTestActivity) fun inject(conceptCardFragmentTestActivity: ConceptCardFragmentTestActivity) fun inject(contentCardTestActivity: ContentCardTestActivity) + fun inject(continuePlayingActivity: ContinuePlayingActivity) fun inject(explorationActivity: ExplorationActivity) fun inject(homeActivity: HomeActivity) - fun inject(htmlParserTestActivty: HtmlParserTestActivity) + fun inject(htmlParserTestActivity: HtmlParserTestActivity) fun inject(profileActivity: ProfileActivity) fun inject(questionPlayerActivity: QuestionPlayerActivity) fun inject(stateFragmentTestActivity: StateFragmentTestActivity) diff --git a/app/src/main/java/org/oppia/app/databinding/DrawableBindingAdapters.kt b/app/src/main/java/org/oppia/app/databinding/DrawableBindingAdapters.kt new file mode 100755 index 00000000000..35ab977a846 --- /dev/null +++ b/app/src/main/java/org/oppia/app/databinding/DrawableBindingAdapters.kt @@ -0,0 +1,15 @@ +package org.oppia.app.databinding + +import android.graphics.drawable.GradientDrawable +import android.view.View +import androidx.annotation.ColorInt +import androidx.databinding.BindingAdapter +import org.oppia.app.R + +/** Used to set a rounded-rect background drawable with a data-bound color. */ +@BindingAdapter("app:roundedRectDrawableWithColor") +fun setBackgroundDrawable(view: View, @ColorInt colorRgb: Int) { + view.setBackgroundResource(R.drawable.rounded_rect_background) + // The input color needs to have alpha channel prepended to it. + (view.background as GradientDrawable).setColor((0xff000000 or colorRgb.toLong()).toInt()) +} diff --git a/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt b/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt index 885957b3028..3b8830f0548 100644 --- a/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt +++ b/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt @@ -3,6 +3,7 @@ package org.oppia.app.fragment import androidx.fragment.app.Fragment import dagger.BindsInstance import dagger.Subcomponent +import org.oppia.app.home.ContinuePlayingFragment import org.oppia.app.home.HomeFragment import org.oppia.app.player.exploration.ExplorationFragment import org.oppia.app.player.state.StateFragment @@ -37,6 +38,7 @@ interface FragmentComponent { fun inject(audioFragment: AudioFragment) fun inject(bindableAdapterTestFragment: BindableAdapterTestFragment) fun inject(conceptCardFragment: ConceptCardFragment) + fun inject(continuePlayingFragment: ContinuePlayingFragment) fun inject(explorationFragment: ExplorationFragment) fun inject(homeFragment: HomeFragment) fun inject(profileChooserFragment: ProfileChooserFragment) diff --git a/app/src/main/java/org/oppia/app/home/ContinuePlayingActivity.kt b/app/src/main/java/org/oppia/app/home/ContinuePlayingActivity.kt new file mode 100644 index 00000000000..a5c6121c0a8 --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/ContinuePlayingActivity.kt @@ -0,0 +1,25 @@ +package org.oppia.app.home + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.app.activity.InjectableAppCompatActivity +import javax.inject.Inject + +/** Activity for recent stories. */ +class ContinuePlayingActivity : InjectableAppCompatActivity() { + @Inject lateinit var continuePlayingActivityPresenter: ContinuePlayingActivityPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activityComponent.inject(this) + continuePlayingActivityPresenter.handleOnCreate() + } + + companion object { + /** Returns a new [Intent] to route to [ContinuePlayingActivity]. */ + fun createContinuePlayingActivityIntent(context: Context): Intent { + return Intent(context, ContinuePlayingActivity::class.java) + } + } +} diff --git a/app/src/main/java/org/oppia/app/home/ContinuePlayingActivityPresenter.kt b/app/src/main/java/org/oppia/app/home/ContinuePlayingActivityPresenter.kt new file mode 100644 index 00000000000..e4341a8d1bd --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/ContinuePlayingActivityPresenter.kt @@ -0,0 +1,24 @@ +package org.oppia.app.home + +import androidx.appcompat.app.AppCompatActivity +import org.oppia.app.R +import org.oppia.app.activity.ActivityScope +import javax.inject.Inject + +/** The presenter for [ContinuePlayingActivity]. */ +@ActivityScope +class ContinuePlayingActivityPresenter @Inject constructor(private val activity: AppCompatActivity) { + fun handleOnCreate() { + activity.setContentView(R.layout.continue_playing_activity) + if (getContinuePlayingFragment() == null) { + activity.supportFragmentManager.beginTransaction().add( + R.id.continue_playing_fragment_placeholder, + ContinuePlayingFragment() + ).commitNow() + } + } + + private fun getContinuePlayingFragment(): ContinuePlayingFragment? { + return activity.supportFragmentManager.findFragmentById(R.id.continue_playing_fragment_placeholder) as ContinuePlayingFragment? + } +} diff --git a/app/src/main/java/org/oppia/app/home/ContinuePlayingFragment.kt b/app/src/main/java/org/oppia/app/home/ContinuePlayingFragment.kt new file mode 100644 index 00000000000..34feca31c9a --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/ContinuePlayingFragment.kt @@ -0,0 +1,23 @@ +package org.oppia.app.home + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.app.fragment.InjectableFragment +import javax.inject.Inject + +/** Fragment that contains all recently played stories. */ +class ContinuePlayingFragment : InjectableFragment() { + @Inject lateinit var continuePlayingFragmentPresenter: ContinuePlayingFragmentPresenter + + override fun onAttach(context: Context?) { + super.onAttach(context) + fragmentComponent.inject(this) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return continuePlayingFragmentPresenter.handleCreateView(inflater, container) + } +} diff --git a/app/src/main/java/org/oppia/app/home/ContinuePlayingFragmentPresenter.kt b/app/src/main/java/org/oppia/app/home/ContinuePlayingFragmentPresenter.kt new file mode 100755 index 00000000000..15d857d75f9 --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/ContinuePlayingFragmentPresenter.kt @@ -0,0 +1,16 @@ +package org.oppia.app.home + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.app.databinding.ContinuePlayingFragmentBinding +import org.oppia.app.fragment.FragmentScope +import javax.inject.Inject + +/** The presenter for [ContinuePlayingFragment]. */ +@FragmentScope +class ContinuePlayingFragmentPresenter @Inject constructor() { + fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { + return ContinuePlayingFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false).root + } +} diff --git a/app/src/main/java/org/oppia/app/home/HomeActivity.kt b/app/src/main/java/org/oppia/app/home/HomeActivity.kt index 149eaf1e3c3..a953579b9cd 100644 --- a/app/src/main/java/org/oppia/app/home/HomeActivity.kt +++ b/app/src/main/java/org/oppia/app/home/HomeActivity.kt @@ -3,11 +3,13 @@ package org.oppia.app.home import android.os.Bundle import org.oppia.app.activity.InjectableAppCompatActivity import org.oppia.app.player.exploration.ExplorationActivity +import org.oppia.app.topic.TopicActivity import javax.inject.Inject /** The central activity for all users entering the app. */ -class HomeActivity : InjectableAppCompatActivity(), RouteToExplorationListener { - @Inject lateinit var homeActivityPresenter: HomeActivityPresenter +class HomeActivity : InjectableAppCompatActivity(), RouteToTopicListener, RouteToExplorationListener { + @Inject + lateinit var homeActivityPresenter: HomeActivityPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -15,6 +17,10 @@ class HomeActivity : InjectableAppCompatActivity(), RouteToExplorationListener { homeActivityPresenter.handleOnCreate() } + override fun routeToTopic(topicId: String) { + startActivity(TopicActivity.createTopicActivityIntent(this, topicId)) + } + override fun routeToExploration(explorationId: String) { startActivity(ExplorationActivity.createExplorationActivityIntent(this, explorationId)) } diff --git a/app/src/main/java/org/oppia/app/home/HomeActivityPresenter.kt b/app/src/main/java/org/oppia/app/home/HomeActivityPresenter.kt index 570d2bbd56c..55737aff039 100644 --- a/app/src/main/java/org/oppia/app/home/HomeActivityPresenter.kt +++ b/app/src/main/java/org/oppia/app/home/HomeActivityPresenter.kt @@ -5,7 +5,7 @@ import org.oppia.app.R import org.oppia.app.activity.ActivityScope import javax.inject.Inject -/** The controller for [HomeActivity]. */ +/** The presenter for [HomeActivity]. */ @ActivityScope class HomeActivityPresenter @Inject constructor(private val activity: AppCompatActivity) { fun handleOnCreate() { diff --git a/app/src/main/java/org/oppia/app/home/HomeFragment.kt b/app/src/main/java/org/oppia/app/home/HomeFragment.kt index 6cb984812c7..d22e1524374 100644 --- a/app/src/main/java/org/oppia/app/home/HomeFragment.kt +++ b/app/src/main/java/org/oppia/app/home/HomeFragment.kt @@ -6,10 +6,12 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import org.oppia.app.fragment.InjectableFragment +import org.oppia.app.home.topiclist.TopicSummaryClickListener +import org.oppia.app.model.TopicSummary import javax.inject.Inject /** Fragment that contains an introduction to the app. */ -class HomeFragment : InjectableFragment() { +class HomeFragment : InjectableFragment(), TopicSummaryClickListener { @Inject lateinit var homeFragmentPresenter: HomeFragmentPresenter override fun onAttach(context: Context?) { @@ -20,4 +22,8 @@ class HomeFragment : InjectableFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return homeFragmentPresenter.handleCreateView(inflater, container) } + + override fun onTopicSummaryClicked(topicSummary: TopicSummary) { + homeFragmentPresenter.onTopicSummaryClicked(topicSummary) + } } diff --git a/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt b/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt index cac6ffe695a..d35682f0df6 100644 --- a/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt @@ -5,49 +5,85 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment +import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import androidx.lifecycle.Transformations +import androidx.recyclerview.widget.GridLayoutManager import org.oppia.app.databinding.HomeFragmentBinding import org.oppia.app.fragment.FragmentScope +import org.oppia.app.home.topiclist.PromotedStoryViewModel +import org.oppia.app.home.topiclist.TopicListAdapter +import org.oppia.app.home.topiclist.TopicSummaryClickListener +import org.oppia.app.home.topiclist.TopicSummaryViewModel +import org.oppia.app.model.TopicList +import org.oppia.app.model.TopicSummary +import org.oppia.app.model.UserAppHistory import org.oppia.app.viewmodel.ViewModelProvider import org.oppia.domain.UserAppHistoryController import org.oppia.domain.exploration.ExplorationDataController import org.oppia.domain.exploration.TEST_EXPLORATION_ID_5 +import org.oppia.domain.topic.TopicListController import org.oppia.util.data.AsyncResult import org.oppia.util.logging.Logger import javax.inject.Inject private const val EXPLORATION_ID = TEST_EXPLORATION_ID_5 -/** The controller for [HomeFragment]. */ +/** The presenter for [HomeFragment]. */ @FragmentScope class HomeFragmentPresenter @Inject constructor( - activity: AppCompatActivity, + private val activity: AppCompatActivity, private val fragment: Fragment, private val viewModelProvider: ViewModelProvider, private val userAppHistoryController: UserAppHistoryController, + private val topicListController: TopicListController, private val explorationDataController: ExplorationDataController, private val logger: Logger ) { private val routeToExplorationListener = activity as RouteToExplorationListener + private val routeToTopicListener = activity as RouteToTopicListener + + private val itemList: MutableList = ArrayList() + + private lateinit var topicListAdapter: TopicListAdapter + + private lateinit var binding: HomeFragmentBinding fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { - val binding = HomeFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) + binding = HomeFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) // NB: Both the view model and lifecycle owner must be set in order to correctly bind LiveData elements to // data-bound view models. + + topicListAdapter = TopicListAdapter(itemList) + + val homeLayoutManager = GridLayoutManager(activity.applicationContext, 2) + homeLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return if (position == 0 || position == 1) { + /* number of spaces this item should occupy = */ 2 + } else { + /* number of spaces this item should occupy = */ 1 + } + } + } + + binding.homeRecyclerView.apply { + adapter = topicListAdapter + // https://stackoverflow.com/a/32763434/32763621 + layoutManager = homeLayoutManager + } binding.let { - it.viewModel = getUserAppHistoryViewModel() it.presenter = this it.lifecycleOwner = fragment } userAppHistoryController.markUserOpenedApp() - return binding.root - } + subscribeToUserAppHistory() + subscribeToTopicList() - private fun getUserAppHistoryViewModel(): UserAppHistoryViewModel { - return viewModelProvider.getForFragment(fragment, UserAppHistoryViewModel::class.java) + return binding.root } fun playExplorationButton(v: View) { @@ -64,4 +100,55 @@ class HomeFragmentPresenter @Inject constructor( } }) } + + private fun getUserAppHistoryViewModel(): UserAppHistoryViewModel { + return viewModelProvider.getForFragment(fragment, UserAppHistoryViewModel::class.java) + } + + private val topicListSummaryResultLiveData: LiveData> by lazy { + topicListController.getTopicList() + } + + private fun subscribeToTopicList() { + getAssumedSuccessfulTopicList().observe(fragment, Observer { result -> + + val promotedStoryViewModel = PromotedStoryViewModel(activity) + promotedStoryViewModel.setPromotedStory(result.promotedStory) + itemList.add(promotedStoryViewModel) + for (topicSummary in result.topicSummaryList) { + val topicSummaryViewModel = TopicSummaryViewModel(topicSummary, fragment as TopicSummaryClickListener) + itemList.add(topicSummaryViewModel) + } + topicListAdapter.notifyDataSetChanged() + }) + } + + private fun getAssumedSuccessfulTopicList(): LiveData { + // If there's an error loading the data, assume the default. + return Transformations.map(topicListSummaryResultLiveData) { it.getOrDefault(TopicList.getDefaultInstance()) } + } + + private fun subscribeToUserAppHistory() { + getUserAppHistory().observe(fragment, Observer { result -> + getUserAppHistoryViewModel().setAlreadyAppOpened(result.alreadyOpenedApp) + itemList.add(0, getUserAppHistoryViewModel()) + topicListAdapter.notifyDataSetChanged() + }) + } + + private fun getUserAppHistory(): LiveData { + // If there's an error loading the data, assume the default. + return Transformations.map(userAppHistoryController.getUserAppHistory(), ::processUserAppHistoryResult) + } + + private fun processUserAppHistoryResult(appHistoryResult: AsyncResult): UserAppHistory { + if (appHistoryResult.isFailure()) { + logger.e("HomeFragment", "Failed to retrieve user app history" + appHistoryResult.getErrorOrNull()) + } + return appHistoryResult.getOrDefault(UserAppHistory.getDefaultInstance()) + } + + fun onTopicSummaryClicked(topicSummary: TopicSummary) { + routeToTopicListener.routeToTopic(topicSummary.topicId) + } } diff --git a/app/src/main/java/org/oppia/app/home/HomeItemViewModel.kt b/app/src/main/java/org/oppia/app/home/HomeItemViewModel.kt new file mode 100644 index 00000000000..efa5f2938fe --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/HomeItemViewModel.kt @@ -0,0 +1,6 @@ +package org.oppia.app.home + +import org.oppia.app.viewmodel.ObservableViewModel + +/** The root [ViewModel] for all individual items that may be displayed in home fragment recycler view. */ +abstract class HomeItemViewModel: ObservableViewModel() diff --git a/app/src/main/java/org/oppia/app/home/RouteToContinuePlayingListener.kt b/app/src/main/java/org/oppia/app/home/RouteToContinuePlayingListener.kt new file mode 100755 index 00000000000..850e04c86a6 --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/RouteToContinuePlayingListener.kt @@ -0,0 +1,6 @@ +package org.oppia.app.home + +/** Listener for when an activity should route to [ContinuePlayingActivity]. */ +interface RouteToContinuePlayingListener { + fun routeToContinuePlaying() +} diff --git a/app/src/main/java/org/oppia/app/home/RouteToTopicListener.kt b/app/src/main/java/org/oppia/app/home/RouteToTopicListener.kt new file mode 100755 index 00000000000..f82d1288e36 --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/RouteToTopicListener.kt @@ -0,0 +1,6 @@ +package org.oppia.app.home + +/** Listener for when an activity should route to a topic. */ +interface RouteToTopicListener { + fun routeToTopic(topicId: String) +} diff --git a/app/src/main/java/org/oppia/app/home/RouteToTopicPlayStoryListener.kt b/app/src/main/java/org/oppia/app/home/RouteToTopicPlayStoryListener.kt new file mode 100755 index 00000000000..9b34c7a3916 --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/RouteToTopicPlayStoryListener.kt @@ -0,0 +1,6 @@ +package org.oppia.app.home + +/** Listener for when an activity should route to a story-item in TopicPlay tab. */ +interface RouteToTopicPlayStoryListener { + fun routeToTopicPlayStory(topicId: String, storyId: String) +} diff --git a/app/src/main/java/org/oppia/app/home/UserAppHistoryViewModel.kt b/app/src/main/java/org/oppia/app/home/UserAppHistoryViewModel.kt index b7b5f2c2bb5..1966a8f4b5e 100644 --- a/app/src/main/java/org/oppia/app/home/UserAppHistoryViewModel.kt +++ b/app/src/main/java/org/oppia/app/home/UserAppHistoryViewModel.kt @@ -1,34 +1,16 @@ package org.oppia.app.home -import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations +import androidx.databinding.ObservableField import androidx.lifecycle.ViewModel import org.oppia.app.fragment.FragmentScope -import org.oppia.app.model.UserAppHistory -import org.oppia.util.logging.Logger -import org.oppia.domain.UserAppHistoryController -import org.oppia.util.data.AsyncResult import javax.inject.Inject /** [ViewModel] for user app usage history. */ @FragmentScope -class UserAppHistoryViewModel @Inject constructor( - private val userAppHistoryController: UserAppHistoryController, - private val logger: Logger -): ViewModel() { - val userAppHistoryLiveData: LiveData? by lazy { - getUserAppHistory() - } - - private fun getUserAppHistory(): LiveData? { - // If there's an error loading the data, assume the default. - return Transformations.map(userAppHistoryController.getUserAppHistory(), ::processUserAppHistoryResult) - } +class UserAppHistoryViewModel @Inject constructor() : HomeItemViewModel() { + var isAppAlreadyOpened = ObservableField(false) - private fun processUserAppHistoryResult(appHistoryResult: AsyncResult): UserAppHistory { - if (appHistoryResult.isFailure()) { - logger.e("HomeFragment", "Failed to retrieve user app history"+ appHistoryResult.getErrorOrNull()) - } - return appHistoryResult.getOrDefault(UserAppHistory.getDefaultInstance()) + fun setAlreadyAppOpened(alreadyOpenedApp: Boolean) { + isAppAlreadyOpened.set(alreadyOpenedApp) } } diff --git a/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryViewModel.kt b/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryViewModel.kt new file mode 100755 index 00000000000..eeac0ab3692 --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryViewModel.kt @@ -0,0 +1,53 @@ +package org.oppia.app.home.topiclist + +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.ObservableField +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import org.oppia.app.home.ContinuePlayingActivity +import org.oppia.app.home.HomeItemViewModel +import org.oppia.app.home.RouteToContinuePlayingListener +import org.oppia.app.home.RouteToTopicPlayStoryListener +import org.oppia.app.model.PromotedStory +import org.oppia.app.topic.TopicActivity + +// TODO(#283): Add download status information to promoted-story-card. + +/** [ViewModel] for displaying a promoted story. */ +class PromotedStoryViewModel(private val activity: AppCompatActivity) : HomeItemViewModel(), + RouteToContinuePlayingListener, RouteToTopicPlayStoryListener { + + /** + * The retrieved [LiveData] for retrieving topic summaries. This model should ensure only one + * [LiveData] is used for all subsequent processed data to ensure the transformed [LiveData]s are + * always in sync. + */ + val promotedStoryObservable = ObservableField() + + fun setPromotedStory(promotedStory: PromotedStory) { + promotedStoryObservable.set(promotedStory) + } + + fun clickOnStoryTile(@Suppress("UNUSED_PARAMETER") v: View) { + routeToTopicPlayStory(promotedStoryObservable.get()!!.topicId, promotedStoryObservable.get()!!.storyId) + } + + fun clickOnViewAll(@Suppress("UNUSED_PARAMETER") v: View) { + routeToContinuePlaying() + } + + override fun routeToTopicPlayStory(topicId: String, storyId: String) { + activity.startActivity( + TopicActivity.createTopicPlayStoryActivityIntent( + activity.applicationContext, + topicId, + storyId + ) + ) + } + + override fun routeToContinuePlaying() { + activity.startActivity(ContinuePlayingActivity.createContinuePlayingActivityIntent(activity.applicationContext)) + } +} diff --git a/app/src/main/java/org/oppia/app/home/topiclist/TopicListAdapter.kt b/app/src/main/java/org/oppia/app/home/topiclist/TopicListAdapter.kt new file mode 100644 index 00000000000..48b8dee8db5 --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/topiclist/TopicListAdapter.kt @@ -0,0 +1,115 @@ +package org.oppia.app.home.topiclist + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.oppia.app.databinding.PromotedStoryCardBinding +import org.oppia.app.databinding.TopicSummaryViewBinding +import org.oppia.app.databinding.WelcomeBinding +import org.oppia.app.home.HomeItemViewModel +import org.oppia.app.home.UserAppHistoryViewModel + +private const val VIEW_TYPE_WELCOME_MESSAGE = 1 +private const val VIEW_TYPE_PROMOTED_STORY = 2 +private const val VIEW_TYPE_TOPIC_LIST = 3 + +/** Adapter to inflate different items/views inside [RecyclerView]. The itemList consists of various ViewModels. */ +class TopicListAdapter( + private val itemList: MutableList +) : + RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + // TODO(#216): Generalize this binding to make adding future items easier. + VIEW_TYPE_WELCOME_MESSAGE -> { + val inflater = LayoutInflater.from(parent.context) + val binding = + WelcomeBinding.inflate( + inflater, + parent, + /* attachToParent= */ false + ) + WelcomeViewHolder(binding) + } + VIEW_TYPE_PROMOTED_STORY -> { + val inflater = LayoutInflater.from(parent.context) + val binding = + PromotedStoryCardBinding.inflate( + inflater, + parent, + /* attachToParent= */ false + ) + PromotedStoryViewHolder(binding) + } + VIEW_TYPE_TOPIC_LIST -> { + val inflater = LayoutInflater.from(parent.context) + val binding = + TopicSummaryViewBinding.inflate( + inflater, + parent, + /* attachToParent= */ false + ) + TopicListViewHolder(binding) + } + else -> throw IllegalArgumentException("Invalid view type: $viewType") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder.itemViewType) { + VIEW_TYPE_WELCOME_MESSAGE -> { + (holder as WelcomeViewHolder).bind(itemList[position] as UserAppHistoryViewModel) + } + VIEW_TYPE_PROMOTED_STORY -> { + (holder as PromotedStoryViewHolder).bind(itemList[position] as PromotedStoryViewModel) + } + VIEW_TYPE_TOPIC_LIST -> { + (holder as TopicListViewHolder).bind(itemList[position] as TopicSummaryViewModel) + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (itemList[position]) { + is UserAppHistoryViewModel -> { + VIEW_TYPE_WELCOME_MESSAGE + } + is PromotedStoryViewModel -> { + VIEW_TYPE_PROMOTED_STORY + } + is TopicSummaryViewModel -> { + VIEW_TYPE_TOPIC_LIST + } + else -> throw IllegalArgumentException("Invalid type of data $position with item ${itemList[position]}") + } + } + + override fun getItemCount(): Int { + return itemList.size + } + + private class WelcomeViewHolder( + val binding: WelcomeBinding + ) : RecyclerView.ViewHolder(binding.root) { + internal fun bind(userAppHistoryViewModel: UserAppHistoryViewModel) { + binding.viewModel = userAppHistoryViewModel + } + } + + private class PromotedStoryViewHolder( + val binding: PromotedStoryCardBinding + ) : RecyclerView.ViewHolder(binding.root) { + internal fun bind(promotedStoryViewModel: PromotedStoryViewModel) { + binding.viewModel = promotedStoryViewModel + } + } + + private class TopicListViewHolder( + val binding: TopicSummaryViewBinding + ) : RecyclerView.ViewHolder(binding.root) { + internal fun bind(topicSummaryViewModel: TopicSummaryViewModel) { + binding.viewModel = topicSummaryViewModel + } + } +} diff --git a/app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryClickListener.kt b/app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryClickListener.kt new file mode 100755 index 00000000000..eacd39bb236 --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryClickListener.kt @@ -0,0 +1,8 @@ +package org.oppia.app.home.topiclist + +import org.oppia.app.model.TopicSummary + +/** Listener interface for when topic summaries are clicked in the UI. */ +interface TopicSummaryClickListener { + fun onTopicSummaryClicked(topicSummary: TopicSummary) +} diff --git a/app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryViewModel.kt b/app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryViewModel.kt new file mode 100755 index 00000000000..a0be5207e55 --- /dev/null +++ b/app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryViewModel.kt @@ -0,0 +1,47 @@ +package org.oppia.app.home.topiclist + +import android.graphics.Color +import android.view.View +import androidx.annotation.ColorInt +import androidx.lifecycle.ViewModel +import org.oppia.app.home.HomeItemViewModel +import org.oppia.app.model.TopicSummary + +// TODO(#206): Remove the color darkening computation and properly set up the topic thumbnails. +// These values were roughly computed based on the mocks. They won't produce the same colors since darker colors in the +// mocks were not consistently darker. An alternative would be to specify both background colors together to ensure +// proper contrast with readable elements. +const val DARKEN_VALUE_MULTIPLIER: Float = 0.9f +const val DARKEN_SATURATION_MULTIPLIER: Float = 1.2f + +/** The view model corresponding to topic summaries in the topic summary RecyclerView. */ +class TopicSummaryViewModel( + val topicSummary: TopicSummary, + private val topicSummaryClickListener: TopicSummaryClickListener +) : HomeItemViewModel() { + val name: String = topicSummary.name + val canonicalStoryCount: Int = topicSummary.canonicalStoryCount + @ColorInt + val backgroundColor: Int = retrieveBackgroundColor() + @ColorInt + val darkerBackgroundOverlayColor: Int = computeDarkerBackgroundColor() + + /** Callback from data-binding for when the summary tile is clicked. */ + fun clickOnSummaryTile(@Suppress("UNUSED_PARAMETER") v: View) { + topicSummaryClickListener.onTopicSummaryClicked(topicSummary) + } + + @ColorInt + private fun retrieveBackgroundColor(): Int { + return topicSummary.topicThumbnail.backgroundColorRgb + } + + /** Returns a version of [backgroundColor] that is slightly darker. */ + private fun computeDarkerBackgroundColor(): Int { + val hsv = floatArrayOf(0f, 0f, 0f) + Color.colorToHSV(backgroundColor, hsv) + hsv[1] = (hsv[1] * DARKEN_SATURATION_MULTIPLIER).coerceIn(0f, 1f) + hsv[2] = (hsv[2] * DARKEN_VALUE_MULTIPLIER).coerceIn(0f, 1f) + return Color.HSVToColor(hsv) + } +} diff --git a/app/src/main/java/org/oppia/app/topic/TopicActivity.kt b/app/src/main/java/org/oppia/app/topic/TopicActivity.kt index 842da2b6716..b876464ea76 100644 --- a/app/src/main/java/org/oppia/app/topic/TopicActivity.kt +++ b/app/src/main/java/org/oppia/app/topic/TopicActivity.kt @@ -1,5 +1,7 @@ package org.oppia.app.topic +import android.content.Context +import android.content.Intent import android.os.Bundle import org.oppia.app.activity.InjectableAppCompatActivity import org.oppia.app.home.RouteToExplorationListener @@ -51,5 +53,22 @@ class TopicActivity : InjectableAppCompatActivity(), RouteToQuestionPlayerListen companion object { internal const val TAG_CONCEPT_CARD_DIALOG = "CONCEPT_CARD_DIALOG" + internal const val TOPIC_ACTIVITY_TOPIC_ID_ARGUMENT_KEY = "TopicActivity.topic_id" + internal const val TOPIC_ACTIVITY_STORY_ID_ARGUMENT_KEY = "TopicActivity.story_id" + + /** Returns a new [Intent] to route to [TopicActivity] for a specified topic ID. */ + fun createTopicActivityIntent(context: Context, topicId: String): Intent { + val intent = Intent(context, TopicActivity::class.java) + intent.putExtra(TOPIC_ACTIVITY_TOPIC_ID_ARGUMENT_KEY, topicId) + return intent + } + + /** Returns a new [Intent] to route to [TopicPlayFragment] for a specified story ID. */ + fun createTopicPlayStoryActivityIntent(context: Context, topicId: String, storyId: String): Intent { + val intent = Intent(context, TopicActivity::class.java) + intent.putExtra(TOPIC_ACTIVITY_TOPIC_ID_ARGUMENT_KEY, topicId) + intent.putExtra(TOPIC_ACTIVITY_STORY_ID_ARGUMENT_KEY, storyId) + return intent + } } } diff --git a/app/src/main/res/drawable/rounded_rect_background.xml b/app/src/main/res/drawable/rounded_rect_background.xml new file mode 100755 index 00000000000..ae66617ecee --- /dev/null +++ b/app/src/main/res/drawable/rounded_rect_background.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/layout/continue_playing_activity.xml b/app/src/main/res/layout/continue_playing_activity.xml new file mode 100644 index 00000000000..1a65fbdfa5e --- /dev/null +++ b/app/src/main/res/layout/continue_playing_activity.xml @@ -0,0 +1,7 @@ + + diff --git a/app/src/main/res/layout/continue_playing_fragment.xml b/app/src/main/res/layout/continue_playing_fragment.xml new file mode 100644 index 00000000000..ef86878bd8b --- /dev/null +++ b/app/src/main/res/layout/continue_playing_fragment.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/app/src/main/res/layout/home_activity.xml b/app/src/main/res/layout/home_activity.xml index 7b1a54c0c3d..dccef140fbf 100644 --- a/app/src/main/res/layout/home_activity.xml +++ b/app/src/main/res/layout/home_activity.xml @@ -1,8 +1,7 @@ - + diff --git a/app/src/main/res/layout/home_fragment.xml b/app/src/main/res/layout/home_fragment.xml index a7d2e051f24..5dbff54192c 100644 --- a/app/src/main/res/layout/home_fragment.xml +++ b/app/src/main/res/layout/home_fragment.xml @@ -1,28 +1,32 @@ - + - - + + + + type="org.oppia.app.home.HomeFragmentPresenter" /> - - + + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="parent" /> +