diff --git a/app/metrics.yaml b/app/metrics.yaml index d69a9a903..d52506f7b 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 556bb309e..14b23df85 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -43,4 +43,9 @@ object FeatureFlags { * Enables picture-in-picture feature */ val pictureInPicture = Config.channel.isNightlyOrDebug + + /** + * Enables tip feature + */ + val tips = Config.channel.isDebug } diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index d76e89ac6..b436d7c42 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index cf9b2e113..750c314c4 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -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( { 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 diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index 0f7d2580f..308429029 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -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? + get() = hashMapOf(Tip.displayedKeys.identifier to identifier) + } + + data class TipPressed(val identifier: String) : Event() { + override val extras: Map? + get() = hashMapOf(Tip.pressedKeys.identifier to identifier) + } + + data class TipClosed(val identifier: String) : Event() { + override val extras: Map? + get() = hashMapOf(Tip.closedKeys.identifier to identifier) + } + data class ToolbarPositionChanged(val position: Position) : Event() { enum class Position { TOP, BOTTOM } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt index 2aeb830a8..8d5c80c98 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt @@ -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) { diff --git a/app/src/main/java/org/mozilla/fenix/components/tips/TipManager.kt b/app/src/main/java/org/mozilla/fenix/components/tips/TipManager.kt new file mode 100644 index 000000000..1c0769db7 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/tips/TipManager.kt @@ -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 +) : TipManager { + override fun getTip(): Tip? { + if (!FeatureFlags.tips) { return null } + return providers + .firstOrNull { it.shouldDisplay } + ?.tip + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/tips/providers/MigrationTipProvider.kt b/app/src/main/java/org/mozilla/fenix/components/tips/providers/MigrationTipProvider.kt new file mode 100644 index 000000000..915265fef --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/tips/providers/MigrationTipProvider.kt @@ -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 -> { "" } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index e70cb9e3f..db85ba7f8 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -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() ) ) diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt index 0b1c0bacb..f9f9cea1b 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt @@ -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, val mode: Mode, val tabs: List, - val topSites: List + val topSites: List, + val tip: Tip? = null ) : State sealed class HomeFragmentAction : Action { @@ -66,7 +68,8 @@ sealed class HomeFragmentAction : Action { val tabs: List, val topSites: List, val mode: Mode, - val collections: List + val collections: List, + val tip: Tip? = null ) : HomeFragmentAction() @@ -77,6 +80,7 @@ sealed class HomeFragmentAction : Action { data class ModeChange(val mode: Mode, val tabs: List = emptyList()) : HomeFragmentAction() data class TabsChange(val tabs: List) : HomeFragmentAction() data class TopSitesChange(val topSites: List) : 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) } } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index 7891fce97..1a98cf592 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -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) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt index e568c1457..549059508 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -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? = null, diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index c6725b934..1b52a3b37 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -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) + } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index 8d6a5e24b..3b2a4a20c 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -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, topSites: List, collections: List, - expandedCollections: Set + expandedCollections: Set, + tip: Tip? ): List { val items = mutableListOf() + 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 = 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) } diff --git a/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt new file mode 100644 index 000000000..a487f6655 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt @@ -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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt index 63d92459b..edd608d28 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/settings/PrivateBrowsingFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/PrivateBrowsingFragment.kt index ff1dc9b07..deaf0b67c 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/PrivateBrowsingFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/PrivateBrowsingFragment.kt @@ -42,6 +42,7 @@ class PrivateBrowsingFragment : PreferenceFragmentCompat() { findPreference(getPreferenceKey(R.string.pref_key_open_links_in_a_private_tab))?.apply { onPreferenceChangeListener = SharedPreferenceUpdater() + isChecked = context.settings().openLinksInAPrivateTab } findPreference(getPreferenceKey diff --git a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt index c34ddbc7b..6f2c35af1 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt @@ -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"), diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 5b15f81e6..886c4a773 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -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 diff --git a/app/src/main/res/drawable/pbm_shortcut_popup_background.xml b/app/src/main/res/drawable/cfr_background_gradient.xml similarity index 91% rename from app/src/main/res/drawable/pbm_shortcut_popup_background.xml rename to app/src/main/res/drawable/cfr_background_gradient.xml index 1d2e0153a..5fa7048f0 100644 --- a/app/src/main/res/drawable/pbm_shortcut_popup_background.xml +++ b/app/src/main/res/drawable/cfr_background_gradient.xml @@ -5,8 +5,8 @@ + + + + + + + + + + + +