Skip to content

Commit eeec7d5

Browse files
MozLandoMugurell
andcommitted
5228: Migrate fennec telemetry setting r=grigoryk a=Mugurell Introduces a new "FennecSettingsMigrator" which will be responsible for migrating all common Fennec - Fenix settings. Starting with the telemetry opt-in status which in Fenix will be "true" only if both "Telemetry" and "Firefox Health Report" were enabled on Fennec. Co-authored-by: Mugurell <[email protected]>
2 parents b056812 + 2b1b347 commit eeec7d5

File tree

5 files changed

+320
-1
lines changed

5 files changed

+320
-1
lines changed

components/support/migration/src/main/java/mozilla/components/support/migration/FennecMigrator.kt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import mozilla.components.lib.crash.CrashReporter
2121
import mozilla.components.service.fxa.manager.FxaAccountManager
2222
import mozilla.components.service.sync.logins.AsyncLoginsStorage
2323
import mozilla.components.support.base.log.logger.Logger
24+
import mozilla.components.support.migration.FennecMigrator.Builder
2425
import java.io.File
2526
import java.lang.Exception
2627
import java.util.concurrent.Executors
@@ -60,6 +61,11 @@ sealed class Migration(val currentVersion: Int) {
6061
* Migrates Gecko(View) internal files.
6162
*/
6263
object Gecko : Migration(currentVersion = 1)
64+
65+
/**
66+
* Migrates all Fennec settings backed by SharedPreferences.
67+
*/
68+
object Settings : Migration(currentVersion = 1)
6369
}
6470

6571
/**
@@ -106,11 +112,18 @@ sealed class FennecMigratorException(cause: Exception) : Exception(cause) {
106112
* @param cause Original exception which caused the problem.
107113
*/
108114
class MigrateOpenTabsException(cause: Exception) : FennecMigratorException(cause)
115+
109116
/**
110117
* Unexpected exception while migrating gecko profile.
111118
* @param cause Original exception which caused the problem.
112119
*/
113120
class MigrateGeckoException(cause: Exception) : FennecMigratorException(cause)
121+
122+
/**
123+
* Unexpected exception while migrating settings.
124+
* @param cause Original exception which caused the problem
125+
*/
126+
class MigrateSettingsException(cause: Exception) : FennecMigratorException(cause)
114127
}
115128

116129
/**
@@ -253,6 +266,14 @@ class FennecMigrator private constructor(
253266
return this
254267
}
255268

269+
/**
270+
* Enable all Fennec - Fenix common settings migration.
271+
*/
272+
fun migrateSettings(version: Int = Migration.Settings.currentVersion): Builder {
273+
migrations.add(VersionedMigration(Migration.Settings, version))
274+
return this
275+
}
276+
256277
/**
257278
* Constructs a [FennecMigrator] based on the current configuration.
258279
*/
@@ -395,6 +416,7 @@ class FennecMigrator private constructor(
395416
Migration.FxA -> migrateFxA()
396417
Migration.Gecko -> migrateGecko()
397418
Migration.Logins -> migrateLogins()
419+
Migration.Settings -> migrateSharedPrefs()
398420
}
399421

