Skip to content

Commit

Permalink
Merge pull request #17432 from wordpress-mobile/issue/17330-jetpack-f…
Browse files Browse the repository at this point in the history
…ocus-create-generic-fullscreen-overlay

Jetpack focus: Show full screen overlay for Jetpack Features
  • Loading branch information
AjeshRPai authored Nov 22, 2022
2 parents 572fabc + eb837a0 commit 5434beb
Show file tree
Hide file tree
Showing 19 changed files with 941 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.wordpress.android.ui.jetpackoverlay

import android.content.res.Resources
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.fragment.app.viewModels
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.databinding.JetpackFeatureRemovalOverlayBinding
import org.wordpress.android.ui.ActivityLauncherWrapper
import org.wordpress.android.ui.ActivityLauncherWrapper.Companion.JETPACK_PACKAGE_NAME
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureOverlayActions.DismissDialog
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureOverlayActions.OpenPlayStore
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureOverlayScreenType
import org.wordpress.android.util.RtlUtils
import org.wordpress.android.util.extensions.exhaustive
import org.wordpress.android.util.extensions.setVisible
import javax.inject.Inject

@AndroidEntryPoint
class JetpackFeatureFullScreenOverlayFragment : BottomSheetDialogFragment() {
@Inject lateinit var activityLauncherWrapper: ActivityLauncherWrapper
private val viewModel: JetpackFeatureFullScreenOverlayViewModel by viewModels()

private var _binding: JetpackFeatureRemovalOverlayBinding? = null
private val binding get() = _binding ?: throw NullPointerException("_binding cannot be null")

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = JetpackFeatureRemovalOverlayBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.init(getSiteScreen(), RtlUtils.isRtl(view.context))
binding.setupObservers()

(dialog as? BottomSheetDialog)?.apply {
setOnShowListener {
val bottomSheet: FrameLayout = dialog?.findViewById(
com.google.android.material.R.id.design_bottom_sheet
) ?: return@setOnShowListener
val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
bottomSheetBehavior.isDraggable = false
if (bottomSheet.layoutParams != null) {
showFullScreenBottomSheet(bottomSheet)
}
expandBottomSheet(bottomSheetBehavior)
}
}
}

private fun showFullScreenBottomSheet(bottomSheet: FrameLayout) {
val layoutParams = bottomSheet.layoutParams
layoutParams.height = Resources.getSystem().displayMetrics.heightPixels
bottomSheet.layoutParams = layoutParams
}

private fun expandBottomSheet(bottomSheetBehavior: BottomSheetBehavior<FrameLayout>) {
bottomSheetBehavior.skipCollapsed = true
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}

private fun getSiteScreen() =
arguments?.getSerializable(OVERLAY_SCREEN_TYPE) as JetpackFeatureOverlayScreenType

private fun JetpackFeatureRemovalOverlayBinding.setupObservers() {
viewModel.uiState.observe(viewLifecycleOwner) {
renderUiState(it)
}

viewModel.action.observe(viewLifecycleOwner) { action ->
when (action) {
is OpenPlayStore -> {
dismiss()
activity?.let {
activityLauncherWrapper.openPlayStoreLink(it, JETPACK_PACKAGE_NAME)
}
}
is DismissDialog -> {
dismiss()
}
}.exhaustive
}
}

private fun JetpackFeatureRemovalOverlayBinding.renderUiState(
jetpackPoweredOverlayUIState: JetpackFeatureOverlayUIState
) {
updateVisibility(jetpackPoweredOverlayUIState.componentVisibility)
updateContent(jetpackPoweredOverlayUIState.overlayContent)
setClickListener(jetpackPoweredOverlayUIState.componentVisibility.secondaryButton)
}

private fun JetpackFeatureRemovalOverlayBinding.setClickListener(secondaryButtonVisible: Boolean) {
primaryButton.setOnClickListener { viewModel.openJetpackAppDownloadLink() }
closeButton.setOnClickListener { viewModel.closeBottomSheet() }
if (secondaryButtonVisible) secondaryButton.setOnClickListener { viewModel.dismissBottomSheet() }
}

private fun JetpackFeatureRemovalOverlayBinding.updateVisibility(
componentVisibility: JetpackFeatureOverlayComponentVisibility
) {
componentVisibility.let {
illustrationView.setVisible(it.illustration)
title.setVisible(it.title)
caption.setVisible(it.caption)
primaryButton.setVisible(it.primaryButton)
secondaryButton.setVisible(it.secondaryButton)
}
}

