1
0
Fork 0

For #9208: Adds in-product prompt to homescreen (#9836)

master
Sawyer Blatz 2020-04-22 14:29:43 -07:00 committed by GitHub
parent c9ea048431
commit dce16964c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 517 additions and 39 deletions

View File

@ -1044,6 +1044,50 @@ history:
- fenix-core@mozilla.com
expires: "2020-09-01"
tip:
displayed:
type: event
description: >
The tip was displayed
extra_keys:
identifier:
description: "The identifier of the tip displayed"
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9328
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/9836
notification_emails:
- fenix-core@mozilla.com
expires: "2020-09-01"
pressed:
type: event
description: >
The tip's button was pressed
extra_keys:
identifier:
description: "The identifier of the tip the action was taken on"
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9328
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/9836
notification_emails:
- fenix-core@mozilla.com
expires: "2020-09-01"
closed:
type: event
description: >
The tip was closed
extra_keys:
identifier:
description: "The identifier of the tip closed"
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9328
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/9836
notification_emails:
- fenix-core@mozilla.com
expires: "2020-09-01"
reader_mode:
available:
type: event

View File

@ -43,4 +43,9 @@ object FeatureFlags {
* Enables picture-in-picture feature
*/
val pictureInPicture = Config.channel.isNightlyOrDebug
/**
* Enables tip feature
*/
val tips = Config.channel.isDebug
}

View File

@ -10,16 +10,16 @@ import android.content.Intent
import androidx.core.net.toUri
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.amo.AddonCollectionProvider
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
import mozilla.components.feature.addons.migration.SupportedAddonsChecker
import mozilla.components.feature.addons.update.AddonUpdater
import mozilla.components.feature.addons.update.DefaultAddonUpdater
import mozilla.components.feature.addons.migration.SupportedAddonsChecker
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.ClipboardHandler
import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.wifi.WifiConnectionMonitor
import java.util.concurrent.TimeUnit

View File

@ -39,6 +39,7 @@ import org.mozilla.fenix.GleanMetrics.SearchWidget
import org.mozilla.fenix.GleanMetrics.SyncAccount
import org.mozilla.fenix.GleanMetrics.SyncAuth
import org.mozilla.fenix.GleanMetrics.Tab
import org.mozilla.fenix.GleanMetrics.Tip
import org.mozilla.fenix.GleanMetrics.ToolbarSettings
import org.mozilla.fenix.GleanMetrics.TopSites
import org.mozilla.fenix.GleanMetrics.TrackingProtection
@ -498,6 +499,18 @@ private val Event.wrapper: EventWrapper<*>?
is Event.AddonsOpenInToolbarMenu -> EventWrapper<NoExtraKeys>(
{ Addons.openAddonInToolbarMenu.record(it) }
)
is Event.TipDisplayed -> EventWrapper(
{ Tip.displayed.record(it) },
{ Tip.displayedKeys.valueOf(it) }
)
is Event.TipPressed -> EventWrapper(
{ Tip.pressed.record(it) },
{ Tip.pressedKeys.valueOf(it) }
)
is Event.TipClosed -> EventWrapper(
{ Tip.closed.record(it) },
{ Tip.closedKeys.valueOf(it) }
)
// Don't record other events in Glean:
is Event.AddBookmark -> null
is Event.OpenedBookmark -> null

View File