400422
results[versionedMigration.migration] = when (migrationResult) {
@@ -625,4 +647,36 @@ class FennecMigrator private constructor(
625647
Result.Failure(e)
626648
}
627649
}
650+
651+
private fun migrateSharedPrefs(): Result<SettingsMigrationResult> {
652+
val result = FennecSettingsMigration.migrateSharedPrefs(context)
653+
if (result is Result.Failure<SettingsMigrationResult>) {
654+
val migrationFailureWrapper = result.throwables.first() as SettingsMigrationException
655+
return when (val failure = migrationFailureWrapper.failure) {
656+
is SettingsMigrationResult.Failure.MissingFHRPrefValue -> {
657+
logger.error("Missing FHR value: $failure")
658+
crashReporter.submitCaughtException(migrationFailureWrapper)
659+
result
660+
}
661+
is SettingsMigrationResult.Failure.WrongTelemetryValueAfterMigration -> {
662+
logger.error("Wrong telemetry value: $failure")
663+
crashReporter.submitCaughtException(migrationFailureWrapper)
664+
result
665+
}
666+
}
667+
}
668+
669+
val migrationSuccess = result as Result.Success<SettingsMigrationResult>
670+
return when (val success = migrationSuccess.value as SettingsMigrationResult.Success) {
671+
// The rest are all successful migrations.
672+
is SettingsMigrationResult.Success.NoFennecPrefs -> {
673+
logger.debug("No Fennec prefs detected")
674+
result
675+
}
676+
is SettingsMigrationResult.Success.SettingsMigrated -> {
677+
logger.debug("Migrated settings; telemetry=${success.telemetry}")
678+
result
679+
}
680+
}
681+
}
628682
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.support.migration
6+
7+
import android.content.Context
8+
import android.content.Context.MODE_PRIVATE
9+
import android.content.SharedPreferences
10+
import androidx.annotation.VisibleForTesting
11+
import mozilla.components.support.base.log.logger.Logger
12+
13+
/**
14+
* Helper for migrating all common Fennec - Fenix settings.
15+
*/
16+
internal object FennecSettingsMigration {
17+
private val logger = Logger("FennecSettingsMigration")
18+
private lateinit var fenixAppPrefs: SharedPreferences
19+
private lateinit var fennecAppPrefs: SharedPreferences
20+
21+
@VisibleForTesting const val FENNEC_APP_SHARED_PREFS_NAME = "GeckoApp"
22+
@VisibleForTesting const val FENIX_SHARED_PREFS_NAME = "fenix_preferences"
23+
const val FENNEC_PREFS_FHR_KEY = "android.not_a_preference.healthreport.uploadEnabled"
24+
const val FENIX_PREFS_TELEMETRY_KEY = "pref_key_telemetry"
25+
26+
/**
27+
* Migrate all Fennec - Fenix common SharedPreferences.
28+
*/
29+
fun migrateSharedPrefs(context: Context): Result<SettingsMigrationResult> {
30+
fennecAppPrefs = context.getSharedPreferences(FENNEC_APP_SHARED_PREFS_NAME, MODE_PRIVATE)
31+
fenixAppPrefs = context.getSharedPreferences(FENIX_SHARED_PREFS_NAME, MODE_PRIVATE)
32+
33+
if (fennecAppPrefs.all.isEmpty()) {
34+
logger.info("No Fennec prefs, bailing out.")
35+
return Result.Success(SettingsMigrationResult.Success.NoFennecPrefs)
36+
}
37+
38+
return migrateAppPreferences()
39+
}
40+
41+
private fun migrateAppPreferences(): Result<SettingsMigrationResult> {
42+
return migrateTelemetryOptInStatus(fennecAppPrefs, fenixAppPrefs)
43+
}
44+
45+
private fun migrateTelemetryOptInStatus(
46+
fennecPrefs: SharedPreferences,
47+
fenixPrefs: SharedPreferences
48+
): Result<SettingsMigrationResult> {
49+
// Sanity check: make sure we actually have an FHR value set.
50+
if (!fennecPrefs.contains(FENNEC_PREFS_FHR_KEY)) {
51+
logger.warn("Missing FHR pref value")
52+
return Result.Failure(SettingsMigrationException(SettingsMigrationResult.Failure.MissingFHRPrefValue))
53+
}
54+
55+
// Fennec has two telemetry settings:
56+
// - Firefox Health Report (FHR) - defaults to 'on',
57+
// - Telemetry - defaults to 'off'.
58+
// These two settings control different parts of the telemetry systems in Fennec. The reality is that even
59+
// though default for "telemetry" is off, since FHR defaults to "on", by default Fennec collects a non-trivial
60+
// subset of available in-product telemetry.
61+
// Since the Telemetry pref defaults to 'off', it's impossible to distinguish between "user disabled telemetry"
62+
// and "user didn't touch the setting".
63+
// So we use FHR value as a proxy for telemetry overall.
64+
// If FHR is disabled by the user, we'll disable telemetry in Fenix. Otherwise, it will be enabled.
65+
66+
// Read Fennec prefs.
67+
val fennecFHRState = fennecPrefs.getBoolean(FENNEC_PREFS_FHR_KEY, false)
68+
logger.info("Fennec FHR state is: $fennecFHRState")
69+
70+
// Update Fenix prefs.
71+
fenixPrefs.edit()
72+
.putBoolean(FENIX_PREFS_TELEMETRY_KEY, fennecFHRState)
73+
.apply()
74+
75+
// Make sure it worked.
76+
if (fenixPrefs.getBoolean(FENIX_PREFS_TELEMETRY_KEY, !fennecFHRState) != fennecFHRState) {
77+
logger.warn("Wrong telemetry value after migration")
78+
return Result.Failure(
79+
SettingsMigrationException(
80+
SettingsMigrationResult.Failure.WrongTelemetryValueAfterMigration(expected = fennecFHRState)
81+
)
82+
)
83+
}
84+
85+
// Done!
86+
return Result.Success(SettingsMigrationResult.Success.SettingsMigrated(telemetry = fennecFHRState))
87+
}
88+
}
89+
90+
/**
91+
* Wraps [SettingsMigrationResult] in an exception so that it can be returned via [Result.Failure].
92+
*
93+
* @property failure Wrapped [SettingsMigrationResult] indicating exact failure reason.
94+
*/
95+
class SettingsMigrationException(val failure: SettingsMigrationResult.Failure) : Exception(failure.toString())
96+
97+
/**
98+
* Result of Fennec settings migration.
99+
*/
100+
sealed class SettingsMigrationResult {
101+
/**
102+
* Successful setting migration.
103+
*/
104+
sealed class Success : SettingsMigrationResult() {
105+
/**
106+
* Fennec app SharedPreference file is not accessible.
107+
*
108+
* This means this is a fresh install of Fenix, not an update from Fennec.
109+
* Nothing to migrate.
110+
*/
111+
object NoFennecPrefs : Success() {
112+
override fun toString(): String {
113+
return "No previous app settings. Nothing to migrate."
114+
}
115+
}
116+
117+
/**
118+
* Migration work completed successfully.
119+
*/
120+
data class SettingsMigrated(val telemetry: Boolean) : Success() {
121+
override fun toString(): String {
122+
return "Previous Fennec settings migrated successfully; value: $telemetry"
123+
}
124+
}
125+
}
126+
127+
/**
128+
* Failed settings migrations.
129+
*/
130+
sealed class Failure : SettingsMigrationResult() {
131+
/**
132+
* Couldn't find FHR pref value in non-empty Fennec prefs.
133+
*/
134+
object MissingFHRPrefValue : Failure() {
135+
override fun toString(): String {
136+
return "Missing FHR pref value"
137+
}
138+
}
139+
140+
/**
141+
* Wrong telemetry value in Fenix after migration.
142+
*/
143+
data class WrongTelemetryValueAfterMigration(val expected: Boolean) : Failure() {
144+
override fun toString(): String {
145+
return "Wrong telemetry pref value after migration. Expected $expected."
146+
}
147+
}
148+
}
149+
}