private fun JetpackFeatureRemovalOverlayBinding.updateContent(overlayContent: JetpackFeatureOverlayContent) {
overlayContent.let {
illustrationView.setAnimation(it.illustration)
illustrationView.playAnimation()
title.text = getString(it.title)
caption.text = getString(it.caption)
primaryButton.text = getString(it.primaryButtonText)
secondaryButton.text = getString(it.secondaryButtonText)
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

companion object {
const val TAG = "JETPACK_POWERED_OVERLAY_FULL_SCREEN_FRAGMENT"
private const val OVERLAY_SCREEN_TYPE = "KEY_JETPACK_OVERLAY_SCREEN"

@JvmStatic fun newInstance(
jetpackFeatureOverlayScreenType: JetpackFeatureOverlayScreenType?
) = JetpackFeatureFullScreenOverlayFragment().apply {
arguments = Bundle().apply {
putSerializable(OVERLAY_SCREEN_TYPE, jetpackFeatureOverlayScreenType)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.wordpress.android.ui.jetpackoverlay

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import org.wordpress.android.modules.UI_THREAD
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureOverlayScreenType
import org.wordpress.android.viewmodel.ScopedViewModel
import javax.inject.Inject
import javax.inject.Named

@HiltViewModel
class JetpackFeatureFullScreenOverlayViewModel @Inject constructor(
@Named(UI_THREAD) mainDispatcher: CoroutineDispatcher,
private val jetpackFeatureOverlayContentBuilder: JetpackFeatureOverlayContentBuilder,
private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper,
private val jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil
) : ScopedViewModel(mainDispatcher) {
private val _uiState = MutableLiveData<JetpackFeatureOverlayUIState>()
val uiState: LiveData<JetpackFeatureOverlayUIState> = _uiState

private val _action = MutableLiveData<JetpackFeatureOverlayActions>()
val action: LiveData<JetpackFeatureOverlayActions> = _action

fun openJetpackAppDownloadLink() {
_action.value = JetpackFeatureOverlayActions.OpenPlayStore
}

fun dismissBottomSheet() {
_action.value = JetpackFeatureOverlayActions.DismissDialog
}

fun closeBottomSheet() {
_action.value = JetpackFeatureOverlayActions.DismissDialog
}

fun init(overlayScreenType: JetpackFeatureOverlayScreenType?, rtlLayout: Boolean) {
val params = JetpackFeatureOverlayContentBuilderParams(
currentPhase = getCurrentPhase()!!,
isRtl = rtlLayout,
feature = overlayScreenType
)
_uiState.postValue(jetpackFeatureOverlayContentBuilder.build(params = params))
jetpackFeatureRemovalOverlayUtil.onOverlayShown(overlayScreenType)
}

private fun getCurrentPhase() = jetpackFeatureRemovalPhaseHelper.getCurrentPhase()
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.wordpress.android.ui.jetpackoverlay

import org.wordpress.android.R
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureOverlayScreenType
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureOverlayScreenType.SITE_CREATION
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseFour
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseNewUsers
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseOne
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseThree
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseTwo
import javax.inject.Inject

class JetpackFeatureOverlayContentBuilder @Inject constructor() {
fun build(params: JetpackFeatureOverlayContentBuilderParams): JetpackFeatureOverlayUIState {
return when (params.currentPhase) {
is PhaseOne -> getStateForPhaseOne(params,params.feature!!)
PhaseTwo -> TODO()
PhaseThree -> TODO()
PhaseFour -> TODO()
PhaseNewUsers -> TODO()
}
}
private fun getStateForPhaseOne(
params: JetpackFeatureOverlayContentBuilderParams,
feature: JetpackFeatureOverlayScreenType
): JetpackFeatureOverlayUIState {
val componentVisibility = JetpackFeatureOverlayComponentVisibility.PhaseOne()
val content = when (feature) {
JetpackFeatureOverlayScreenType.STATS -> getStateForPhaseOneStats(params.isRtl)
JetpackFeatureOverlayScreenType.NOTIFICATIONS -> getStateForPhaseOneNotifications(params.isRtl)
JetpackFeatureOverlayScreenType.READER -> getStateForPhaseOneReader(params.isRtl)
SITE_CREATION -> TODO()
}
return JetpackFeatureOverlayUIState(componentVisibility, content)
}

private fun getStateForPhaseOneStats(rtl: Boolean): JetpackFeatureOverlayContent {
return JetpackFeatureOverlayContent(
illustration = if (rtl) R.raw.jp_stats_rtl else R.raw.jp_stats_left,
title = R.string.wp_jetpack_feature_removal_overlay_phase_one_title_stats,
caption = R.string.wp_jetpack_feature_removal_overlay_phase_one_description_stats,
primaryButtonText = R.string.wp_jetpack_feature_removal_overlay_switch_to_new_jetpack_app,
secondaryButtonText = R.string.wp_jetpack_continue_to_stats
)
}

private fun getStateForPhaseOneReader(rtl: Boolean): JetpackFeatureOverlayContent {
return JetpackFeatureOverlayContent(
illustration = if (rtl) R.raw.jp_reader_rtl else R.raw.jp_reader_left,
title = R.string.wp_jetpack_feature_removal_overlay_phase_one_title_reader,
caption = R.string.wp_jetpack_feature_removal_overlay_phase_one_description_reader,
primaryButtonText = R.string.wp_jetpack_feature_removal_overlay_switch_to_new_jetpack_app,
secondaryButtonText = R.string.wp_jetpack_continue_to_reader
)
}

private fun getStateForPhaseOneNotifications(rtl: Boolean): JetpackFeatureOverlayContent {
return JetpackFeatureOverlayContent(
illustration = if (rtl) R.raw.jp_notifications_rtl else R.raw.jp_notifications_left,
title = R.string.wp_jetpack_feature_removal_overlay_phase_one_title_notifications,
caption = R.string.wp_jetpack_feature_removal_overlay_phase_one_description_notifications,
primaryButtonText = R.string.wp_jetpack_feature_removal_overlay_switch_to_new_jetpack_app,
secondaryButtonText = R.string.wp_jetpack_continue_to_notifications
)
}
}

data class JetpackFeatureOverlayContentBuilderParams(
val currentPhase: JetpackFeatureRemovalPhase,
val isRtl: Boolean = true,
val feature: JetpackFeatureOverlayScreenType?
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.wordpress.android.ui.jetpackoverlay

import android.content.SharedPreferences
import org.wordpress.android.ui.jetpackoverlay.JetpackOverlayConnectedFeature.NOTIFICATIONS
import org.wordpress.android.ui.jetpackoverlay.JetpackOverlayConnectedFeature.READER
import org.wordpress.android.ui.jetpackoverlay.JetpackOverlayConnectedFeature.STATS
import javax.inject.Inject

class JetpackFeatureOverlayShownTracker @Inject constructor(private val sharedPrefs: SharedPreferences) {
fun getEarliestOverlayShownTime(phase: JetpackFeatureRemovalOverlayPhase): Long? {
val overlayShownTimeStampList: ArrayList<Long> = arrayListOf()
getFeatureOverlayShownTimeStamp(STATS, phase)?.let { overlayShownTimeStampList.add(it) }
getFeatureOverlayShownTimeStamp(NOTIFICATIONS, phase)?.let { overlayShownTimeStampList.add(it) }
getFeatureOverlayShownTimeStamp(READER, phase)?.let { overlayShownTimeStampList.add(it) }
// No jetpack connected feature is accessed yet
if (overlayShownTimeStampList.isEmpty()) return null
return overlayShownTimeStampList.minOf { it }
}

fun getFeatureOverlayShownTimeStamp(
jetpackOverlayConnectedFeature: JetpackOverlayConnectedFeature,
phase: JetpackFeatureRemovalOverlayPhase
): Long? {
val overlayShownTime = sharedPrefs.getLong(jetpackOverlayConnectedFeature.getPreferenceKey(phase), 0L)
// jetpack connected feature is not accessed yet
if (overlayShownTime == 0L) return null
return overlayShownTime
}

fun setFeatureOverlayShownTimeStamp(
jetpackOverlayConnectedFeature: JetpackOverlayConnectedFeature,
phase: JetpackFeatureRemovalOverlayPhase,
timeStamp: Long
) {
sharedPrefs.edit().putLong(jetpackOverlayConnectedFeature.getPreferenceKey(phase), timeStamp).apply()
}
}

enum class JetpackOverlayConnectedFeature(private val featureSpecificPreferenceKey: String) {
STATS("STATS_OVERLAY_SHOWN_TIME_STAMP_KEY"),
NOTIFICATIONS("READER_OVERLAY_SHOWN_TIME_STAMP_KEY"),
READER("NOTIFICATIONS_OVERLAY_SHOWN_TIME_STAMP_KEY");
fun getPreferenceKey(phase: JetpackFeatureRemovalOverlayPhase): String {
return featureSpecificPreferenceKey.plus(phase.preferenceKey)
}
}

enum class JetpackFeatureRemovalOverlayPhase(val preferenceKey: String) {
PHASE_ONE("JETPACK_FEATURE_PHASE_ONE"),
PHASE_TWO("JETPACK_FEATURE_PHASE_TWO"),
PHASE_THREE("JETPACK_FEATURE_PHASE_THREE")
}
Loading

0 comments on commit 5434beb

Please sign in to comment.