@ -32,6 +32,7 @@ import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.Library
import org.mozilla.fenix.GleanMetrics.Logins
import org.mozilla.fenix.GleanMetrics.SearchShortcuts
import org.mozilla.fenix.GleanMetrics.Tip
import org.mozilla.fenix.GleanMetrics.ToolbarSettings
import org.mozilla.fenix.GleanMetrics.TrackingProtection
import org.mozilla.fenix.R
@ -193,6 +194,21 @@ sealed class Event {
}
}
data class TipDisplayed(val identifier: String) : Event() {
override val extras: Map<Tip.displayedKeys, String>?
get() = hashMapOf(Tip.displayedKeys.identifier to identifier)
}
data class TipPressed(val identifier: String) : Event() {
override val extras: Map<Tip.pressedKeys, String>?
get() = hashMapOf(Tip.pressedKeys.identifier to identifier)
}
data class TipClosed(val identifier: String) : Event() {
override val extras: Map<Tip.closedKeys, String>?
get() = hashMapOf(Tip.closedKeys.identifier to identifier)
}
data class ToolbarPositionChanged(val position: Position) : Event() {
enum class Position { TOP, BOTTOM }

View File

@ -12,14 +12,14 @@ object MozillaProductDetector {
enum class MozillaProducts(val productName: String) {
// Browsers
FIREFOX("org.mozilla.firefox"),
FIREFOX_NIGHTLY("org.mozilla.fennec_aurora"),
FIREFOX_BETA("org.mozilla.firefox_beta"),
FIREFOX_AURORA("org.mozilla.fennec_aurora"),
FIREFOX_NIGHTLY("org.mozilla.fennec"),
FIREFOX_FDROID("org.mozilla.fennec_fdroid"),
FIREFOX_LITE("org.mozilla.rocket"),
REFERENCE_BROWSER("org.mozilla.reference.browser"),
REFERENCE_BROWSER_DEBUG("org.mozilla.reference.browser.debug"),
FENIX("org.mozilla.fenix"),
FENIX_NIGHTLY("org.mozilla.fenix.nightly"),
FOCUS("org.mozilla.focus"),
KLAR("org.mozilla.klar"),
@ -43,7 +43,7 @@ object MozillaProductDetector {
return mozillaProducts
}
private fun packageIsInstalled(context: Context, packageName: String): Boolean {
fun packageIsInstalled(context: Context, packageName: String): Boolean {
try {
context.packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {

View File

@ -0,0 +1,39 @@
/* 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.components.tips
import org.mozilla.fenix.FeatureFlags
sealed class TipType {
data class Button(val text: String, val action: () -> Unit) : TipType()
}
open class Tip(
val type: TipType,
val identifier: String,
val title: String,
val description: String,
val learnMoreURL: String?
)
interface TipProvider {
val tip: Tip?
val shouldDisplay: Boolean
}
interface TipManager {
fun getTip(): Tip?
}
class FenixTipManager(
private val providers: List<TipProvider>
) : TipManager {
override fun getTip(): Tip? {
if (!FeatureFlags.tips) { return null }
return providers
.firstOrNull { it.shouldDisplay }
?.tip
}
}

View File

@ -0,0 +1,109 @@
/* 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.components.tips.providers
import android.content.Context
import android.content.Intent
import android.net.Uri
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.MozillaProductDetector
import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts.FENIX
import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts.FENIX_NIGHTLY
import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts.FIREFOX_NIGHTLY
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.TipProvider
import org.mozilla.fenix.components.tips.TipType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.settings.SupportUtils
/**
* Tip explaining to users the migration of Fenix channels
*/
class MigrationTipProvider(private val context: Context) : TipProvider {
override val tip: Tip? =
when (context.packageName) {
FENIX.productName -> firefoxPreviewMovedTip()
FIREFOX_NIGHTLY.productName -> getNightlyMigrationTip()
FENIX_NIGHTLY.productName -> getNightlyMigrationTip()
else -> null
}
override val shouldDisplay: Boolean = context.settings().shouldDisplayFenixMovingTip()
private fun firefoxPreviewMovedTip(): Tip =
Tip(
type = TipType.Button(
text = context.getString(R.string.tip_firefox_preview_moved_button),
action = ::getFirefoxMovedButtonAction
),
identifier = getIdentifier(),
title = context.getString(R.string.tip_firefox_preview_moved_header),
description = context.getString(R.string.tip_firefox_preview_moved_description),
learnMoreURL = SupportUtils.getGenericSumoURLForTopic(SupportUtils.SumoTopic.FENIX_MOVING)
)
private fun firefoxPreviewMovedPreviewInstalledTip(): Tip =
Tip(
type = TipType.Button(
text = context.getString(R.string.tip_firefox_preview_moved_button_preview_installed),
action = ::getFirefoxMovedButtonAction
),
identifier = getIdentifier(),
title = context.getString(R.string.tip_firefox_preview_moved_header_preview_installed),
description = context.getString(R.string.tip_firefox_preview_moved_description_preview_installed),
learnMoreURL = SupportUtils.getGenericSumoURLForTopic(SupportUtils.SumoTopic.FENIX_MOVING)
)
private fun firefoxPreviewMovedPreviewNotInstalledTip(): Tip =
Tip(
type = TipType.Button(
text = context.getString(R.string.tip_firefox_preview_moved_button_preview_not_installed),
action = ::getFirefoxMovedButtonAction
),
identifier = getIdentifier(),
title = context.getString(R.string.tip_firefox_preview_moved_header_preview_not_installed),
description = context.getString(R.string.tip_firefox_preview_moved_description_preview_not_installed),
learnMoreURL = SupportUtils.getGenericSumoURLForTopic(SupportUtils.SumoTopic.FENIX_MOVING)
)
private fun getNightlyMigrationTip(): Tip? {
return if (MozillaProductDetector.packageIsInstalled(context, FENIX.productName)) {
firefoxPreviewMovedPreviewInstalledTip()
} else {
firefoxPreviewMovedPreviewNotInstalledTip()
}
}
private fun getFirefoxMovedButtonAction() {
when (context.packageName) {
FENIX.productName -> context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(SupportUtils.FIREFOX_BETA_PLAY_STORE_URL))
)
FIREFOX_NIGHTLY.productName -> getNightlyMigrationAction()
FENIX_NIGHTLY.productName -> getNightlyMigrationAction()
else -> { }
}
}
private fun getNightlyMigrationAction() {
return if (MozillaProductDetector.packageIsInstalled(context, FENIX.productName)) {
context.startActivity(context.packageManager.getLaunchIntentForPackage(FENIX.productName))
} else {
context.startActivity(Intent(
Intent.ACTION_VIEW, Uri.parse(SupportUtils.FIREFOX_NIGHTLY_PLAY_STORE_URL)
))
}
}
private fun getIdentifier(): String {
return when (context.packageName) {
FENIX.productName -> context.getString(R.string.pref_key_migrating_from_fenix_tip)
FIREFOX_NIGHTLY.productName -> context.getString(R.string.pref_key_migrating_from_firefox_nightly_tip)
FENIX_NIGHTLY.productName -> context.getString(R.string.pref_key_migrating_from_fenix_nightly_tip)
else -> { "" }
}
}
}

View File

@ -77,6 +77,8 @@ import org.mozilla.fenix.components.PrivateShortcutCreateManager
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.tips.FenixTipManager
import org.mozilla.fenix.components.tips.providers.MigrationTipProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.metrics
@ -199,7 +201,8 @@ class HomeFragment : Fragment() {
expandedCollections = emptySet(),
mode = currentMode.getCurrentMode(),
tabs = emptyList(),
topSites = requireComponents.core.topSiteStorage.cachedTopSites
topSites = requireComponents.core.topSiteStorage.cachedTopSites,
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip()
)
)
}
@ -379,7 +382,8 @@ class HomeFragment : Fragment() {
collections = components.core.tabCollectionStorage.cachedTabCollections,
mode = currentMode.getCurrentMode(),
tabs = getListOfSessions().toTabs(),
topSites = components.core.topSiteStorage.cachedTopSites
topSites = components.core.topSiteStorage.cachedTopSites,
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip()
)
)

View File

@ -13,6 +13,7 @@ import mozilla.components.feature.top.sites.TopSite
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.components.tips.Tip
/**
* The [Store] for holding the [HomeFragmentState] and applying [HomeFragmentAction]s.
@ -58,7 +59,8 @@ data class HomeFragmentState(
val expandedCollections: Set<Long>,
val mode: Mode,
val tabs: List<Tab>,
val topSites: List<TopSite>
val topSites: List<TopSite>,
val tip: Tip? = null
) : State
sealed class HomeFragmentAction : Action {
@ -66,7 +68,8 @@ sealed class HomeFragmentAction : Action {
val tabs: List<Tab>,
val topSites: List<TopSite>,
val mode: Mode,
val collections: List<TabCollection>
val collections: List<TabCollection>,
val tip: Tip? = null
) :
HomeFragmentAction()
@ -77,6 +80,7 @@ sealed class HomeFragmentAction : Action {
data class ModeChange(val mode: Mode, val tabs: List<Tab> = emptyList()) : HomeFragmentAction()
data class TabsChange(val tabs: List<Tab>) : HomeFragmentAction()
data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction()
data class RemoveTip(val tip: Tip) : HomeFragmentAction()
}
private fun homeFragmentStateReducer(
@ -88,7 +92,8 @@ private fun homeFragmentStateReducer(
collections = action.collections,
mode = action.mode,
tabs = action.tabs,
topSites = action.topSites
topSites = action.topSites,
tip = action.tip
)
is HomeFragmentAction.CollectionExpanded -> {
val newExpandedCollection = state.expandedCollections.toMutableSet()
@ -105,5 +110,6 @@ private fun homeFragmentStateReducer(
is HomeFragmentAction.ModeChange -> state.copy(mode = action.mode, tabs = action.tabs)
is HomeFragmentAction.TabsChange -> state.copy(tabs = action.tabs)
is HomeFragmentAction.TopSitesChange -> state.copy(topSites = action.topSites)
is HomeFragmentAction.RemoveTip -> { state.copy(tip = null) }
}
}

View File

@ -17,16 +17,17 @@ import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.tab_list_row.*
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.home.OnboardingState
import org.mozilla.fenix.home.Tab
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoContentMessageViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoContentMessageWithActionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.SaveTabGroupViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteViewHolder
@ -41,10 +42,12 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTh
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingToolbarPositionPickerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTrackingProtectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingWhatsNewViewHolder
import org.mozilla.fenix.home.tips.ButtonTipViewHolder
import mozilla.components.feature.tab.collections.Tab as ComponentTab
sealed class AdapterItem(@LayoutRes val viewType: Int) {
data class TipItem(val tip: Tip) : AdapterItem(
ButtonTipViewHolder.LAYOUT_ID)
data class TabHeader(val isPrivate: Boolean, val hasTabs: Boolean) : AdapterItem(TabHeaderViewHolder.LAYOUT_ID)
data class TabItem(val tab: Tab) : AdapterItem(TabViewHolder.LAYOUT_ID) {
override fun sameAs(other: AdapterItem) = other is TabItem && tab.sessionId == other.tab.sessionId
@ -164,6 +167,7 @@ class SessionControlAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
ButtonTipViewHolder.LAYOUT_ID -> ButtonTipViewHolder(view, interactor)
TabHeaderViewHolder.LAYOUT_ID -> TabHeaderViewHolder(view, interactor)
TopSiteHeaderViewHolder.LAYOUT_ID -> TopSiteHeaderViewHolder(view)
TabViewHolder.LAYOUT_ID -> TabViewHolder(view, interactor)
@ -196,6 +200,10 @@ class SessionControlAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
when (holder) {
is ButtonTipViewHolder -> {
val tipItem = item as AdapterItem.TipItem
holder.bind(tipItem.tip)
}
is TabHeaderViewHolder -> {
val tabHeader = item as AdapterItem.TabHeader
holder.bind(tabHeader.isPrivate, tabHeader.hasTabs)

View File

@ -35,6 +35,7 @@ import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.Tab
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.settings.SupportUtils
import mozilla.components.feature.tab.collections.Tab as ComponentTab
@ -163,6 +164,8 @@ interface SessionControlController {
* @see [TabSessionInteractor.onOpenNewTabClicked]
*/
fun handleonOpenNewTabClicked()
fun handleCloseTip(tip: Tip)
}
@SuppressWarnings("TooManyFunctions", "LargeClass")
@ -386,6 +389,10 @@ class DefaultSessionControlController(
openSearchScreen()
}
override fun handleCloseTip(tip: Tip) {
fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
}
private fun showCollectionCreationFragment(
step: SaveCollectionStep,
selectedTabIds: Array<String>? = null,

View File

@ -8,6 +8,7 @@ import android.view.View
import mozilla.components.feature.tab.collections.Tab
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.components.tips.Tip
/**
* Interface for collection related actions in the [SessionControlInteractor].
@ -104,6 +105,13 @@ interface OnboardingInteractor {
fun onReadPrivacyNoticeClicked()
}
interface TipInteractor {
/**
* Dismisses the tip view adapter
*/
fun onCloseTip(tip: Tip)
}
/**
* Interface for tab related actions in the [SessionControlInteractor].
*/
@ -205,7 +213,7 @@ interface TopSiteInteractor {
@SuppressWarnings("TooManyFunctions")
class SessionControlInteractor(
private val controller: SessionControlController
) : CollectionInteractor, OnboardingInteractor, TabSessionInteractor, TopSiteInteractor {
) : CollectionInteractor, OnboardingInteractor, TabSessionInteractor, TopSiteInteractor, TipInteractor {
override fun onCloseTab(sessionId: String) {
controller.handleCloseTab(sessionId)
}
@ -301,4 +309,8 @@ class SessionControlInteractor(
override fun onOpenNewTabClicked() {
controller.handleonOpenNewTabClicked()
}
override fun onCloseTip(tip: Tip) {
controller.handleCloseTip(tip)
}
}

View File

@ -22,6 +22,7 @@ import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.OnboardingState
import org.mozilla.fenix.home.Tab
import org.mozilla.fenix.components.tips.Tip
val noTabMessage = AdapterItem.NoContentMessageWithAction(
R.string.no_open_tabs_header_2,
@ -39,10 +40,13 @@ private fun normalModeAdapterItems(
tabs: List<Tab>,
topSites: List<TopSite>,
collections: List<TabCollection>,
expandedCollections: Set<Long>
expandedCollections: Set<Long>,
tip: Tip?
): List<AdapterItem> {
val items = mutableListOf<AdapterItem>()
tip?.let { items.add(AdapterItem.TipItem(it)) }
if (topSites.isNotEmpty()) {
items.add(AdapterItem.TopSiteHeader)
items.add(AdapterItem.TopSiteList(topSites))
@ -150,7 +154,7 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List<Adapt
}
private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
is Mode.Normal -> normalModeAdapterItems(tabs, topSites, collections, expandedCollections)
is Mode.Normal -> normalModeAdapterItems(tabs, topSites, collections, expandedCollections, tip)
is Mode.Private -> privateModeAdapterItems(tabs)
is Mode.Onboarding -> onboardingAdapterItems(mode.state)
}

View File

@ -0,0 +1,78 @@
package org.mozilla.fenix.home.tips
import android.text.SpannableString
import android.text.style.UnderlineSpan
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.button_tip_item.view.*
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.TipType
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
class ButtonTipViewHolder(
val view: View,
val interactor: SessionControlInteractor
) : RecyclerView.ViewHolder(view) {
var tip: Tip? = null
fun bind(tip: Tip) {
require(tip.type is TipType.Button)
this.tip = tip
view.apply {
context.components.analytics.metrics.track(Event.TipDisplayed(tip.identifier))
tip_header_text.text = tip.title
tip_description_text.text = tip.description
tip_button.text = tip.type.text
if (tip.learnMoreURL == null) {
tip_learn_more.visibility = View.GONE
} else {
val learnMoreText = context.getString(R.string.search_suggestions_onboarding_learn_more_link)
val textWithLink = SpannableString(learnMoreText).apply {
setSpan(UnderlineSpan(), 0, learnMoreText.length, 0)
}
tip_learn_more.text = textWithLink
tip_learn_more.setOnClickListener {
(context as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = tip.learnMoreURL,
newTab = true,
from = BrowserDirection.FromHome
)
}
}
tip_button.setOnClickListener {
tip.type.action.invoke()
context.components.analytics.metrics.track(
Event.TipPressed(tip.identifier)
)
}
tip_close.setOnClickListener {
context.components.analytics.metrics.track(Event.TipClosed(tip.identifier))
context.settings().preferences
.edit()
.putBoolean(tip.identifier, false)
.apply()
interactor.onCloseTip(tip)
}
}
}
companion object {
const val LAYOUT_ID = R.layout.button_tip_item
}
}

View File

@ -21,6 +21,8 @@ import org.mozilla.fenix.ext.showToolbar
/**
* Lets the user customize the UI.
*/
@Suppress("TooManyFunctions")
class CustomizationFragment : PreferenceFragmentCompat() {
private lateinit var radioLightTheme: RadioButtonPreference
private lateinit var radioDarkTheme: RadioButtonPreference

View File

@ -42,6 +42,7 @@ class PrivateBrowsingFragment : PreferenceFragmentCompat() {
findPreference<SwitchPreference>(getPreferenceKey(R.string.pref_key_open_links_in_a_private_tab))?.apply {
onPreferenceChangeListener = SharedPreferenceUpdater()
isChecked = context.settings().openLinksInAPrivateTab
}
findPreference<SwitchPreference>(getPreferenceKey

View File

@ -21,13 +21,15 @@ import java.util.Locale
object SupportUtils {
const val RATE_APP_URL = "market://details?id=" + BuildConfig.APPLICATION_ID
const val MOZILLA_MANIFESTO_URL = "https://www.mozilla.org/en-GB/about/manifesto/"
const val POCKET_TRENDING_URL = "https://getpocket.com/fenix-top-articles"
const val WIKIPEDIA_URL = "https://www.wikipedia.org/"
const val FENIX_PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=${BuildConfig.APPLICATION_ID}"
const val FIREFOX_BETA_PLAY_STORE_URL = "market://details?id=org.mozilla.firefox_beta"
const val FIREFOX_NIGHTLY_PLAY_STORE_URL = "market://details?id=org.mozilla.fennec_aurora"
const val YOUTUBE_URL = "https://www.youtube.com/"
enum class SumoTopic(internal val topicStr: String) {
FENIX_MOVING("sync-delist"),
HELP("faq-android"),
PRIVATE_BROWSING_MYTHS("common-myths-about-private-browsing"),
YOUR_RIGHTS("your-rights"),

View File

@ -137,6 +137,12 @@ class Settings private constructor(
default = true
)
// If any of the prefs have been modified, quit displaying the fenix moved tip
fun shouldDisplayFenixMovingTip(): Boolean =
preferences.getBoolean(appContext.getString(R.string.pref_key_migrating_from_fenix_nightly_tip), true) &&
preferences.getBoolean(appContext.getString(R.string.pref_key_migrating_from_firefox_nightly_tip), true) &&
preferences.getBoolean(appContext.getString(R.string.pref_key_migrating_from_fenix_tip), true)
var defaultSearchEngineName by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_search_engine),
default = ""
@ -250,6 +256,11 @@ class Settings private constructor(
default = true
)
fun isDefaultBrowser(): Boolean {
val browsers = BrowsersCache.all(appContext)
return browsers.isDefaultBrowser
}
val shouldUseAutoBatteryTheme by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_auto_battery_theme),
default = false

View File

@ -5,8 +5,8 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="225"
android:endColor="#B833E1"
android:angle="45"
android:endColor="#AB71FF"
android:startColor="#7542E5"
android:type="linear" />
<size

View File

@ -0,0 +1,81 @@
<?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/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tip_card"
android:background="@drawable/cfr_background_gradient"
style="@style/OnboardingCardLight"
android:padding="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tip_header_text"
android:layout_width="0dp"
android:maxLines="2"
android:lineSpacingExtra="2dp"
android:layout_height="wrap_content"
android:textColor="@color/primary_text_dark_theme"
android:textAppearance="@style/HeaderTextStyle"
android:gravity="center_vertical"
tools:text="Header text"
android:layout_margin="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ImageButton
android:id="@+id/tip_close"
android:tint="@color/primary_text_dark_theme"
app:srcCompat="@drawable/ic_close"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/create_collection_close"
android:layout_width="48dp"
android:layout_height="48dp" />
<TextView
android:id="@+id/tip_description_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingExtra="2dp"
android:textAppearance="@style/Body14TextStyle"
android:textColor="@color/primary_text_dark_theme"
tools:text="Tip description"
android:layout_marginTop="8dp"
android:layout_marginHorizontal="16dp"
app:layout_constraintTop_toBottomOf="@id/tip_header_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/tip_learn_more"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/search_suggestions_onboarding_learn_more_link"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/primary_text_dark_theme"
app:layout_constraintEnd_toEndOf="@id/tip_description_text"
app:layout_constraintStart_toStartOf="@id/tip_description_text"
app:layout_constraintTop_toBottomOf="@id/tip_description_text"
tools:textColor="@color/accent_high_contrast_private_theme"/>
<Button
style="@style/NeutralButton"
android:id="@+id/tip_button"
android:layout_marginTop="8dp"
tools:text="Call to action"
android:layout_height="36dp"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintTop_toBottomOf="@id/tip_learn_more"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,13 +6,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:paddingStart="8dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:layout_marginBottom="8dp"
android:paddingEnd="?android:attr/scrollbarSize">

View File

@ -19,7 +19,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/pbm_shortcut_popup_background">
android:background="@drawable/cfr_background_gradient">
<TextView
android:id="@+id/cfr_message"
@ -38,21 +38,22 @@
app:fontFamily="@font/metropolis_medium" />
<Button
android:id="@+id/cfr_pos_button"
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/cfr_pos_button_text"
android:textAllCaps="false"
android:textColor="@color/above_dark_theme"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cfr_message"
style="@style/MetropolisButton"/>
android:id="@+id/cfr_pos_button"
style="@style/MetropolisButton"
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/cfr_pos_button_text"
android:textAllCaps="false"
android:textColor="@color/above_dark_theme"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cfr_message" />
<Button
android:id="@+id/cfr_neg_button"

View File

@ -7,7 +7,7 @@
android:id="@+id/top_site_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginBottom="8dp"
android:padding="4dp"
android:paddingBottom="6dp">

View File

@ -160,4 +160,9 @@
<string name="pref_key_top_sites_size" translatable="false">pref_key_top_sites_size</string>
<string name="pref_key_show_first_time_pwa" translatable="false">pref_key_show_first_time_pwa</string>
<string name="pref_key_migrating_from_fenix_nightly_tip" translatable="false">pref_key_migrating_from_fenix_nightly_tip</string>
<string name="pref_key_migrating_from_firefox_nightly_tip" translatable="false">pref_key_migrating_from_firefox_nightly_tip</string>
<string name="pref_key_migrating_from_fenix_tip" translatable="false">pref_key_migrating_from_fenix_tip</string>
</resources>

View File

@ -842,6 +842,34 @@
<!-- Text for the snackbar to show the user that the deletion of browsing data is in progress -->
<string name="deleting_browsing_data_in_progress">Deleting browsing data&#8230;</string>
<!-- Tips -->
<!-- text for firefox preview moving tip header "Firefox Preview" and "Firefox Nightly" are intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header">Firefox Preview is now Firefox Nightly</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description">
Firefox Nightly gets updated every night and has experimental new features.
However, it may be less stable. Download our beta browser for a more stable experience.</string>
<!-- text for firefox preview moving tip button. "Mozilla Firefox Browser" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_button">Get Mozilla Firefox Browser</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_installed">Firefox Nightly has moved</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description_preview_installed">
This app will no longer receive security updates. Stop using this app and switch to the new Nightly.
\n\nTo transfer your bookmarks, logins, and history to another app, create a Firefox account.</string>
<!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_installed">Switch to the new Nightly</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_not_installed">Firefox Nightly has moved</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description_preview_not_installed">
This app will no longer receive security updates. Get the new Nightly and stop using this app.
\n\nTo transfer your bookmarks, logins, and history to another app, create a Firefox account.</string>
<!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_not_installed">Get the new Nightly</string>
<!-- Onboarding -->
<!-- Text for onboarding welcome message
The first parameter is the name of the app (e.g. Firefox Preview) -->

View File

@ -177,6 +177,9 @@ The following metrics are added to the ping:
| sync_auth.sign_up |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |User registered a new Firefox Account, and was signed into it |[1](https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300)||2020-09-01 |
| tab.media_pause |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the pause icon on a tab from the home screen |[1](https://github.com/mozilla-mobile/fenix/pull/5266)||2020-09-01 |
| tab.media_play |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the play icon on a tab from the home screen |[1](https://github.com/mozilla-mobile/fenix/pull/5266)||2020-09-01 |
| tip.closed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The tip was closed |[1](https://github.com/mozilla-mobile/fenix/pull/9836)|<ul><li>identifier: The identifier of the tip closed</li></ul>|2020-09-01 |
| tip.displayed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The tip was displayed |[1](https://github.com/mozilla-mobile/fenix/pull/9836)|<ul><li>identifier: The identifier of the tip displayed</li></ul>|2020-09-01 |
| tip.pressed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The tip's button was pressed |[1](https://github.com/mozilla-mobile/fenix/pull/9836)|<ul><li>identifier: The identifier of the tip the action was taken on</li></ul>|2020-09-01 |
| toolbar_settings.changed_position |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The user selected a new position for the toolbar |[1](https://github.com/mozilla-mobile/fenix/pull/6608)|<ul><li>position: A string that indicates the new position of the toolbar TOP or BOTTOM</li></ul>|2020-09-01 |
| top_sites.open_in_new_tab |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opens a new tab based on a top site item |[1](https://github.com/mozilla-mobile/fenix/pull/7523)||2020-09-01 |
| top_sites.open_in_private_tab |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opens a new private tab based on a top site item |[1](https://github.com/mozilla-mobile/fenix/pull/7523)||2020-09-01 |