* For #4474: Adds what's new button to home screen menu * For #4474: Adds tests for what's new buttonnightly-build-test
parent
46b09395f8
commit
09dcdb079d
@ -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