Skip to content

Commit

Permalink
Bug 1812204 - Add usage growth data event
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewTighe authored and mergify[bot] committed Feb 2, 2023
1 parent 203aa1b commit 1b47977
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 5 deletions.
2 changes: 2 additions & 0 deletions app/src/main/java/org/mozilla/fenix/FenixApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {

ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))

components.analytics.metricsStorage.tryRegisterAsUsageRecorder(this)

downloadWallpapers()
}

Expand Down
15 changes: 10 additions & 5 deletions app/src/main/java/org/mozilla/fenix/components/Analytics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.DefaultMetricsStorage
import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsStorage
import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.CustomAttributeProvider
Expand Down Expand Up @@ -117,17 +118,21 @@ class Analytics(
)
}

val metricsStorage: MetricsStorage by lazyMonitored {
DefaultMetricsStorage(
context = context,
settings = context.settings(),
checkDefaultBrowser = { BrowsersCache.all(context).isDefaultBrowser },
)
}

val metrics: MetricController by lazyMonitored {
MetricController.create(
listOf(
GleanMetricsService(context),
AdjustMetricsService(
application = context as Application,
storage = DefaultMetricsStorage(
context = context,
settings = context.settings(),
checkDefaultBrowser = { BrowsersCache.all(context).isDefaultBrowser },
),
storage = metricsStorage,
crashReporter = crashReporter,
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,10 @@ sealed class Event {
* Event recording the first time Firefox is used 3 days in a row in the first week of install.
*/
object FirstWeekSeriesActivity : GrowthData("20ay7u")

/**
* Event recording that usage time has reached a threshold.
*/
object UsageThreshold : GrowthData("m66prt")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class MetricsMiddleware(
is AppAction.ResumedMetricsAction -> {
metrics.track(Event.GrowthData.SetAsDefault)
metrics.track(Event.GrowthData.FirstWeekSeriesActivity)
metrics.track(Event.GrowthData.UsageThreshold)
}
else -> Unit
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

package org.mozilla.fenix.components.metrics

import android.app.Activity
import android.app.Application
import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.mozilla.fenix.android.DefaultActivityLifecycleCallbacks
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings
Expand All @@ -29,6 +32,18 @@ interface MetricsStorage {
* Updates locally-stored state for an [event] that has just been sent.
*/
suspend fun updateSentState(event: Event)

/**
* Will try to register this as a recorder of app usage based on whether usage recording is still
* needed. It will measure usage by to monitoring lifecycle callbacks from [application]'s
* activities and should update local state using [updateUsageState].
*/
fun tryRegisterAsUsageRecorder(application: Application)

/**
* Update local state with a [usageLength] measurement.
*/
fun updateUsageState(usageLength: Long)
}

internal class DefaultMetricsStorage(
Expand Down Expand Up @@ -62,6 +77,10 @@ internal class DefaultMetricsStorage(
Event.GrowthData.SerpAdClicked -> {
currentTime.duringFirstMonth() && !settings.adClickGrowthSent
}
Event.GrowthData.UsageThreshold -> {
!settings.usageTimeGrowthSent &&
settings.usageTimeGrowthData > usageThresholdMillis
}
}
}

Expand All @@ -76,9 +95,23 @@ internal class DefaultMetricsStorage(
Event.GrowthData.SerpAdClicked -> {
settings.adClickGrowthSent = true
}
Event.GrowthData.UsageThreshold -> {
settings.usageTimeGrowthSent = true
}
}
}

override fun tryRegisterAsUsageRecorder(application: Application) {
// Currently there is only interest in measuring usage during the first day of install.
if (!settings.usageTimeGrowthSent && System.currentTimeMillis().duringFirstDay()) {
application.registerActivityLifecycleCallbacks(UsageRecorder(this))
}
}

override fun updateUsageState(usageLength: Long) {
settings.usageTimeGrowthData += usageLength
}

private fun updateDaysOfUse() {
val daysOfUse = settings.firstWeekDaysOfUseGrowthData
val currentDate = Calendar.getInstance(Locale.US)
Expand Down Expand Up @@ -121,6 +154,8 @@ internal class DefaultMetricsStorage(
calendar.timeInMillis = this
}

private fun Long.duringFirstDay() = this < getInstalledTime() + dayMillis

private fun Long.duringFirstWeek() = this < getInstalledTime() + fullWeekMillis

private fun Long.duringFirstMonth() = this < getInstalledTime() + shortestMonthMillis
Expand All @@ -129,6 +164,28 @@ internal class DefaultMetricsStorage(
calendar.add(Calendar.DAY_OF_MONTH, 1)
}

/**
* This will store app usage time to disk, based on Resume and Pause lifecycle events. Currently,
* there is only interest in usage during the first day after install.
*/
internal class UsageRecorder(
private val metricsStorage: MetricsStorage,
) : DefaultActivityLifecycleCallbacks {
private val activityStartTimes: MutableMap<String, Long?> = mutableMapOf()

override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
activityStartTimes[activity.componentName.toString()] = System.currentTimeMillis()
}

override fun onActivityPaused(activity: Activity) {
super.onActivityPaused(activity)
val startTime = activityStartTimes[activity.componentName.toString()] ?: return
val elapsedTimeMillis = System.currentTimeMillis() - startTime
metricsStorage.updateUsageState(elapsedTimeMillis)
}
}

companion object {
private const val dayMillis: Long = 1000 * 60 * 60 * 24
private const val shortestMonthMillis: Long = dayMillis * 28
Expand All @@ -137,6 +194,9 @@ internal class DefaultMetricsStorage(
// of the 7th day after install
private const val fullWeekMillis: Long = dayMillis * 8

// The usage threshold we are interested in is currently 340 seconds.
private const val usageThresholdMillis = 1000 * 340

/**
* Determines whether events should be tracked based on some general criteria:
* - user has installed as a result of a campaign
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/mozilla/fenix/utils/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1557,4 +1557,14 @@ class Settings(private val appContext: Context) : PreferencesHolder {
key = appContext.getPreferenceKey(R.string.pref_key_growth_ad_click_sent),
default = false,
)

var usageTimeGrowthData by longPreference(
key = appContext.getPreferenceKey(R.string.pref_key_growth_usage_time),
default = -1,
)

var usageTimeGrowthSent by booleanPreference(
key = appContext.getPreferenceKey(R.string.pref_key_growth_usage_time_sent),
default = false,
)
}
2 changes: 2 additions & 0 deletions app/src/main/res/values/preference_keys.xml
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@
<string name="pref_key_growth_first_week_series_sent" translatable="false">pref_key_growth_first_week_series_sent</string>
<string name="pref_key_growth_first_week_days_of_use" translatable="false">pref_key_growth_first_week_days_of_use</string>
<string name="pref_key_growth_ad_click_sent" translatable="false">pref_key_growth_ad_click_sent</string>
<string name="pref_key_growth_usage_time" translatable="false">pref_key_growth_usage_time</string>
<string name="pref_key_growth_usage_time_sent" translatable="false">pref_key_growth_usage_time_sent</string>

<!-- Notification Pre Permission Prompt -->
<string name="pref_key_notification_pre_permission_prompt_enabled">pref_key_notification_pre_permission_prompt_enabled</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@

package org.mozilla.fenix.components.metrics

import android.app.Activity
import android.app.Application
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.mock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
Expand All @@ -23,6 +28,7 @@ class DefaultMetricsStorageTest {
private val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
private val calendarStart = Calendar.getInstance(Locale.US)
private val dayMillis: Long = 1000 * 60 * 60 * 24
private val usageThresholdMillis: Long = 340 * 1000

private var checkDefaultBrowser = false
private val doCheckDefaultBrowser = { checkDefaultBrowser }
Expand Down Expand Up @@ -229,6 +235,89 @@ class DefaultMetricsStorageTest {
assertTrue(result)
}

@Test
fun `GIVEN usage time has not passed threshold and has not been sent WHEN checking to track THEN event will not be sent`() = runTest(dispatcher) {
every { settings.usageTimeGrowthData } returns usageThresholdMillis - 1
every { settings.usageTimeGrowthSent } returns false

val result = storage.shouldTrack(Event.GrowthData.UsageThreshold)

assertFalse(result)
}

@Test
fun `GIVEN usage time has passed threshold and has not been sent WHEN checking to track THEN event will be sent`() = runTest(dispatcher) {
every { settings.usageTimeGrowthData } returns usageThresholdMillis + 1
every { settings.usageTimeGrowthSent } returns false

val result = storage.shouldTrack(Event.GrowthData.UsageThreshold)

assertTrue(result)
}

@Test
fun `GIVEN usage time growth has not been sent and within first day WHEN registering as usage recorder THEN will be registered`() {
val application = mockk<Application>()
every { settings.usageTimeGrowthSent } returns false
every { application.registerActivityLifecycleCallbacks(any()) } returns Unit

storage.tryRegisterAsUsageRecorder(application)

verify { application.registerActivityLifecycleCallbacks(any()) }
}

@Test
fun `GIVEN usage time growth has not been sent and not within first day WHEN registering as usage recorder THEN will not be registered`() {
val application = mockk<Application>()
installTime = System.currentTimeMillis() - dayMillis * 2
every { settings.usageTimeGrowthSent } returns false

storage.tryRegisterAsUsageRecorder(application)

verify(exactly = 0) { application.registerActivityLifecycleCallbacks(any()) }
}

@Test
fun `GIVEN usage time growth has been sent WHEN registering as usage recorder THEN will not be registered`() {
val application = mockk<Application>()
every { settings.usageTimeGrowthSent } returns true

storage.tryRegisterAsUsageRecorder(application)

verify(exactly = 0) { application.registerActivityLifecycleCallbacks(any()) }
}

@Test
fun `WHEN updating usage state THEN storage will be delegated to settings`() {
val initial = 10L
val update = 15L
val slot = slot<Long>()
every { settings.usageTimeGrowthData } returns initial
every { settings.usageTimeGrowthData = capture(slot) } returns Unit

storage.updateUsageState(update)

assertEquals(slot.captured, initial + update)
}

@Test
fun `WHEN usage recorder receives onResume and onPause callbacks THEN it will store usage length`() {
val storage = mockk<MetricsStorage>()
val activity = mockk<Activity>()
val slot = slot<Long>()
every { storage.updateUsageState(capture(slot)) } returns Unit
every { activity.componentName } returns mock()

val usageRecorder = DefaultMetricsStorage.UsageRecorder(storage)
val startTime = System.currentTimeMillis()

usageRecorder.onActivityResumed(activity)
usageRecorder.onActivityPaused(activity)
val stopTime = System.currentTimeMillis()

assertTrue(slot.captured < stopTime - startTime)
}

private fun Calendar.copy() = clone() as Calendar
private fun Calendar.createNextDay() = copy().apply {
add(Calendar.DAY_OF_MONTH, 1)
Expand Down

0 comments on commit 1b47977

Please sign in to comment.