components/support/migration/src/main/java/mozilla/components/support/migration/MigrationResultsStore.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ internal class MigrationResultsStore(context: Context) : SharedPreferencesCache<
4343
}
4444
}
4545

46+
@Suppress("ComplexMethod")
4647
override fun fromJSON(obj: JSONObject): MigrationResults {
4748
val result = mutableMapOf<Migration, MigrationRun>()
4849
val list = obj.optJSONArray("list") ?: throw IllegalStateException("Corrupt migration history")
@@ -59,6 +60,7 @@ internal class MigrationResultsStore(context: Context) : SharedPreferencesCache<
5960
Migration.Gecko.javaClass.simpleName -> Migration.Gecko
6061
Migration.FxA.javaClass.simpleName -> Migration.FxA
6162
Migration.Logins.javaClass.simpleName -> Migration.Logins
63+
Migration.Settings.javaClass.simpleName -> Migration.Settings
6264
else -> throw IllegalStateException("Unrecognized migration type: $migrationName")
6365
}
6466
result[migration] = MigrationRun(version = migrationVersion, success = migrationSuccess)

components/support/migration/src/test/java/mozilla/components/support/migration/FennecMigratorTest.kt

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package mozilla.components.support.migration
66

7+
import android.content.Context
78
import androidx.test.ext.junit.runners.AndroidJUnit4
89
import kotlinx.coroutines.runBlocking
910
import mozilla.appservices.places.PlacesException
@@ -765,4 +766,46 @@ class FennecMigratorTest {
765766
loginStorage.ensureUnlocked("test storage key").await()
766767
assertEquals(0, loginStorage.list().await().size)
767768
}
768-
}
769+
770+
@Test
771+
fun `settings migration - no fennec prefs`() = runBlocking {
772+
// Fennec SharedPreferences are missing / empty
773+
val crashReporter: CrashReporter = mock()
774+
val migrator = FennecMigrator.Builder(testContext, crashReporter)
775+
.migrateSettings()
776+
.setCoroutineContext(this.coroutineContext)
777+
.build()
778+
779+
with(migrator.migrateAsync().await()) {
780+
assertEquals(1, this.size)
781+
assertTrue(this.containsKey(Migration.Settings))
782+
assertTrue(this.getValue(Migration.Settings).success)
783+
}
784+
verifyZeroInteractions(crashReporter)
785+
}
786+
787+
@Test
788+
fun `settings migration - missing FHR value`() = runBlocking {
789+
val fennecAppPrefs = testContext.getSharedPreferences(FennecSettingsMigration.FENNEC_APP_SHARED_PREFS_NAME, Context.MODE_PRIVATE)
790+
791+
// Make prefs non-empty.
792+
fennecAppPrefs.edit().putString("dummy", "key").apply()
793+
794+
val crashReporter: CrashReporter = mock()
795+
val migrator = FennecMigrator.Builder(testContext, crashReporter)
796+
.migrateSettings()
797+
.setCoroutineContext(this.coroutineContext)
798+
.build()
799+
800+
with(migrator.migrateAsync().await()) {
801+
assertEquals(1, this.size)
802+
assertTrue(this.containsKey(Migration.Settings))
803+
assertFalse(this.getValue(Migration.Settings).success)
804+
}
805+
val captor = argumentCaptor<Exception>()
806+
verify(crashReporter).submitCaughtException(captor.capture())
807+
808+
assertEquals(SettingsMigrationException::class, captor.value::class)
809+
assertEquals("Missing FHR pref value", captor.value.message)
810+
}
811+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.support.migration
6+
7+
import android.content.Context
8+
import androidx.test.ext.junit.runners.AndroidJUnit4
9+
import mozilla.components.support.migration.FennecSettingsMigration.FENIX_PREFS_TELEMETRY_KEY
10+
import mozilla.components.support.migration.FennecSettingsMigration.FENIX_SHARED_PREFS_NAME
11+
import mozilla.components.support.migration.FennecSettingsMigration.FENNEC_APP_SHARED_PREFS_NAME
12+
import mozilla.components.support.migration.FennecSettingsMigration.FENNEC_PREFS_FHR_KEY
13+
import mozilla.components.support.test.robolectric.testContext
14+
import org.junit.After
15+
import org.junit.Assert.assertEquals
16+
import org.junit.Test
17+
import org.junit.runner.RunWith
18+
import java.io.File
19+
20+
@RunWith(AndroidJUnit4::class)
21+
class FennecSettingsMigrationTest {
22+
@After
23+
fun `Clean up all SharedPreferences`() {
24+
File(testContext.filesDir.parent, "shared_prefs").delete()
25+
}
26+
27+
@Test
28+
fun `Missing Fennec preferences entirely is treated as success`() {
29+
with(FennecSettingsMigration.migrateSharedPrefs(testContext) as Result.Success) {
30+
assertEquals(SettingsMigrationResult.Success.NoFennecPrefs::class, this.value::class)
31+
}
32+
}
33+
34+
@Test
35+
fun `Missing FHR pref value is treated as failure`() {
36+
val fennecAppPrefs = testContext.getSharedPreferences(FENNEC_APP_SHARED_PREFS_NAME, Context.MODE_PRIVATE)
37+
38+
// Make prefs non-empty.
39+
fennecAppPrefs.edit().putString("dummy", "key").apply()
40+
41+
with(FennecSettingsMigration.migrateSharedPrefs(testContext) as Result.Failure) {
42+
val unwrapped = this.throwables.first() as SettingsMigrationException
43+
assertEquals(SettingsMigrationResult.Failure.MissingFHRPrefValue::class, unwrapped.failure::class)
44+
}
45+
}
46+
47+
@Test
48+
fun `Fennec FHR disabled will set Fenix telemetry as stopped and return SUCCESS`() {
49+
assertFHRMigration(isFHREnabled = false)
50+
}
51+
52+
@Test
53+
fun `Fennec FHR enabled will set Fenix telemetry as active and return SUCCESS`() {
54+
assertFHRMigration(isFHREnabled = true)
55+
}
56+
57+
private fun assertFHRMigration(isFHREnabled: Boolean) {
58+
val fennecAppPrefs = testContext.getSharedPreferences(FENNEC_APP_SHARED_PREFS_NAME, Context.MODE_PRIVATE)
59+
val fenixPrefs = testContext.getSharedPreferences(FENIX_SHARED_PREFS_NAME, Context.MODE_PRIVATE)
60+
61+
fennecAppPrefs.edit().putBoolean(FENNEC_PREFS_FHR_KEY, isFHREnabled).commit()
62+
63+
with(FennecSettingsMigration.migrateSharedPrefs(testContext) as Result.Success) {
64+
assertEquals(SettingsMigrationResult.Success.SettingsMigrated::class, this.value::class)
65+
val v = this.value as SettingsMigrationResult.Success.SettingsMigrated
66+
assertEquals(isFHREnabled, v.telemetry)
67+
}
68+
69+
assertEquals(isFHREnabled, fenixPrefs.getBoolean(FENIX_PREFS_TELEMETRY_KEY, !isFHREnabled))
70+
}
71+
}

0 commit comments

Comments
 (0)