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/. */
5 package org.mozilla.fenix.components.metrics
7 import android.app.Application
8 import android.util.Log
9 import com.leanplum.Leanplum
10 import com.leanplum.LeanplumActivityHelper
11 import com.leanplum.annotations.Parser
12 import com.leanplum.internal.LeanplumInternal
13 import kotlinx.coroutines.CoroutineScope
14 import kotlinx.coroutines.Dispatchers
15 import kotlinx.coroutines.Dispatchers.Main
16 import kotlinx.coroutines.Job
17 import kotlinx.coroutines.launch
18 import kotlinx.coroutines.withContext
19 import mozilla.components.support.locale.LocaleManager
20 import org.mozilla.fenix.BuildConfig
21 import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts
22 import org.mozilla.fenix.ext.settings
23 import java.util.Locale
24 import java.util.UUID.randomUUID
26 private val Event.name: String?
28 is Event.AddBookmark -> "E_Add_Bookmark"
29 is Event.RemoveBookmark -> "E_Remove_Bookmark"
30 is Event.OpenedBookmark -> "E_Opened_Bookmark"
31 is Event.OpenedApp -> "E_Opened_App"
32 is Event.OpenedAppFirstRun -> "E_Opened_App_FirstRun"
33 is Event.InteractWithSearchURLArea -> "E_Interact_With_Search_URL_Area"
34 is Event.CollectionSaved -> "E_Collection_Created"
35 is Event.CollectionTabRestored -> "E_Collection_Tab_Opened"
36 is Event.SyncAuthSignIn -> "E_Sign_In_FxA"
37 is Event.SyncAuthSignOut -> "E_Sign_Out_FxA"
38 is Event.ClearedPrivateData -> "E_Cleared_Private_Data"
39 is Event.DismissedOnboarding -> "E_Dismissed_Onboarding"
40 is Event.FennecToFenixMigrated -> "E_Fennec_To_Fenix_Migrated"
41 is Event.AddonInstalled -> "E_Addon_Installed"
42 is Event.SearchWidgetInstalled -> "E_Search_Widget_Added"
43 is Event.ChangedToDefaultBrowser -> "E_Changed_Default_To_Fenix"
44 is Event.TrackingProtectionSettingChanged -> "E_Changed_ETP"
46 // Do not track other events in Leanplum
50 class LeanplumMetricsService(private val application: Application) : MetricsService {
51 val scope = CoroutineScope(Dispatchers.IO)
52 var leanplumJob: Job? = null
54 data class Token(val id: String, val token: String) {
55 enum class Type { Development, Production, Invalid }
59 token.take(ProdPrefix.length) == ProdPrefix -> Type.Production
60 token.take(DevPrefix.length) == DevPrefix -> Type.Development
66 private const val ProdPrefix = "prod"
67 private const val DevPrefix = "dev"
71 override val type = MetricServiceType.Marketing
72 private val token = Token(LeanplumId, LeanplumToken)
74 override fun start() {
76 if (!application.settings().isMarketingTelemetryEnabled) return
78 Leanplum.setIsTestModeEnabled(false)
79 Leanplum.setApplicationContext(application)
80 Leanplum.setDeviceId(randomUUID().toString())
81 Parser.parseVariables(application)
83 leanplumJob = scope.launch {
85 val applicationSetLocale = LocaleManager.getCurrentLocale(application)
86 val currentLocale = when (applicationSetLocale != null) {
87 true -> applicationSetLocale.isO3Language
88 false -> Locale.getDefault().isO3Language
90 if (!isLeanplumEnabled(currentLocale)) {
91 Log.i(LOGTAG, "Leanplum is not available for this locale: $currentLocale")
96 Token.Type.Production -> Leanplum.setAppIdForProductionMode(token.id, token.token)
97 Token.Type.Development -> Leanplum.setAppIdForDevelopmentMode(token.id, token.token)
98 Token.Type.Invalid -> {
99 Log.i(LOGTAG, "Invalid or missing Leanplum token")
104 LeanplumActivityHelper.enableLifecycleCallbacks(application)
106 val installedApps = MozillaProductDetector.getInstalledMozillaProducts(application)
108 val trackingProtection = application.settings().run {
110 !shouldUseTrackingProtection -> "none"
111 useStandardTrackingProtection -> "standard"
112 useStrictTrackingProtection -> "strict"
117 Leanplum.start(application, hashMapOf(
118 "default_browser" to MozillaProductDetector.getMozillaBrowserDefault(application).orEmpty(),
119 "fennec_installed" to installedApps.contains(MozillaProducts.FIREFOX.productName),
120 "focus_installed" to installedApps.contains(MozillaProducts.FOCUS.productName),
121 "klar_installed" to installedApps.contains(MozillaProducts.KLAR.productName),
122 "fxa_signed_in" to application.settings().fxaSignedIn,
123 "fxa_has_synced_items" to application.settings().fxaHasSyncedItems,
124 "search_widget_installed" to application.settings().searchWidgetInstalled,
125 "tracking_protection_enabled" to application.settings().shouldUseTrackingProtection,
126 "tracking_protection_setting" to trackingProtection,
131 LeanplumInternal.setCalledStart(true)
132 LeanplumInternal.setHasStarted(true)
133 LeanplumInternal.setStartedInBackground(true)
138 override fun stop() {
139 if (application.settings().isMarketingTelemetryEnabled) return
140 // As written in LeanPlum SDK documentation, "This prevents Leanplum from communicating with the server."
141 // as this "isTestMode" flag is checked before LeanPlum SDK does anything.
142 // Also has the benefit effect of blocking the display of already downloaded messages.
143 // The reverse of this - setIsTestModeEnabled(false) must be called before trying to init
144 // LP in the same session.
145 Leanplum.setIsTestModeEnabled(true)
147 // This is just to allow restarting LP and it's functionality in the same app session
148 // as LP stores it's state internally and check against it
149 LeanplumInternal.setCalledStart(false)
150 LeanplumInternal.setHasStarted(false)
151 leanplumJob?.cancel()
154 override fun track(event: Event) {
155 val leanplumExtras = event.extras
156 ?.map { (key, value) -> key.toString() to value }
160 Leanplum.track(it, leanplumExtras)
164 override fun shouldTrack(event: Event): Boolean {
165 return application.settings().isTelemetryEnabled &&
166 token.type != Token.Type.Invalid && !event.name.isNullOrEmpty()
169 private fun isLeanplumEnabled(locale: String): Boolean {
170 return LEANPLUM_ENABLED_LOCALES.contains(locale)
174 private const val LOGTAG = "LeanplumMetricsService"
176 private val LeanplumId: String
177 // Debug builds have a null (nullable) LEANPLUM_ID
178 get() = BuildConfig.LEANPLUM_ID.orEmpty()
179 private val LeanplumToken: String
180 // Debug builds have a null (nullable) LEANPLUM_TOKEN
181 get() = BuildConfig.LEANPLUM_TOKEN.orEmpty()
182 // Leanplum needs to be enabled for the following locales.
183 // Irrespective of the actual device location.
184 private val LEANPLUM_ENABLED_LOCALES = listOf(
192 "spa", // Spanish; Castilian