1
0
Fork 0

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
master
Sawyer Blatz 2019-09-03 13:16:29 -07:00 committed by Colin Lee
parent 46b09395f8
commit 09dcdb079d
17 changed files with 414 additions and 7 deletions

View File

@ -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)
}

View File

@ -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(

View File

@ -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
)
}
}
}
}

View File

@ -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)
}
)
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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
)
)
}
}
}

View File

@ -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()
}
}

View File

@ -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 ?: ""
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,19 @@
<?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/. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_whats_new" />
<item
android:left="200dp"
android:bottom="200dp">
<shape
android:shape="oval">
<solid android:color="@color/whats_new_notification_color" />
<size
android:width="48dp"
android:height="48dp"/>
</shape>
</item>
</layer-list>

View File

@ -81,6 +81,20 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/whats_new_button"
android:layout_margin="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/about_whats_new"
android:textSize="16sp"
android:textColor="?accent"
android:textStyle="bold"
android:textAllCaps="false"
app:layout_constraintTop_toBottomOf="@id/view_licenses_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -404,7 +404,11 @@
<fragment
android:id="@+id/aboutFragment"
android:name="org.mozilla.fenix.settings.AboutFragment"
android:label="AboutFragment" />
android:label="AboutFragment" >
<action
android:id="@+id/action_aboutFragment_to_browserFragment"
app:destination="@id/browserFragment" />
</fragment>
<fragment
android:id="@+id/crashReporterFragment"
android:name="org.mozilla.fenix.crashes.CrashReporterFragment"

View File

@ -217,4 +217,7 @@
<!-- Private Browsing Mode Persistent Notification -->
<color name="pbm_notification_color">#592ACB</color>
<!-- Notification Color -->
<color name="whats_new_notification_color">#00B3F4</color>
</resources>

View File

@ -42,6 +42,8 @@
<string name="browser_menu_stop">Stop</string>
<!-- Browser menu button that sends a user to help articles -->
<string name="browser_menu_help">Help</string>
<!-- Browser menu button that sends a to a the what's new article -->
<string name="browser_menu_whats_new">What\'s New</string>
<!-- Browser menu button that opens the settings menu -->
<string name="browser_menu_settings">Settings</string>
<!-- Browser menu button that opens a user's library -->
@ -817,6 +819,8 @@
<string name="about_your_rights">Your rights</string>
<!-- About page link text to open open source licenses screen -->
<string name="about_open_source_licenses">Open source libraries we use</string>
<!-- About page link text to open what's new link -->
<string name="about_whats_new">What\'s new in %s</string>
<!-- Open source licenses page title
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | OSS Libraries</string>

View File

@ -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()
}

View File

@ -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)
}
}