diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt
index c4095ca8c..c81d53f92 100644
--- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt
+++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt
@@ -20,5 +20,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromSettings(R.id.settingsFragment),
FromBookmarks(R.id.bookmarkFragment),
FromHistory(R.id.historyFragment),
- FromExceptions(R.id.exceptionsFragment)
+ FromExceptions(R.id.exceptionsFragment),
+ FromAbout(R.id.aboutFragment)
}
diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
index 5efbdc96f..96d32656a 100644
--- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
+++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
@@ -53,6 +53,7 @@ import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.search.SearchFragmentDirections
+import org.mozilla.fenix.settings.AboutFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.share.ShareFragment
import org.mozilla.fenix.theme.DefaultThemeManager
@@ -233,6 +234,8 @@ open class HomeActivity : AppCompatActivity(), ShareFragment.TabsSharedCallback
ExceptionsFragmentDirections.actionExceptionsFragmentToBrowserFragment(
customTabSessionId
)
+ BrowserDirection.FromAbout ->
+ AboutFragmentDirections.actionAboutFragmentToBrowserFragment(customTabSessionId)
}
private fun load(
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 44073cc4f..e7b77a2b1 100644
--- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt
+++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt
@@ -84,6 +84,7 @@ import org.mozilla.fenix.share.ShareTab
import org.mozilla.fenix.utils.FragmentPreDrawManager
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.utils.allowUndo
+import org.mozilla.fenix.whatsnew.WhatsNew
@SuppressWarnings("TooManyFunctions", "LargeClass")
class HomeFragment : Fragment(), AccountObserver {
@@ -572,6 +573,19 @@ class HomeFragment : Fragment(), AccountObserver {
from = BrowserDirection.FromHome
)
}
+ HomeMenu.Item.WhatsNew -> {
+ invokePendingDeleteJobs()
+ hideOnboardingIfNeeded()
+ WhatsNew.userViewedWhatsNew(context!!)
+ (activity as HomeActivity).openToBrowserAndLoad(
+ searchTermOrURL = SupportUtils.getSumoURLForTopic(
+ context!!,
+ SupportUtils.SumoTopic.WHATS_NEW
+ ),
+ newTab = true,
+ from = BrowserDirection.FromHome
+ )
+ }
}
}
}
diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt b/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt
index 0e925636f..ebfc7d727 100644
--- a/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt
+++ b/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt
@@ -5,17 +5,21 @@
package org.mozilla.fenix.home
import android.content.Context
+import androidx.core.content.ContextCompat
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.BrowserMenuDivider
+import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuImageText
import org.mozilla.fenix.R
import org.mozilla.fenix.theme.ThemeManager
+import org.mozilla.fenix.whatsnew.WhatsNew
class HomeMenu(
private val context: Context,
private val onItemTapped: (Item) -> Unit = {}
) {
sealed class Item {
+ object WhatsNew : Item()
object Help : Item()
object Settings : Item()
object Library : Item()
@@ -30,7 +34,7 @@ class HomeMenu(
R.drawable.ic_settings,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
- onItemTapped.invoke(HomeMenu.Item.Settings)
+ onItemTapped.invoke(Item.Settings)
},
BrowserMenuImageText(
@@ -38,7 +42,7 @@ class HomeMenu(
R.drawable.ic_library,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
- onItemTapped.invoke(HomeMenu.Item.Library)
+ onItemTapped.invoke(Item.Library)
},
BrowserMenuDivider(),
@@ -47,7 +51,21 @@ class HomeMenu(
R.drawable.ic_help,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
- onItemTapped.invoke(HomeMenu.Item.Help)
- })
+ onItemTapped.invoke(Item.Help)
+ },
+
+ BrowserMenuHighlightableItem(
+ context.getString(R.string.browser_menu_whats_new),
+ R.drawable.ic_whats_new,
+ highlight = BrowserMenuHighlightableItem.Highlight(
+ startImageResource = R.drawable.ic_whats_new_notification,
+ backgroundResource = ThemeManager.resolveAttribute(R.attr.selectableItemBackground, context),
+ colorResource = ContextCompat.getColor(context, R.color.whats_new_notification_color)
+ ),
+ isHighlighted = { WhatsNew.shouldHighlightWhatsNew(context) }
+ ) {
+ onItemTapped.invoke(Item.WhatsNew)
+ }
+ )
}
}
diff --git a/app/src/main/java/org/mozilla/fenix/settings/AboutFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/AboutFragment.kt
index 0b4a53658..c5a03a84b 100644
--- a/app/src/main/java/org/mozilla/fenix/settings/AboutFragment.kt
+++ b/app/src/main/java/org/mozilla/fenix/settings/AboutFragment.kt
@@ -17,8 +17,11 @@ import androidx.core.content.pm.PackageInfoCompat
import androidx.fragment.app.Fragment
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import kotlinx.android.synthetic.main.fragment_about.*
+import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BuildConfig
+import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
+import org.mozilla.fenix.whatsnew.WhatsNew
import org.mozilla.geckoview.BuildConfig as GeckoViewBuildConfig
/**
@@ -70,6 +73,21 @@ class AboutFragment : Fragment() {
startActivity(Intent(context, OssLicensesMenuActivity::class.java))
OssLicensesMenuActivity.setActivityTitle(getString(R.string.open_source_licenses_title, appName))
}
+
+ with(whats_new_button) {
+ text = getString(R.string.about_whats_new, getString(R.string.app_name))
+ setOnClickListener {
+ WhatsNew.userViewedWhatsNew(context!!)
+ (activity as HomeActivity).openToBrowserAndLoad(
+ searchTermOrURL = SupportUtils.getSumoURLForTopic(
+ context!!,
+ SupportUtils.SumoTopic.WHATS_NEW
+ ),
+ newTab = true,
+ from = BrowserDirection.FromAbout
+ )
+ }
+ }
}
companion object {
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 54c6699b7..40ab0126f 100644
--- a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt
+++ b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt
@@ -29,7 +29,8 @@ object SupportUtils {
HELP("faq-android"),
PRIVATE_BROWSING_MYTHS("common-myths-about-private-browsing"),
YOUR_RIGHTS("your-rights"),
- TRACKING_PROTECTION("tracking-protection-firefox-preview")
+ TRACKING_PROTECTION("tracking-protection-firefox-preview"),
+ WHATS_NEW("whats-new-firefox-preview")
}
fun getSumoURLForTopic(context: Context, topic: SumoTopic): String {
diff --git a/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNew.kt b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNew.kt
new file mode 100644
index 000000000..b13b625e1
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNew.kt
@@ -0,0 +1,97 @@
+package org.mozilla.fenix.whatsnew
+
+/* 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/. */
+
+import android.content.Context
+
+// This file is a modified port from Focus Android
+
+/**
+ * Helper class tracking whether the application was recently updated in order to show "What's new"
+ * menu items and indicators in the application UI.
+ *
+ * The application is considered updated when the application's version name changes (versionName
+ * in the manifest). The applications version code would be a good candidates too, but it might
+ * change more often (RC builds) without the application actually changing from the user's point
+ * of view.
+ *
+ * Whenever the application was updated we still consider the application to be "recently updated"
+ * for the next few days.
+ */
+class WhatsNew private constructor(private val storage: WhatsNewStorage) {
+
+ private fun hasBeenUpdatedRecently(currentVersion: WhatsNewVersion): Boolean {
+ val lastKnownAppVersion = storage.getVersion()
+
+ // Update the version and date if *just* updated
+ lastKnownAppVersion?.let {
+ if (currentVersion.majorVersionNumber > it.majorVersionNumber) {
+ storage.setVersion(currentVersion)
+ storage.setDateOfUpdate(System.currentTimeMillis())
+ return true
+ }
+ }
+
+ return (!storage.getWhatsNewHasBeenCleared() && storage.getDaysSinceUpdate() < DAYS_PER_UPDATE)
+ }
+
+ companion object {
+ /**
+ * How many days do we consider the app to be updated?
+ */
+ private const val DAYS_PER_UPDATE = 3
+
+ internal var wasUpdatedRecently: Boolean? = null
+
+ /**
+ * Should we highlight the "What's new" menu item because this app been updated recently?
+ *
+ * This method returns true either if this is the first start of the application since it
+ * was updated or this is a later start but still recent enough to consider the app to be
+ * updated recently.
+ */
+ @JvmStatic
+ fun shouldHighlightWhatsNew(currentVersion: WhatsNewVersion, storage: WhatsNewStorage): Boolean {
+ // Cache the value for the lifetime of this process (or until userViewedWhatsNew() is called)
+ if (wasUpdatedRecently == null) {
+ val whatsNew = WhatsNew(storage)
+ wasUpdatedRecently = whatsNew.hasBeenUpdatedRecently(currentVersion)
+ }
+
+ return wasUpdatedRecently!!
+ }
+
+ /**
+ * Convenience function to run from the context.
+ */
+ fun shouldHighlightWhatsNew(context: Context): Boolean {
+ return shouldHighlightWhatsNew(
+ ContextWhatsNewVersion(context),
+ SharedPreferenceWhatsNewStorage(context)
+ )
+ }
+
+ /**
+ * Reset the "updated" state and continue as if the app was not updated recently.
+ */
+ @JvmStatic
+ private fun userViewedWhatsNew(storage: WhatsNewStorage) {
+ wasUpdatedRecently = false
+ storage.setWhatsNewHasBeenCleared(true)
+ }
+
+ /**
+ * Convenience function to run from the context.
+ */
+ @JvmStatic
+ fun userViewedWhatsNew(context: Context) {
+ userViewedWhatsNew(
+ SharedPreferenceWhatsNewStorage(
+ context
+ )
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewStorage.kt b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewStorage.kt
new file mode 100644
index 000000000..8dea8acbf
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewStorage.kt
@@ -0,0 +1,69 @@
+package org.mozilla.fenix.whatsnew
+
+/* 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/. */
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.preference.PreferenceManager
+import java.util.concurrent.TimeUnit
+
+// This file is a modified port from Focus Android
+
+/**
+ * Interface to abstract where the cached version and session counter is stored
+ */
+interface WhatsNewStorage {
+ fun getVersion(): WhatsNewVersion?
+ fun setVersion(version: WhatsNewVersion)
+ fun getWhatsNewHasBeenCleared(): Boolean
+ fun setWhatsNewHasBeenCleared(cleared: Boolean)
+ fun getDaysSinceUpdate(): Long
+ fun setDateOfUpdate(day: Long)
+
+ companion object {
+ internal const val PREFERENCE_KEY_APP_NAME = "whatsnew-lastKnownAppVersionName"
+ internal const val PREFERENCE_KEY_WHATS_NEW_CLEARED = "whatsnew-cleared"
+ internal const val PREFERENCE_KEY_UPDATE_DAY = "whatsnew-lastKnownAppVersionUpdateDay"
+ }
+}
+
+class SharedPreferenceWhatsNewStorage(private val sharedPreference: SharedPreferences) :
+ WhatsNewStorage {
+
+ constructor(context: Context) : this(PreferenceManager.getDefaultSharedPreferences(context))
+
+ override fun getVersion(): WhatsNewVersion? {
+ return sharedPreference.getString(WhatsNewStorage.PREFERENCE_KEY_APP_NAME, null)?.let {
+ WhatsNewVersion(it)
+ }
+ }
+
+ override fun setVersion(version: WhatsNewVersion) {
+ sharedPreference.edit()
+ .putString(WhatsNewStorage.PREFERENCE_KEY_APP_NAME, version.version)
+ .apply()
+ }
+
+ override fun getWhatsNewHasBeenCleared(): Boolean {
+ return sharedPreference.getBoolean(WhatsNewStorage.PREFERENCE_KEY_WHATS_NEW_CLEARED, false)
+ }
+
+ override fun setWhatsNewHasBeenCleared(cleared: Boolean) {
+ sharedPreference.edit()
+ .putBoolean(WhatsNewStorage.PREFERENCE_KEY_WHATS_NEW_CLEARED, cleared)
+ .apply()
+ }
+
+ override fun getDaysSinceUpdate(): Long {
+ val updateDay = sharedPreference.getLong(WhatsNewStorage.PREFERENCE_KEY_UPDATE_DAY, 0)
+ return TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - updateDay)
+ }
+
+ override fun setDateOfUpdate(day: Long) {
+ sharedPreference.edit()
+ .putLong(WhatsNewStorage.PREFERENCE_KEY_UPDATE_DAY, day)
+ .apply()
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewVersion.kt b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewVersion.kt
new file mode 100644
index 000000000..8bb4702ee
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewVersion.kt
@@ -0,0 +1,37 @@
+package org.mozilla.fenix.whatsnew
+/* 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/. */
+
+import android.content.Context
+import mozilla.components.support.ktx.android.content.appVersionName
+
+// This file is a modified port from Focus Android
+
+/**
+ * Convenience class to deal with the application version number
+ * I opted to keep it contained to the whatsnew package. We may
+ * want to pull it
+ */
+open class WhatsNewVersion(internal open val version: String) {
+
+ override fun hashCode(): Int {
+ return version.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other is WhatsNewVersion) {
+ return version == other.version
+ }
+
+ return false
+ }
+
+ val majorVersionNumber: Int
+ get() = version.split(".").first().toInt()
+}
+
+data class ContextWhatsNewVersion(private val context: Context) : WhatsNewVersion("") {
+ override val version: String
+ get() = context.appVersionName ?: ""
+}
diff --git a/app/src/main/res/drawable/ic_whats_new.xml b/app/src/main/res/drawable/ic_whats_new.xml
new file mode 100644
index 000000000..7fb9e238c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_whats_new.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_whats_new_notification.xml b/app/src/main/res/drawable/ic_whats_new_notification.xml
new file mode 100644
index 000000000..d05fc67b0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_whats_new_notification.xml
@@ -0,0 +1,19 @@
+
+
+
+
+-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml
index e195062d8..a68ffb263 100644
--- a/app/src/main/res/layout/fragment_about.xml
+++ b/app/src/main/res/layout/fragment_about.xml
@@ -81,6 +81,20 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
+
+
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index 73f246fe8..fb518b4df 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -404,7 +404,11 @@
+ android:label="AboutFragment" >
+
+
#592ACB
+
+
+ #00B3F4
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2628bc309..3fc4241ed 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -42,6 +42,8 @@
Stop
Help
+
+ What\'s New
Settings
@@ -817,6 +819,8 @@
Your rights
Open source libraries we use
+
+ What\'s new in %s
%s | OSS Libraries
diff --git a/app/src/test/java/org/mozilla/fenix/whatsnew/WhatsNewStorageTest.kt b/app/src/test/java/org/mozilla/fenix/whatsnew/WhatsNewStorageTest.kt
new file mode 100644
index 000000000..5a768eec4
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/whatsnew/WhatsNewStorageTest.kt
@@ -0,0 +1,64 @@
+package org.mozilla.fenix.whatsnew
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ObsoleteCoroutinesApi
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.TestApplication
+import org.mozilla.fenix.ext.clearAndCommit
+import org.mozilla.fenix.utils.Settings
+import org.robolectric.annotation.Config
+
+@ObsoleteCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+@Config(application = TestApplication::class)
+class WhatsNewStorageTest {
+ private lateinit var storage: SharedPreferenceWhatsNewStorage
+ private lateinit var settings: Settings
+
+ @Before
+ fun setUp() {
+ storage = SharedPreferenceWhatsNewStorage(testContext)
+ settings = Settings.getInstance(testContext)
+ .apply(Settings::clear)
+ }
+
+ @Test
+ fun testGettingAndSettingAVersion() {
+ val version = WhatsNewVersion("3.0")
+ storage.setVersion(version)
+
+ val storedVersion = storage.getVersion()
+ Assert.assertEquals(version, storedVersion)
+ }
+
+ @Test
+ fun testGettingAndSettingTheDateOfUpdate() {
+ val currentTime = System.currentTimeMillis()
+ val twoDaysAgo = (currentTime - DAY_IN_MILLIS * 2)
+ storage.setDateOfUpdate(twoDaysAgo)
+
+ val storedDate = storage.getDaysSinceUpdate()
+ Assert.assertEquals(2, storedDate)
+ }
+
+ @Test
+ fun testGettingAndSettingHasBeenCleared() {
+ val hasBeenCleared = true
+ storage.setWhatsNewHasBeenCleared(hasBeenCleared)
+
+ val storedHasBeenCleared = storage.getWhatsNewHasBeenCleared()
+ Assert.assertEquals(hasBeenCleared, storedHasBeenCleared)
+ }
+
+ companion object {
+ const val DAY_IN_MILLIS = 3600 * 1000 * 24
+ }
+}
+
+private fun Settings.clear() {
+ preferences.clearAndCommit()
+}
diff --git a/app/src/test/java/org/mozilla/fenix/whatsnew/WhatsNewVersionTest.kt b/app/src/test/java/org/mozilla/fenix/whatsnew/WhatsNewVersionTest.kt
new file mode 100644
index 000000000..0acedf273
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/whatsnew/WhatsNewVersionTest.kt
@@ -0,0 +1,28 @@
+package org.mozilla.fenix.whatsnew
+
+/* 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/. */
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ObsoleteCoroutinesApi
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.TestApplication
+import org.robolectric.annotation.Config
+
+@ObsoleteCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+@Config(application = TestApplication::class)
+class WhatsNewVersionTest {
+ @Test
+ fun testMajorVersionNumber() {
+ val versionOne = WhatsNewVersion("1.2.0")
+ assertEquals(1, versionOne.majorVersionNumber)
+
+ val versionTwo = WhatsNewVersion("2.4.0")
+ assertNotEquals(1, versionTwo.majorVersionNumber)
+ }
+}