From 09dcdb079dc8f6048dc3178134f11a71f66f95ce Mon Sep 17 00:00:00 2001 From: Sawyer Blatz Date: Tue, 3 Sep 2019 13:16:29 -0700 Subject: [PATCH] For #4474: Adds what's new button to home screen menu (#5088) * For #4474: Adds what's new button to home screen menu * For #4474: Adds tests for what's new button --- .../org/mozilla/fenix/BrowserDirection.kt | 3 +- .../java/org/mozilla/fenix/HomeActivity.kt | 3 + .../org/mozilla/fenix/home/HomeFragment.kt | 14 +++ .../java/org/mozilla/fenix/home/HomeMenu.kt | 26 ++++- .../mozilla/fenix/settings/AboutFragment.kt | 18 ++++ .../mozilla/fenix/settings/SupportUtils.kt | 3 +- .../org/mozilla/fenix/whatsnew/WhatsNew.kt | 97 +++++++++++++++++++ .../mozilla/fenix/whatsnew/WhatsNewStorage.kt | 69 +++++++++++++ .../mozilla/fenix/whatsnew/WhatsNewVersion.kt | 37 +++++++ app/src/main/res/drawable/ic_whats_new.xml | 13 +++ .../drawable/ic_whats_new_notification.xml | 19 ++++ app/src/main/res/layout/fragment_about.xml | 14 +++ app/src/main/res/navigation/nav_graph.xml | 6 +- app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/strings.xml | 4 + .../fenix/whatsnew/WhatsNewStorageTest.kt | 64 ++++++++++++ .../fenix/whatsnew/WhatsNewVersionTest.kt | 28 ++++++ 17 files changed, 414 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNew.kt create mode 100644 app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewStorage.kt create mode 100644 app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewVersion.kt create mode 100644 app/src/main/res/drawable/ic_whats_new.xml create mode 100644 app/src/main/res/drawable/ic_whats_new_notification.xml create mode 100644 app/src/test/java/org/mozilla/fenix/whatsnew/WhatsNewStorageTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/whatsnew/WhatsNewVersionTest.kt 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) + } +}