Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

1570647 - Do not send 'metrics' ping if the app was not used #3993

Merged
merged 1 commit into from
Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ open class GleanInternalAPI internal constructor () {
// threaded race conditions.
@Suppress("EXPERIMENTAL_API_USAGE")
if (!Dispatchers.API.testingMode) {
metricsPingScheduler.startupCheck()
metricsPingScheduler.schedule()
}

// Signal Dispatcher that init is complete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.VisibleForTesting
import android.text.format.DateUtils
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.Worker
Expand All @@ -31,8 +35,12 @@ import java.util.concurrent.TimeUnit as AndroidTimeUnit
* - ping is overdue (due time already passed) for the current calendar day;
* - ping is soon to be sent in the current calendar day;
* - ping was already sent, and must be scheduled for the next calendar day.
*
* The scheduler also makes use of the [LifecycleObserver] in order to correctly schedule
* the [MetricsPingWorker]
*/
internal class MetricsPingScheduler(val applicationContext: Context) {
@Suppress("TooManyFunctions")
internal class MetricsPingScheduler(val applicationContext: Context) : LifecycleObserver {
private val logger = Logger("glean/MetricsPingScheduler")
internal val sharedPreferences: SharedPreferences by lazy {
applicationContext.getSharedPreferences(this.javaClass.canonicalName, Context.MODE_PRIVATE)
Expand All @@ -41,6 +49,12 @@ internal class MetricsPingScheduler(val applicationContext: Context) {
companion object {
const val LAST_METRICS_PING_SENT_DATETIME = "last_metrics_ping_iso_datetime"
const val DUE_HOUR_OF_THE_DAY = 4
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var isInForeground = false
}

init {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}

/**
Expand Down Expand Up @@ -157,7 +171,7 @@ internal class MetricsPingScheduler(val applicationContext: Context) {
* Performs startup checks to decide when to schedule the next metrics ping
* collection.
*/
fun startupCheck() {
fun schedule() {
val now = getCalendarInstance()
val lastSentDate = getLastCollectedDate()

Expand Down Expand Up @@ -218,8 +232,14 @@ internal class MetricsPingScheduler(val applicationContext: Context) {
// Update the collection date: we don't really care if we have data or not, let's
// always update the sent date.
updateSentDate(getISOTimeString(now, truncateTo = TimeUnit.Day))
// Reschedule the collection.
schedulePingCollection(now, sendTheNextCalendarDay = true)

// Reschedule the collection if we are in the foreground so that any metrics collected after
// this are sent in the next window. If we are in the background, then we may stay there
// until the app is killed so we shouldn't reschedule unless the app is foregrounded again
// (see GleanLifecycleObserver).
if (isInForeground) {
schedulePingCollection(now, sendTheNextCalendarDay = true)
}
}

/**
Expand Down Expand Up @@ -262,6 +282,32 @@ internal class MetricsPingScheduler(val applicationContext: Context) {
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun getCalendarInstance(): Calendar = Calendar.getInstance()

/**
* Update flag to show we are no longer in the foreground.
*/
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onEnterBackground() {
isInForeground = false
}

/**
* Update the flag to indicate we are moving to the foreground, and if Glean is initialized we
* will check to see if the metrics ping needs scheduled for collection.
*/
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onEnterForeground() {
isInForeground = true

// We check for the metrics ping schedule here because the app could have been in the
// background and resumed in which case Glean would already be initialized but we still need
// to perform the check to determine whether or not to collect and schedule the metrics ping.
// If this is the first ON_START event since the app was launched, Glean wouldn't be
// initialized yet.
if (Glean.isInitialized()) {
schedule()
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@

package mozilla.components.service.glean.scheduler

import android.content.Context
import android.os.SystemClock
import androidx.test.core.app.ApplicationProvider
import androidx.work.testing.WorkManagerTestInitHelper
import mozilla.components.service.glean.getContextWithMockedInfo
import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.checkPingSchema
import mozilla.components.service.glean.private.Lifetime
import mozilla.components.service.glean.resetGlean
import mozilla.components.service.glean.private.StringMetricType
import mozilla.components.service.glean.private.TimeUnit
import mozilla.components.service.glean.checkPingSchema
import mozilla.components.service.glean.triggerWorkManager
import mozilla.components.service.glean.config.Configuration
import mozilla.components.service.glean.getContextWithMockedInfo
import mozilla.components.service.glean.getMockWebServer
import mozilla.components.service.glean.getWorkerStatus
import mozilla.components.service.glean.resetGlean
import mozilla.components.service.glean.triggerWorkManager
import mozilla.components.service.glean.utils.getISOTimeString
import mozilla.components.service.glean.utils.parseISOTimeString
import org.json.JSONObject
Expand Down Expand Up @@ -65,7 +64,7 @@ class MetricsPingSchedulerTest {
@Test
fun `milliseconds until the due time must be correctly computed`() {
val metricsPingScheduler = MetricsPingScheduler(
ApplicationProvider.getApplicationContext<Context>())
ApplicationProvider.getApplicationContext())

val fakeNow = Calendar.getInstance()
fakeNow.clear()
Expand Down Expand Up @@ -103,7 +102,7 @@ class MetricsPingSchedulerTest {

@Test
fun `getDueTimeForToday must correctly return the due time for the current day`() {
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>())
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext())

val fakeNow = Calendar.getInstance()
fakeNow.clear()
Expand All @@ -122,7 +121,7 @@ class MetricsPingSchedulerTest {

@Test
fun `isAfterDueTime must report false before the due time on the same calendar day`() {
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>())
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext())

val fakeNow = Calendar.getInstance()
fakeNow.clear()
Expand All @@ -142,7 +141,7 @@ class MetricsPingSchedulerTest {

@Test
fun `isAfterDueTime must report true after the due time on the same calendar day`() {
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>())
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext())

val fakeNow = Calendar.getInstance()
fakeNow.clear()
Expand All @@ -154,7 +153,7 @@ class MetricsPingSchedulerTest {

@Test
fun `getLastCollectedDate must report null when no stored date is available`() {
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>())
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext())
mps.sharedPreferences.edit().clear().apply()

assertNull(
Expand All @@ -165,7 +164,7 @@ class MetricsPingSchedulerTest {

@Test
fun `getLastCollectedDate must report null when the stored date is corrupted`() {
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>())
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext())
mps.sharedPreferences
.edit()
.putLong(MetricsPingScheduler.LAST_METRICS_PING_SENT_DATETIME, 123L)
Expand All @@ -192,7 +191,7 @@ class MetricsPingSchedulerTest {
@Test
fun `getLastCollectedDate must report the stored last collected date, if available`() {
val testDate = "2018-12-19T12:36:00-06:00"
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>())
val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext())
mps.updateSentDate(testDate)

val expectedDate = parseISOTimeString(testDate)!!
Expand All @@ -205,14 +204,14 @@ class MetricsPingSchedulerTest {

@Test
fun `collectMetricsPing must update the last sent date and reschedule the collection`() {
val mpsSpy = spy<MetricsPingScheduler>(
MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>()))
val mpsSpy = spy(
MetricsPingScheduler(ApplicationProvider.getApplicationContext()))

// Ensure we have the right assumptions in place: the methods were not called
// prior to |collectPingAndReschedule|.
verify(mpsSpy, times(0)).updateSentDate(anyString())
verify(mpsSpy, times(0)).schedulePingCollection(
kotlinFriendlyAny<Calendar>(),
kotlinFriendlyAny(),
anyBoolean()
)

Expand All @@ -221,7 +220,7 @@ class MetricsPingSchedulerTest {
// Verify that we correctly called in the methods.
verify(mpsSpy, times(1)).updateSentDate(anyString())
verify(mpsSpy, times(1)).schedulePingCollection(
kotlinFriendlyAny<Calendar>(),
kotlinFriendlyAny(),
anyBoolean()
)
}
Expand Down Expand Up @@ -286,19 +285,21 @@ class MetricsPingSchedulerTest {

// Set the last sent date to a previous day, so that today's ping is overdue.
val mpsSpy =
spy<MetricsPingScheduler>(MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>()))
spy(MetricsPingScheduler(ApplicationProvider.getApplicationContext()))
val overdueTestDate = "2015-07-05T12:36:00-06:00"
mpsSpy.updateSentDate(overdueTestDate)

verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny<Calendar>())
MetricsPingScheduler.isInForeground = true

verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny())

// Make sure to return the fake date when requested.
doReturn(fakeNow).`when`(mpsSpy).getCalendarInstance()

// Trigger the startup check. We need to wrap this in `blockDispatchersAPI` since
// the immediate startup collection happens in the Dispatchers.API context. If we
// don't, test will fail due to async weirdness.
mpsSpy.startupCheck()
mpsSpy.schedule()

// And that we're storing the current date (this only reports the date, not the time).
fakeNow.set(Calendar.HOUR_OF_DAY, 0)
Expand All @@ -318,21 +319,21 @@ class MetricsPingSchedulerTest {

// Set the last sent date to now.
val mpsSpy =
spy<MetricsPingScheduler>(MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>()))
spy(MetricsPingScheduler(ApplicationProvider.getApplicationContext()))
mpsSpy.updateSentDate(getISOTimeString(fakeNow, truncateTo = TimeUnit.Day))

verify(mpsSpy, never()).schedulePingCollection(kotlinFriendlyAny<Calendar>(), anyBoolean())
verify(mpsSpy, never()).schedulePingCollection(kotlinFriendlyAny(), anyBoolean())

// Make sure to return the fake date when requested.
doReturn(fakeNow).`when`(mpsSpy).getCalendarInstance()

// Trigger the startup check.
mpsSpy.startupCheck()
mpsSpy.schedule()

// Verify that we're scheduling for the next day and not collecting immediately.
verify(mpsSpy, times(1)).schedulePingCollection(fakeNow, sendTheNextCalendarDay = true)
verify(mpsSpy, never()).schedulePingCollection(fakeNow, sendTheNextCalendarDay = false)
verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny<Calendar>())
verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny())
}

@Test
Expand All @@ -345,7 +346,7 @@ class MetricsPingSchedulerTest {

// Set the last sent date to yesterday.
val mpsSpy =
spy<MetricsPingScheduler>(MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>()))
spy(MetricsPingScheduler(ApplicationProvider.getApplicationContext()))

val fakeYesterday = Calendar.getInstance()
fakeYesterday.time = fakeNow.time
Expand All @@ -355,15 +356,15 @@ class MetricsPingSchedulerTest {
// Make sure to return the fake date when requested.
doReturn(fakeNow).`when`(mpsSpy).getCalendarInstance()

verify(mpsSpy, never()).schedulePingCollection(kotlinFriendlyAny<Calendar>(), anyBoolean())
verify(mpsSpy, never()).schedulePingCollection(kotlinFriendlyAny(), anyBoolean())

// Trigger the startup check.
mpsSpy.startupCheck()
mpsSpy.schedule()

// Verify that we're scheduling for today, but not collecting immediately.
verify(mpsSpy, times(1)).schedulePingCollection(fakeNow, sendTheNextCalendarDay = false)
verify(mpsSpy, never()).schedulePingCollection(fakeNow, sendTheNextCalendarDay = true)
verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny<Calendar>())
verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny())
}

@Test
Expand All @@ -375,16 +376,16 @@ class MetricsPingSchedulerTest {

// Clear the last sent date.
val mpsSpy =
spy<MetricsPingScheduler>(MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>()))
spy(MetricsPingScheduler(ApplicationProvider.getApplicationContext()))
mpsSpy.sharedPreferences.edit().clear().apply()

verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny<Calendar>())
verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny())

// Make sure to return the fake date when requested.
doReturn(fakeNow).`when`(mpsSpy).getCalendarInstance()

// Trigger the startup check.
mpsSpy.startupCheck()
mpsSpy.schedule()

// Verify that we're immediately collecting.
verify(mpsSpy, never()).collectPingAndReschedule(fakeNow)
Expand All @@ -400,16 +401,16 @@ class MetricsPingSchedulerTest {

// Clear the last sent date.
val mpsSpy =
spy<MetricsPingScheduler>(MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>()))
spy(MetricsPingScheduler(ApplicationProvider.getApplicationContext()))
mpsSpy.sharedPreferences.edit().clear().apply()

verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny<Calendar>())
verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny())

// Make sure to return the fake date when requested.
doReturn(fakeNow).`when`(mpsSpy).getCalendarInstance()

// Trigger the startup check.
mpsSpy.startupCheck()
mpsSpy.schedule()

// And that we're storing the current date (this only reports the date, not the time).
fakeNow.set(Calendar.HOUR_OF_DAY, 0)
Expand All @@ -428,7 +429,8 @@ class MetricsPingSchedulerTest {
fun `schedulePingCollection must correctly append a work request to the WorkManager`() {
// Replacing the singleton's metricsPingScheduler here since doWork() refers to it when
// the worker runs, otherwise we can get a lateinit property is not initialized error.
Glean.metricsPingScheduler = MetricsPingScheduler(ApplicationProvider.getApplicationContext<Context>())
Glean.metricsPingScheduler = MetricsPingScheduler(ApplicationProvider.getApplicationContext())
MetricsPingScheduler.isInForeground = true

// No work should be enqueued at the beginning of the test.
assertFalse(getWorkerStatus(MetricsPingWorker.TAG).isEnqueued)
Expand All @@ -438,6 +440,23 @@ class MetricsPingSchedulerTest {

// We expect the worker to be scheduled.
assertTrue(getWorkerStatus(MetricsPingWorker.TAG).isEnqueued)

resetGlean(clearStores = true)
}

@Test
fun `schedule() happens when returning from background when Glean is already initialized`() {
// Initialize Glean
resetGlean()

// We expect the worker to not be scheduled.
assertFalse(getWorkerStatus(MetricsPingWorker.TAG).isEnqueued)

// Simulate returning to the foreground with glean initialized.
Glean.metricsPingScheduler.onEnterForeground()

// We expect the worker to be scheduled.
assertTrue(getWorkerStatus(MetricsPingWorker.TAG).isEnqueued)
}

// @Test
Expand Down