Skip to content

Commit

Permalink
Use the LifecycleEventObserver API (#501)
Browse files Browse the repository at this point in the history
* Use the LifecycleEventObserver API

This doesn't require annotations or any Java8 magic, so hopefully will work.

* Add CHANGELOG
  • Loading branch information
mdboom committed Nov 20, 2019
1 parent a995c2c commit 711e023
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 52 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,11 @@

[Full changelog](https://github.com/mozilla/glean/compare/v21.1.0...master)

* Android:

* Use the `LifecycleEventObserver` interface, rather than the `DefaultLifecycleObserver`
interface, since the latter isn't compatible with old SDK targets.

# v21.1.0 (2019-11-20)

[Full changelog](https://github.com/mozilla/glean/compare/v21.0.0...v21.1.0)
Expand Down
2 changes: 1 addition & 1 deletion glean-core/android/build.gradle
Expand Up @@ -175,7 +175,7 @@ dependencies {

implementation "androidx.annotation:annotation:$versions.androidx_annotation"
implementation "androidx.lifecycle:lifecycle-extensions:$versions.androidx_lifecycle_extensions"
implementation "androidx.lifecycle:lifecycle-common-java8:$versions.androidx_lifecycle_extensions"
implementation "androidx.lifecycle:lifecycle-common:$versions.androidx_lifecycle_extensions"
implementation "androidx.work:work-runtime-ktx:$versions.androidx_work"

// We need a compileOnly dependency on the following block of testing
Expand Down
Expand Up @@ -4,7 +4,8 @@

package mozilla.telemetry.glean.scheduler

import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import mozilla.telemetry.glean.Glean
import mozilla.telemetry.glean.GleanMetrics.GleanBaseline
Expand All @@ -13,29 +14,35 @@ import mozilla.telemetry.glean.GleanMetrics.GleanBaseline
* Connects process lifecycle events from Android to Glean's handleEvent
* functionality (where the actual work of sending pings is done).
*/
internal class GleanLifecycleObserver : DefaultLifecycleObserver {
internal class GleanLifecycleObserver : LifecycleEventObserver {
/**
* Calls the "background" event when entering the background.
* Called when lifecycle events are triggered.
*/
override fun onStop(owner: LifecycleOwner) {
// We're going to background, so store how much time we spent
// on foreground.
GleanBaseline.duration.stop()
Glean.handleBackgroundEvent()
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
when (event) {
Lifecycle.Event.ON_STOP -> {
// We're going to background, so store how much time we spent
// on foreground.
GleanBaseline.duration.stop()
Glean.handleBackgroundEvent()
}
Lifecycle.Event.ON_START -> {
// Updates the baseline.duration metric when entering the foreground.
// We use ON_START here because we don't want to incorrectly count metrics in
// ON_RESUME as pause/resume can happen when interacting with things like the
// navigation shade which could lead to incorrectly recording the start of a
// duration, etc.
//
// https://developer.android.com/reference/android/app/Activity.html#onStart()

/**
* Updates the baseline.duration metric when entering the foreground.
* We use ON_START here because we don't want to incorrectly count metrics in ON_RESUME as
* pause/resume can happen when interacting with things like the navigation shade which could
* lead to incorrectly recording the start of a duration, etc.
*
* https://developer.android.com/reference/android/app/Activity.html#onStart()
*/
override fun onStart(owner: LifecycleOwner) {
// Note that this is sending the length of the last foreground session
// because it belongs to the baseline ping and that ping is sent every
// time the app goes to background.
GleanBaseline.duration.start()
// Note that this is sending the length of the last foreground session
// because it belongs to the baseline ping and that ping is sent every
// time the app goes to background.
GleanBaseline.duration.start()
}
else -> {
// For other lifecycle events, do nothing
}
}
}
}
Expand Up @@ -9,7 +9,8 @@ import android.content.SharedPreferences
import androidx.annotation.VisibleForTesting
import android.text.format.DateUtils
import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.ExistingWorkPolicy
Expand Down Expand Up @@ -43,7 +44,7 @@ import java.util.concurrent.TimeUnit as AndroidTimeUnit
internal class MetricsPingScheduler(
private val applicationContext: Context,
migratedLastSentDate: String? = null
) : DefaultLifecycleObserver {
) : LifecycleEventObserver {
internal val sharedPreferences: SharedPreferences by lazy {
applicationContext.getSharedPreferences(this.javaClass.canonicalName, Context.MODE_PRIVATE)
}
Expand Down Expand Up @@ -310,31 +311,38 @@ internal class MetricsPingScheduler(
internal fun getCalendarInstance(): Calendar = Calendar.getInstance()

/**
* Update flag to show we are no longer in the foreground.
* Called when lifecycle events are triggered.
*/
override fun onStop(owner: LifecycleOwner) {
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 to be scheduled for collection.
*/
override fun onStart(owner: LifecycleOwner) {
isInForeground = true
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
when (event) {
Lifecycle.Event.ON_STOP -> {
// Update flag to show we are no longer in the foreground.
isInForeground = false
}
Lifecycle.Event.ON_START -> {
// 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 to be scheduled for
// collection.
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 to collect and schedule the metrics ping.
// Since Glean.initialize() is called in the Application.onCreate() function, we will get
// this after glean is initialized and thus will call schedule() twice. So we prevent this
// by using a flag to prevent scheduling from the lifecycle observer on the first foreground
// event. See https://bugzilla.mozilla.org/1590329
if (Glean.isInitialized()) {
if (!firstForeground) {
schedule()
} else {
firstForeground = false
// 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 to collect and schedule
// the metrics ping. Since Glean.initialize() is called in the
// Application.onCreate() function, we will get this after glean is initialized and
// thus will call schedule() twice. So we prevent this by using a flag to prevent
// scheduling from the lifecycle observer on the first foreground event.
// See https://bugzilla.mozilla.org/1590329
if (Glean.isInitialized()) {
if (!firstForeground) {
schedule()
} else {
firstForeground = false
}
}
}
else -> {
// For other lifecycle events, do nothing
}
}
}
Expand Down
Expand Up @@ -6,6 +6,7 @@ package mozilla.telemetry.glean.scheduler

import android.content.Context
import android.os.SystemClock
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
Expand Down Expand Up @@ -459,23 +460,23 @@ class MetricsPingSchedulerTest {
// Initialize Glean
resetGlean(clearStores = true)
val mpsSpy = mock(MetricsPingScheduler::class.java)
`when`(mpsSpy.onStart(ProcessLifecycleOwner.get())).thenCallRealMethod()
`when`(mpsSpy.onStateChanged(ProcessLifecycleOwner.get(), Lifecycle.Event.ON_START)).thenCallRealMethod()
Glean.metricsPingScheduler = mpsSpy

// Make sure schedule() has not been called. Since we are adding the spy after resetGlean
// has called Glean.initialize(), we won't see the first invocation of schedule().
verify(mpsSpy, times(0)).schedule()

// Simulate returning to the foreground with Glean initialized.
Glean.metricsPingScheduler.onStart(ProcessLifecycleOwner.get())
Glean.metricsPingScheduler.onStateChanged(ProcessLifecycleOwner.get(), Lifecycle.Event.ON_START)

// Verify that schedule hasn't been called since we don't schedule on the first foreground
// since Glean.initialize() ensures schedule is called before any queued tasks are executed
verify(mpsSpy, times(0)).schedule()

// Simulate going to background and then foreground
Glean.metricsPingScheduler.onStop(ProcessLifecycleOwner.get())
Glean.metricsPingScheduler.onStart(ProcessLifecycleOwner.get())
Glean.metricsPingScheduler.onStateChanged(ProcessLifecycleOwner.get(), Lifecycle.Event.ON_STOP)
Glean.metricsPingScheduler.onStateChanged(ProcessLifecycleOwner.get(), Lifecycle.Event.ON_START)

// Verify that schedule has been called on subsequent foreground events
verify(mpsSpy, times(1)).schedule()
Expand Down

0 comments on commit 711e023

Please sign in to comment.