For #26777 - Part 2: Refactor the Jump Back In onboarding CFR to use the CFRPopup

This commit is contained in:
Gabriel Luong 2022-09-07 01:29:15 -04:00 committed by mergify[bot]
parent 86b62af556
commit 812f073e39
4 changed files with 105 additions and 226 deletions

View File

@ -23,7 +23,6 @@ import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.OnboardingState
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.onboarding.JumpBackInCFRDialog
import org.mozilla.fenix.onboarding.HomeCFRPresenter
import org.mozilla.fenix.utils.Settings
@ -213,17 +212,15 @@ class SessionControlView(
override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
if (!context.settings().showHomeOnboardingDialog) {
if (context.settings().showSyncCFR) {
HomeCFRPresenter(
context = context,
recyclerView = view,
).showSyncCFR()
}
if (context.settings().shouldShowJumpBackInCFR) {
JumpBackInCFRDialog(view).showIfNeeded()
}
if (!context.settings().showHomeOnboardingDialog && (
context.settings().showSyncCFR ||
context.settings().shouldShowJumpBackInCFR
)
) {
HomeCFRPresenter(
context = context,
recyclerView = view,
).show()
}
// We want some parts of the home screen UI to be rendered first if they are

View File

@ -10,11 +10,13 @@ import androidx.compose.ui.unit.dp
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Onboarding
import org.mozilla.fenix.GleanMetrics.RecentTabs
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.cfr.CFRPopup
import org.mozilla.fenix.compose.cfr.CFRPopupProperties
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.recentsyncedtabs.view.RecentSyncedTabViewHolder
import org.mozilla.fenix.home.recenttabs.view.RecentTabsHeaderViewHolder
/**
* Vertical padding needed to improve the visual alignment of the popup and respect the UX design.
@ -22,58 +24,119 @@ import org.mozilla.fenix.home.recentsyncedtabs.view.RecentSyncedTabViewHolder
private const val CFR_TO_ANCHOR_VERTICAL_PADDING = -16
/**
* Delegate for handling synced tab onboarding CFR.
* Delegate for handling the Home Onboarding CFR.
*
* @param context [Context] used for various Android interactions.
* @param recyclerView [RecyclerView] will serve as anchor for the sync CFR.
* @param recyclerView [RecyclerView] will serve as anchor for the CFR.
*/
class HomeCFRPresenter(
private val context: Context,
private val recyclerView: RecyclerView,
) {
private var syncCFR: CFRPopup? = null
/**
* Find the synced view and if it is available, then show the synced tab CFR.
* Determine the CFR to be shown on the Home screen and show a CFR for the resultant view
* if any.
*/
fun showSyncCFR() {
findSyncTabsView()?.let {
CFRPopup(
text = context.getString(R.string.sync_cfr_message),
anchor = it,
properties = CFRPopupProperties(
indicatorDirection = CFRPopup.IndicatorDirection.DOWN,
popupVerticalOffset = CFR_TO_ANCHOR_VERTICAL_PADDING.dp,
),
onDismiss = {
when (it) {
true -> Onboarding.syncCfrExplicitDismissal.record(NoExtras())
false -> Onboarding.syncCfrImplicitDismissal.record(NoExtras())
}
}
).apply {
syncCFR = this
show()
fun show() {
when (val result = getCFRToShow()) {
is Result.SyncedTab -> showSyncedTabCFR(view = result.view)
is Result.JumpBackIn -> showJumpBackInCFR(view = result.view)
else -> {
// no-op
}
context.settings().showSyncCFR = false
context.settings().shouldShowJumpBackInCFR = false
Onboarding.synCfrShown.record(NoExtras())
}
}
private fun findSyncTabsView(): View? {
val count = recyclerView.adapter?.itemCount ?: return null
private fun showSyncedTabCFR(view: View) {
CFRPopup(
text = context.getString(R.string.sync_cfr_message),
anchor = view,
properties = CFRPopupProperties(
indicatorDirection = CFRPopup.IndicatorDirection.DOWN,
popupVerticalOffset = CFR_TO_ANCHOR_VERTICAL_PADDING.dp,
),
onDismiss = {
when (it) {
true -> Onboarding.syncCfrExplicitDismissal.record(NoExtras())
false -> Onboarding.syncCfrImplicitDismissal.record(NoExtras())
}
}
).show()
// Turn off both the recent tab and synced tab CFR after the recent synced tab CFR is shown.
context.settings().showSyncCFR = false
context.settings().shouldShowJumpBackInCFR = false
Onboarding.synCfrShown.record(NoExtras())
}
@Suppress("MagicNumber")
private fun showJumpBackInCFR(view: View) {
CFRPopup(
text = context.getString(R.string.onboarding_home_screen_jump_back_contextual_hint_2),
anchor = view,
properties = CFRPopupProperties(
indicatorDirection = CFRPopup.IndicatorDirection.DOWN,
popupVerticalOffset = (-40).dp, // Offset the top spacer in the recent tabs header.
),
onDismiss = {
when (it) {
true -> RecentTabs.jumpBackInCfrDismissed.record(NoExtras())
false -> RecentTabs.jumpBackInCfrCancelled.record(NoExtras())
}
}
).show()
// Users can still see the recent synced tab CFR after the recent tab CFR is shown in
// subsequent navigation to the Home screen.
context.settings().shouldShowJumpBackInCFR = false
RecentTabs.jumpBackInCfrShown.record(NoExtras())
}
/**
* Returns a [Result] that indicates the CFR that should be shown on the Home screen if any
* based on the views available and the preferences.
*/
private fun getCFRToShow(): Result {
var result: Result = Result.None
val count = recyclerView.adapter?.itemCount ?: return result
for (index in count downTo 0) {
val viewHolder = recyclerView.findViewHolderForAdapterPosition(index)
if (viewHolder is RecentSyncedTabViewHolder) {
return viewHolder.composeView
if (context.settings().showSyncCFR && viewHolder is RecentSyncedTabViewHolder) {
result = Result.SyncedTab(view = viewHolder.composeView)
break
} else if (context.settings().shouldShowJumpBackInCFR &&
viewHolder is RecentTabsHeaderViewHolder
) {
result = Result.JumpBackIn(view = viewHolder.composeView)
}
}
return null
return result
}
/**
* The result of determining which CFR to show on the Home screen.
*/
sealed class Result {
/**
* Indicates no CFR should be shown on the Home screen.
*/
object None : Result()
/**
* Indicates a CFR should be shown for a Synced Tab and the associated [view] to anchor
* the CFR.
*/
data class SyncedTab(val view: View) : Result()
/**
* Indicates a CFR should be for Jump Back In and the associated [view] to anchor the CFR.
*/
data class JumpBackIn(val view: View) : Result()
}
}

View File

@ -1,118 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.onboarding
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.RecentTabs
import org.mozilla.fenix.databinding.OnboardingJumpBackInCfrBinding
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.recentsyncedtabs.view.RecentSyncedTabViewHolder
import org.mozilla.fenix.home.recenttabs.view.RecentTabsHeaderViewHolder
/**
* Dialog displayed once when the jump back in section is shown in the home screen.
*/
class JumpBackInCFRDialog(val recyclerView: RecyclerView) {
/**
* Try to show the crf dialog if it hasn't been shown before.
*/
fun showIfNeeded() {
val jumpBackInView = findJumpBackInView()
jumpBackInView?.let {
val crfDialog = createJumpCRF(anchor = jumpBackInView)
crfDialog?.let {
RecentTabs.jumpBackInCfrShown.record(NoExtras())
val context = jumpBackInView.context
context.settings().shouldShowJumpBackInCFR = false
it.show()
}
}
}
private fun findJumpBackInView(): View? {
val count = recyclerView.adapter?.itemCount ?: return null
for (index in 0..count) {
val viewHolder = recyclerView.findViewHolderForAdapterPosition(index)
if (viewHolder is RecentTabsHeaderViewHolder) {
return viewHolder.composeView
}
}
return null
}
private fun hasSyncTabsView(): Boolean {
val count = recyclerView.adapter?.itemCount ?: return false
for (index in count downTo 0) {
val viewHolder = recyclerView.findViewHolderForAdapterPosition(index)
if (viewHolder is RecentSyncedTabViewHolder) {
return true
}
}
return false
}
private fun createJumpCRF(anchor: View): Dialog? {
val context: Context = recyclerView.context
if (context.settings().showSyncCFR && hasSyncTabsView()) {
context.settings().shouldShowJumpBackInCFR = false
}
if (!context.settings().shouldShowJumpBackInCFR) {
return null
}
val anchorPosition = IntArray(2)
val popupBinding = OnboardingJumpBackInCfrBinding.inflate(LayoutInflater.from(context))
val popup = Dialog(context)
popup.apply {
setContentView(popupBinding.root)
setCanceledOnTouchOutside(true)
setOnCancelListener {
RecentTabs.jumpBackInCfrCancelled.record(NoExtras())
}
// removing title or setting it as an empty string does not prevent a11y services from assigning one
setTitle(" ")
}
popupBinding.closeInfoBanner.setOnClickListener {
RecentTabs.jumpBackInCfrDismissed.record(NoExtras())
popup.dismiss()
}
anchor.getLocationOnScreen(anchorPosition)
val (x, y) = anchorPosition
if (x == 0 && y == 0) {
return null
}
popupBinding.root.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
popup.window?.apply {
val attr = attributes
setGravity(Gravity.START or Gravity.TOP)
attr.x = x
attr.y = y - popupBinding.root.measuredHeight
attributes = attr
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setDimAmount(0f)
}
return popup
}
}

View File

@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/banner_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/onboarding_popup_shape">
<TextView
android:id="@+id/banner_info_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:maxWidth="279dp"
android:text="@string/onboarding_home_screen_jump_back_contextual_hint_2"
android:textAppearance="@style/Body16TextStyle"
android:textColor="@color/fx_mobile_private_text_color_primary"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/close_info_banner"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<ImageButton
android:id="@+id/close_info_banner"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/content_description_close_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mozac_ic_close_20"
app:tint="@color/fx_mobile_private_text_color_primary" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:layout_width="@dimen/cfr_triangle_width"
android:layout_height="@dimen/cfr_triangle_height"
android:layout_gravity="start"
android:layout_marginStart="15dp"
android:layout_marginEnd="@dimen/cfr_triangle_margin_edge"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_cfr_triangle"
android:rotation="180"
app:tint="@color/fx_mobile_layer_color_gradient_start" />
</LinearLayout>