[fenix] For https://github.com/mozilla-mobile/fenix/issues/4474: Adds what's new button to home screen menu (https://github.com/mozilla-mobile/fenix/pull/5088)
* For https://github.com/mozilla-mobile/fenix/issues/4474: Adds what's new button to home screen menu * For https://github.com/mozilla-mobile/fenix/issues/4474: Adds tests for what's new buttonpull/600/head
parent
8c4f0ecc02
commit
e0e1bdba5c
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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 ?: ""
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<!-- 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/. -->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?primaryText"
|
||||
android:pathData="M10.714,13.939L10.714,22L5.877,22a1.628,1.628 0,0 1,-1.591 -1.663v-6.4zM19.706,13.939h-6.42L13.286,22h4.837a1.628,1.628 0,0 0,1.591 -1.663v-6.39a0.007,0.007 0,0 0,-0.008 -0.008zM10.714,7.221L4.286,7.221A1.316,1.316 0,0 0,3 8.565v4.018a0.011,0.011 0,0 0,0.011 0.012h7.7zM19.714,7.221h-6.428L13.286,12.6h7.7A0.011,0.011 0,0 0,21 12.583L21,8.565a1.316,1.316 0,0 0,-1.286 -1.344zM14.761,2.393c-1.008,-0.9 -2.288,-0.084 -2.75,0.8L12,3.2L12,3.193c-0.462,-0.881 -1.742,-1.7 -2.75,-0.8S7.843,5.185 12,7.221c4.157,-2.036 3.768,-3.921 2.761,-4.828z"/>
|
||||
</vector>
|
@ -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>
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue