Skip to content
Permalink
89df01ebc1
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
405 lines (356 sloc) 16.8 KB
/*
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.iosched.ui.sessiondetail
import android.content.Intent
import android.os.Bundle
import android.provider.CalendarContract
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.app.ShareCompat
import androidx.core.net.toUri
import androidx.core.view.forEach
import androidx.core.view.updatePadding
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import androidx.transition.TransitionInflater
import com.google.samples.apps.iosched.R
import com.google.samples.apps.iosched.R.style
import com.google.samples.apps.iosched.databinding.FragmentSessionDetailBinding
import com.google.samples.apps.iosched.model.Session
import com.google.samples.apps.iosched.model.SessionId
import com.google.samples.apps.iosched.model.SpeakerId
import com.google.samples.apps.iosched.shared.analytics.AnalyticsActions
import com.google.samples.apps.iosched.shared.analytics.AnalyticsHelper
import com.google.samples.apps.iosched.shared.di.MapFeatureEnabledFlag
import com.google.samples.apps.iosched.shared.domain.users.SwapRequestParameters
import com.google.samples.apps.iosched.shared.notifications.AlarmBroadcastReceiver
import com.google.samples.apps.iosched.shared.result.EventObserver
import com.google.samples.apps.iosched.shared.util.activityViewModelProvider
import com.google.samples.apps.iosched.shared.util.toEpochMilli
import com.google.samples.apps.iosched.shared.util.viewModelProvider
import com.google.samples.apps.iosched.ui.MainNavigationFragment
import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
import com.google.samples.apps.iosched.ui.prefs.SnackbarPreferenceViewModel
import com.google.samples.apps.iosched.ui.reservation.RemoveReservationDialogFragment
import com.google.samples.apps.iosched.ui.reservation.RemoveReservationDialogFragment.Companion.DIALOG_REMOVE_RESERVATION
import com.google.samples.apps.iosched.ui.reservation.RemoveReservationDialogParameters
import com.google.samples.apps.iosched.ui.reservation.SwapReservationDialogFragment
import com.google.samples.apps.iosched.ui.reservation.SwapReservationDialogFragment.Companion.DIALOG_SWAP_RESERVATION
import com.google.samples.apps.iosched.ui.schedule.ScheduleFragmentDirections.Companion.toSessionDetail
import com.google.samples.apps.iosched.ui.sessiondetail.SessionDetailFragmentDirections.Companion.toSpeakerDetail
import com.google.samples.apps.iosched.ui.setUpSnackbar
import com.google.samples.apps.iosched.ui.signin.NotificationsPreferenceDialogFragment
import com.google.samples.apps.iosched.ui.signin.NotificationsPreferenceDialogFragment.Companion.DIALOG_NOTIFICATIONS_PREFERENCE
import com.google.samples.apps.iosched.ui.signin.SignInDialogFragment
import com.google.samples.apps.iosched.ui.signin.SignInDialogFragment.Companion.DIALOG_SIGN_IN
import com.google.samples.apps.iosched.util.doOnApplyWindowInsets
import com.google.samples.apps.iosched.util.openWebsiteUrl
import com.google.samples.apps.iosched.util.postponeEnterTransition
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Named
class SessionDetailFragment : MainNavigationFragment(), SessionFeedbackFragment.Listener {
private var shareString = ""
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject lateinit var snackbarMessageManager: SnackbarMessageManager
private lateinit var sessionDetailViewModel: SessionDetailViewModel
@Inject lateinit var analyticsHelper: AnalyticsHelper
@Inject
@field:Named("tagViewPool")
lateinit var tagRecycledViewPool: RecycledViewPool
@Inject
@JvmField
@MapFeatureEnabledFlag
var isMapEnabled: Boolean = false
private var session: Session? = null
private lateinit var sessionTitle: String
private lateinit var binding: FragmentSessionDetailBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
sessionDetailViewModel = viewModelProvider(viewModelFactory)
sharedElementReturnTransition =
TransitionInflater.from(context).inflateTransition(R.transition.speaker_shared_enter)
// Delay the enter transition until speaker image has loaded.
postponeEnterTransition(500L)
val themedInflater =
inflater.cloneInContext(ContextThemeWrapper(requireActivity(), style.AppTheme_Detail))
binding = FragmentSessionDetailBinding.inflate(themedInflater, container, false).apply {
viewModel = sessionDetailViewModel
lifecycleOwner = viewLifecycleOwner
}
binding.sessionDetailBottomAppBar.run {
inflateMenu(R.menu.session_detail_menu)
menu.findItem(R.id.menu_item_map)?.isVisible = isMapEnabled
sessionDetailViewModel.session.observe(viewLifecycleOwner, Observer { session ->
menu.findItem(R.id.menu_item_ask_question).isVisible = session.doryLink.isNotBlank()
})
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_item_share -> {
ShareCompat.IntentBuilder.from(activity)
.setType("text/plain")
.setText(shareString)
.setChooserTitle(R.string.intent_chooser_session_detail)
.startChooser()
}
R.id.menu_item_star -> {
sessionDetailViewModel.onStarClicked()
}
R.id.menu_item_map -> {
val directions = SessionDetailFragmentDirections.toMap(
featureId = session?.room?.id,
startTime = session?.startTime?.toEpochMilli() ?: 0L
)
findNavController().navigate(directions)
}
R.id.menu_item_ask_question -> {
sessionDetailViewModel.session.value?.let { session ->
openWebsiteUrl(requireContext(), session.doryLink)
}
}
R.id.menu_item_calendar -> {
sessionDetailViewModel.session.value?.let(::addToCalendar)
}
}
true
}
}
val detailsAdapter = SessionDetailAdapter(
viewLifecycleOwner,
sessionDetailViewModel,
tagRecycledViewPool
)
binding.sessionDetailRecyclerView.run {
adapter = detailsAdapter
itemAnimator?.run {
addDuration = 120L
moveDuration = 120L
changeDuration = 120L
removeDuration = 100L
}
doOnApplyWindowInsets { view, insets, state ->
view.updatePadding(bottom = state.bottom + insets.systemWindowInsetBottom)
// CollapsingToolbarLayout's defualt scrim visible trigger height is a bit large.
// Choose something smaller so that the content stays visible longer.
binding.collapsingToolbar.scrimVisibleHeightTrigger =
insets.systemWindowInsetTop * 2
}
}
sessionDetailViewModel.session.observe(this, Observer {
detailsAdapter.speakers = it?.speakers?.toList() ?: emptyList()
})
sessionDetailViewModel.relatedUserSessions.observe(this, Observer {
detailsAdapter.related = it ?: emptyList()
})
sessionDetailViewModel.session.observe(this, Observer {
session = it
shareString = if (it == null) {
""
} else {
getString(R.string.share_text_session_detail, it.title, it.sessionUrl)
}
})
sessionDetailViewModel.navigateToYouTubeAction.observe(this, EventObserver { youtubeUrl ->
openYoutubeUrl(youtubeUrl)
})
sessionDetailViewModel.navigateToSessionAction.observe(this, EventObserver { sessionId ->
findNavController().navigate(toSessionDetail(sessionId))
})
val snackbarPreferenceViewModel: SnackbarPreferenceViewModel =
activityViewModelProvider(viewModelFactory)
setUpSnackbar(
sessionDetailViewModel.snackBarMessage,
binding.snackbar,
snackbarMessageManager,
actionClickListener = {
snackbarPreferenceViewModel.onStopClicked()
}
)
sessionDetailViewModel.errorMessage.observe(this, EventObserver { errorMsg ->
// TODO: Change once there's a way to show errors to the user
Toast.makeText(this.context, errorMsg, Toast.LENGTH_LONG).show()
})
sessionDetailViewModel.navigateToSignInDialogAction.observe(this, EventObserver {
openSignInDialog(requireActivity())
})
sessionDetailViewModel.navigateToRemoveReservationDialogAction.observe(this, EventObserver {
openRemoveReservationDialog(requireActivity(), it)
})
sessionDetailViewModel.navigateToSwapReservationDialogAction.observe(this, EventObserver {
openSwapReservationDialog(requireActivity(), it)
})
sessionDetailViewModel.shouldShowNotificationsPrefAction.observe(this, EventObserver {
if (it) {
openNotificationsPreferenceDialog()
}
})
sessionDetailViewModel.navigateToSpeakerDetail.observe(this, EventObserver { speakerId ->
val sharedElement = findSpeakerHeadshot(binding.sessionDetailRecyclerView, speakerId)
val extras = FragmentNavigatorExtras(sharedElement to sharedElement.transitionName)
findNavController().navigate(toSpeakerDetail(speakerId), extras)
})
sessionDetailViewModel.navigateToSessionFeedbackAction.observe(this, EventObserver {
openFeedbackDialog(it)
})
// When opened from the post session notification, open the feedback dialog
requireNotNull(arguments).apply {
val sessionId = getString(EXTRA_SESSION_ID)
?: SessionDetailFragmentArgs.fromBundle(this).sessionId
val openRateSession =
arguments?.getBoolean(AlarmBroadcastReceiver.EXTRA_SHOW_RATE_SESSION_FLAG) ?: false
sessionDetailViewModel.showFeedbackButton.observe(this@SessionDetailFragment, Observer {
if (it == true && openRateSession) {
openFeedbackDialog(sessionId)
}
})
}
return binding.root
}
override fun onStart() {
super.onStart()
Timber.d("Loading details for session $arguments")
requireNotNull(arguments).apply {
// TODO(benbaxter): Only use SessionDetailFragmentArgs and delete SessionDetailActivity.
// Default with the value passed from the activity, otherwise assume the fragment was
// added from the navigation controller.
val sessionId = getString(EXTRA_SESSION_ID)
?: SessionDetailFragmentArgs.fromBundle(this).sessionId
sessionDetailViewModel.setSessionId(sessionId)
}
}
override fun onStop() {
super.onStop()
// Force a refresh when this screen gets added to a backstack and user comes back to it.
sessionDetailViewModel.setSessionId(null)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Observing the changes from Fragment because data binding doesn't work with menu items.
val menu = binding.sessionDetailBottomAppBar.menu
val starMenu = menu.findItem(R.id.menu_item_star)
sessionDetailViewModel.shouldShowStarInBottomNav.observe(this, Observer { showStar ->
showStar?.let {
if (it) {
starMenu.setVisible(true)
} else {
starMenu.setVisible(false)
}
}
})
sessionDetailViewModel.userEvent.observe(this, Observer { userEvent ->
userEvent?.let {
if (it.isStarred) {
starMenu.setIcon(R.drawable.ic_star)
} else {
starMenu.setIcon(R.drawable.ic_star_border)
}
}
})
var titleUpdated = false
sessionDetailViewModel.session.observe(this, Observer {
if (it != null && !titleUpdated) {
sessionTitle = it.title
activity?.let { activity ->
analyticsHelper.sendScreenView("Session Details: $sessionTitle", activity)
}
titleUpdated = true
}
})
}
override fun onFeedbackSubmitted() {
binding.snackbar.show(R.string.feedback_thank_you)
}
private fun openYoutubeUrl(youtubeUrl: String) {
analyticsHelper.logUiEvent(sessionTitle, AnalyticsActions.YOUTUBE_LINK)
startActivity(Intent(Intent.ACTION_VIEW, youtubeUrl.toUri()))
}
private fun openSignInDialog(activity: FragmentActivity) {
SignInDialogFragment().show(activity.supportFragmentManager, DIALOG_SIGN_IN)
}
private fun openNotificationsPreferenceDialog() {
NotificationsPreferenceDialogFragment()
.show(requireActivity().supportFragmentManager, DIALOG_NOTIFICATIONS_PREFERENCE)
}
private fun openRemoveReservationDialog(
activity: FragmentActivity,
parameters: RemoveReservationDialogParameters
) {
RemoveReservationDialogFragment.newInstance(parameters)
.show(activity.supportFragmentManager, DIALOG_REMOVE_RESERVATION)
}
private fun openSwapReservationDialog(
activity: FragmentActivity,
parameters: SwapRequestParameters
) {
SwapReservationDialogFragment.newInstance(parameters)
.show(activity.supportFragmentManager, DIALOG_SWAP_RESERVATION)
}
private fun findSpeakerHeadshot(speakers: ViewGroup, speakerId: SpeakerId): View {
speakers.forEach {
if (it.getTag(R.id.tag_speaker_id) == speakerId) {
return it.findViewById(R.id.speaker_item_headshot)
}
}
Timber.e("Could not find view for speaker id $speakerId")
return speakers
}
private fun addToCalendar(session: Session) {
val intent = Intent(Intent.ACTION_INSERT)
.setData(CalendarContract.Events.CONTENT_URI)
.putExtra(CalendarContract.Events.TITLE, session.title)
.putExtra(CalendarContract.Events.EVENT_LOCATION, session.room?.name)
.putExtra(CalendarContract.Events.DESCRIPTION, session.getCalendarDescription(
getString(R.string.paragraph_delimiter),
getString(R.string.speaker_delimiter)
))
.putExtra(
CalendarContract.EXTRA_EVENT_BEGIN_TIME,
session.startTime.toEpochMilli()
)
.putExtra(
CalendarContract.EXTRA_EVENT_END_TIME,
session.endTime.toEpochMilli()
)
if (intent.resolveActivity(requireContext().packageManager) != null) {
startActivity(intent)
}
}
private fun openFeedbackDialog(sessionId: String) {
SessionFeedbackFragment.createInstance(sessionId)
.show(childFragmentManager, FRAGMENT_SESSION_FEEDBACK)
}
companion object {
private const val FRAGMENT_SESSION_FEEDBACK = "feedback"
private const val EXTRA_SESSION_ID = "SESSION_ID"
fun newInstance(sessionId: SessionId, openRateSession: Boolean): SessionDetailFragment {
val bundle = Bundle().apply {
putString(EXTRA_SESSION_ID, sessionId)
putBoolean(AlarmBroadcastReceiver.EXTRA_SHOW_RATE_SESSION_FLAG, openRateSession)
}
return SessionDetailFragment().apply { arguments = bundle }
}
}
}