Bug 1812461 - Migrate private browsing recommend PopupWindow to CFRPopup

Because previously the `privateBrowsingRecommend` Popup was using
PopupWindow from android widget, it could not be modified to
respect to PopupWindow behaviors. Therefore, we decided to migrate
it to the CFRPopup.
fenix/119.0
DreVla 1 year ago committed by mergify[bot]
parent 4f96cde8c5
commit 611df98d22

@ -4,15 +4,19 @@
package org.mozilla.fenix.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.searchScreen
@ -20,7 +24,8 @@ class AddToHomeScreenTest {
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
val composeTestRule =
AndroidComposeTestRule(HomeActivityTestRule.withDefaultSettingsOverrides()) { it.activity }
@Before
fun setUp() {
@ -35,14 +40,45 @@ class AddToHomeScreenTest {
mockWebServer.shutdown()
}
// Verifies the Add to home screen option in a tab's 3 dot menu
@SmokeTest
@Test
fun mainMenuAddToHomeScreenTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val shortcutTitle = TestHelper.generateRandomString(5)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
clickCancelShortcutButton()
}
browserScreen {
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
verifyShortcutTextFieldTitle("Test_Page_1")
addShortcutName(shortcutTitle)
clickAddShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(shortcutTitle) {
verifyUrl(website.url.toString())
verifyTabCounter("1")
}
}
@Ignore("Failure, more details at: https://bugzilla.mozilla.org/show_bug.cgi?id=1830005")
@SmokeTest
@Test
fun addPrivateBrowsingShortcutTest() {
homeScreen {
}.triggerPrivateBrowsingShortcutPrompt {
verifyNoThanksPrivateBrowsingShortcutButton()
verifyAddPrivateBrowsingShortcutButton()
clickAddPrivateBrowsingShortcutButton()
verifyNoThanksPrivateBrowsingShortcutButton(composeTestRule)
verifyAddPrivateBrowsingShortcutButton(composeTestRule)
clickAddPrivateBrowsingShortcutButton(composeTestRule)
clickAddAutomaticallyButton()
}.openHomeScreenShortcut("Private ${TestHelper.appName}") {}
searchScreen {

@ -5,23 +5,21 @@
package org.mozilla.fenix.ui.robots
import android.os.Build
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
import java.util.regex.Pattern
/**
@ -29,11 +27,11 @@ import java.util.regex.Pattern
*/
class AddToHomeScreenRobot {
fun verifyAddPrivateBrowsingShortcutButton() = assertAddPrivateBrowsingShortcutButton()
fun verifyAddPrivateBrowsingShortcutButton(composeTestRule: ComposeTestRule) = assertAddPrivateBrowsingShortcutButton(composeTestRule)
fun verifyNoThanksPrivateBrowsingShortcutButton() = assertNoThanksPrivateBrowsingShortcutButton()
fun verifyNoThanksPrivateBrowsingShortcutButton(composeTestRule: ComposeTestRule) = assertNoThanksPrivateBrowsingShortcutButton(composeTestRule)
fun clickAddPrivateBrowsingShortcutButton() = addPrivateBrowsingShortcutButton().click()
fun clickAddPrivateBrowsingShortcutButton(composeTestRule: ComposeTestRule) = composeTestRule.onNodeWithTag("private.add").performClick()
fun addShortcutName(title: String) = shortcutTextField.setText(title)
@ -104,15 +102,11 @@ fun addToHomeScreen(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenR
private fun addAutomaticallyButton() =
mDevice.findObject(UiSelector().textContains("add automatically"))
private fun addPrivateBrowsingShortcutButton() = onView(withId(R.id.cfr_pos_button))
private fun assertAddPrivateBrowsingShortcutButton(composeTestRule: ComposeTestRule) =
composeTestRule.onNodeWithTag("private.add").assertIsDisplayed()
private fun assertAddPrivateBrowsingShortcutButton() = addPrivateBrowsingShortcutButton()
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun noThanksPrivateBrowsingShortcutButton() = onView(withId(R.id.cfr_neg_button))
private fun assertNoThanksPrivateBrowsingShortcutButton() = noThanksPrivateBrowsingShortcutButton()
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertNoThanksPrivateBrowsingShortcutButton(composeTestRule: ComposeTestRule) =
composeTestRule.onNodeWithTag("private.cancel").assertIsDisplayed()
private val cancelAddToHomeScreenButton =
itemWithResId("$packageName:id/cancel_button")

@ -271,7 +271,6 @@ class BrowserToolbarCFRPresenter(
}
},
).run {
settings.shouldShowTotalCookieProtectionCFR = false
popup = this
show()
TrackingProtection.tcpCfrShown.record(NoExtras())

@ -10,16 +10,27 @@ import android.content.res.Configuration
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getColor
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -47,6 +58,8 @@ import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.compose.cfr.CFRPopup
import mozilla.components.compose.cfr.CFRPopupProperties
import mozilla.components.concept.storage.FrecencyThresholdOption
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
@ -61,6 +74,7 @@ import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.GleanMetrics.HomeScreen
import org.mozilla.fenix.GleanMetrics.Homepage
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcutCfr
@ -77,10 +91,8 @@ import org.mozilla.fenix.databinding.FragmentHomeBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.containsQueryParameters
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.scaleToBottomOfView
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.pocket.DefaultPocketStoriesController
@ -111,11 +123,11 @@ import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.search.toolbar.DefaultSearchSelectorController
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wallpapers.Wallpaper
import java.lang.ref.WeakReference
import kotlin.math.min
@Suppress("TooManyFunctions", "LargeClass")
class HomeFragment : Fragment() {
@ -828,49 +840,95 @@ class HomeFragment : Fragment() {
requireComponents.useCases.sessionUseCases.updateLastAccess()
}
@SuppressLint("InflateParams")
private var recommendPrivateBrowsingCFR: CFRPopup? = null
@OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongMethod")
private fun recommendPrivateBrowsingShortcut() {
context?.let { context ->
val layout = LayoutInflater.from(context)
.inflate(R.layout.pbm_shortcut_popup, null)
val privateBrowsingRecommend =
PopupWindow(
layout,
min(
(resources.displayMetrics.widthPixels / CFR_WIDTH_DIVIDER).toInt(),
(resources.displayMetrics.heightPixels / CFR_WIDTH_DIVIDER).toInt(),
CFRPopup(
anchor = binding.privateBrowsingButton,
properties = CFRPopupProperties(
popupWidth = 256.dp,
popupAlignment = CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
popupBodyColors = listOf(
getColor(context, R.color.fx_mobile_layer_color_gradient_end),
getColor(context, R.color.fx_mobile_layer_color_gradient_start),
),
LinearLayout.LayoutParams.WRAP_CONTENT,
true,
)
layout.findViewById<Button>(R.id.cfr_pos_button).apply {
this.increaseTapArea(CFR_TAP_INCREASE_DPS)
setOnClickListener {
PrivateBrowsingShortcutCfr.addShortcut.record(NoExtras())
PrivateShortcutCreateManager.createPrivateShortcut(context)
privateBrowsingRecommend.dismiss()
}
}
layout.findViewById<Button>(R.id.cfr_neg_button).apply {
setOnClickListener {
showDismissButton = false,
dismissButtonColor = getColor(context, R.color.fx_mobile_icon_color_oncolor),
indicatorDirection = CFRPopup.IndicatorDirection.UP,
),
onDismiss = {
PrivateBrowsingShortcutCfr.cancel.record()
privateBrowsingRecommend.dismiss()
}
}
// We want to show the popup only after privateBrowsingButton is available.
// Otherwise, we will encounter an activity token error.
binding.privateBrowsingButton.post {
runIfFragmentIsAttached {
context.settings().showedPrivateModeContextualFeatureRecommender = true
context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
privateBrowsingRecommend.showAsDropDown(
binding.privateBrowsingButton,
0,
CFR_Y_OFFSET,
Gravity.TOP or Gravity.END,
)
}
recommendPrivateBrowsingCFR?.dismiss()
},
text = {
FirefoxTheme {
Text(
text = context.getString(R.string.private_mode_cfr_message_2),
color = FirefoxTheme.colors.textOnColorPrimary,
style = FirefoxTheme.typography.headline7,
)
}
},
action = {
FirefoxTheme {
TextButton(
onClick = {
PrivateBrowsingShortcutCfr.addShortcut.record(NoExtras())
PrivateShortcutCreateManager.createPrivateShortcut(context)
context.settings().showedPrivateModeContextualFeatureRecommender = true
context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
recommendPrivateBrowsingCFR?.dismiss()
},
colors = ButtonDefaults.buttonColors(backgroundColor = PhotonColors.LightGrey30),
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(top = 16.dp)
.heightIn(36.dp)
.fillMaxWidth()
.semantics {
testTagsAsResourceId = true
testTag = "private.add"
},
) {
Text(
text = context.getString(R.string.private_mode_cfr_pos_button_text),
color = PhotonColors.DarkGrey50,
style = FirefoxTheme.typography.headline7,
textAlign = TextAlign.Center,
)
}
TextButton(
onClick = {
PrivateBrowsingShortcutCfr.cancel.record()
context.settings().showedPrivateModeContextualFeatureRecommender = true
context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
recommendPrivateBrowsingCFR?.dismiss()
},
modifier = Modifier
.heightIn(36.dp)
.fillMaxWidth()
.semantics {
testTagsAsResourceId = true
testTag = "private.cancel"
},
) {
Text(
text = context.getString(R.string.cfr_neg_button_text),
textAlign = TextAlign.Center,
color = FirefoxTheme.colors.textOnColorPrimary,
style = FirefoxTheme.typography.headline7,
)
}
}
},
).run {
recommendPrivateBrowsingCFR = this
show()
}
}
}
@ -995,11 +1053,6 @@ class HomeFragment : Fragment() {
private const val SCROLL_TO_COLLECTION = "scrollToCollection"
private const val ANIM_SCROLL_DELAY = 100L
private const val CFR_WIDTH_DIVIDER = 1.7
private const val CFR_Y_OFFSET = -20
private const val CFR_TAP_INCREASE_DPS = 6
// Sponsored top sites titles and search engine names used for filtering
const val AMAZON_SPONSORED_TITLE = "Amazon"
const val AMAZON_SEARCH_ENGINE_NAME = "Amazon.com"

@ -69,7 +69,12 @@ class HomeCFRPresenter(
),
onDismiss = {
when (it) {
true -> Onboarding.syncCfrExplicitDismissal.record(NoExtras())
true -> {
Onboarding.syncCfrExplicitDismissal.record(NoExtras())
// 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
}
false -> Onboarding.syncCfrImplicitDismissal.record(NoExtras())
}
},
@ -84,10 +89,6 @@ class HomeCFRPresenter(
},
).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())
}
@ -106,7 +107,12 @@ class HomeCFRPresenter(
),
onDismiss = {
when (it) {
true -> RecentTabs.jumpBackInCfrDismissed.record(NoExtras())
true -> {
RecentTabs.jumpBackInCfrDismissed.record(NoExtras())
// 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
}
false -> RecentTabs.jumpBackInCfrCancelled.record(NoExtras())
}
},
@ -121,10 +127,6 @@ class HomeCFRPresenter(
},
).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())
}

@ -1,20 +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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="45"
android:endColor="@color/fx_mobile_layer_color_gradient_end"
android:startColor="@color/fx_mobile_layer_color_gradient_start"
android:type="linear" />
<size
android:width="256dp"
android:height="152dp" />
<corners
android:bottomLeftRadius="8dp"
android:bottomRightRadius="8dp"
android:topLeftRadius="8dp"
android:topRightRadius="8dp" />
</shape>

@ -1,8 +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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/fx_mobile_action_color_secondary" />
<corners android:radius="@dimen/tab_corner_radius"/>
</shape>

@ -1,74 +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:orientation="vertical">
<ImageView
android:layout_width="@dimen/cfr_triangle_width"
android:layout_height="@dimen/cfr_triangle_height"
android:layout_gravity="end"
android:layout_marginEnd="@dimen/cfr_triangle_margin_edge"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_cfr_triangle"
app:tint="@color/fx_mobile_layer_color_gradient_end" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/cfr_background_gradient">
<TextView
android:id="@+id/cfr_message"
style="@style/Header16TextStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:lineSpacingExtra="2dp"
android:text="@string/private_mode_cfr_message_2"
android:textColor="@color/fx_mobile_text_color_oncolor_primary"
app:layout_constraintBottom_toTopOf="@id/cfr_pos_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/cfr_pos_button"
style="@style/NeutralButton"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/rounded_gray_corners"
android:text="@string/private_mode_cfr_pos_button_text"
android:textAllCaps="false"
android:textColor="@color/photonDarkGrey50"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/cfr_neg_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cfr_message" />
<Button
android:id="@+id/cfr_neg_button"
style="@style/NeutralButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:text="@string/cfr_neg_button_text"
android:textAllCaps="false"
android:textColor="@color/fx_mobile_text_color_oncolor_primary"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cfr_pos_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
Loading…
Cancel
Save