Skip to content

Commit 87e3806

Browse files
Reedyukdblake78
andauthored
Android firebase integration (#12)
* Using in-house fork of Firebase Integration for Android * Converted integration to be kotlin first * Supressed lint * Linting issues resolved * Bump kotlin version to fix cinterop error * Opt in to experimental api. Required for build --------- Co-authored-by: Dave Blake <david.blake@myunidays.com>
1 parent c803e2f commit 87e3806

File tree

6 files changed

+260
-6
lines changed

6 files changed

+260
-6
lines changed

library/Segmenkt.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |spec|
22
spec.name = 'Segmenkt'
3-
spec.version = '0.3.2'
3+
spec.version = '0.3.3'
44
spec.homepage = ''
55
spec.source = { :http=> ''}
66
spec.authors = ''

library/build.gradle.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ kotlin {
9292
val jsTest by getting
9393
val androidMain by getting {
9494
dependencies {
95+
implementation("com.google.firebase:firebase-analytics:21.0.0")
9596
api("com.segment.analytics.android:analytics:4.10.4")
9697
api("com.appsflyer:segment-android-integration:6.5.2")
97-
api("com.segment.analytics.android.integrations:firebase:1.1.0")
9898
}
9999
}
100100
val androidUnitTest by getting {
@@ -108,6 +108,10 @@ kotlin {
108108
val iosTest by getting
109109
val iosSimulatorArm64Test by getting
110110
iosSimulatorArm64Test.dependsOn(iosTest)
111+
112+
all {
113+
languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi")
114+
}
111115
}
112116
}
113117

library/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ signing.keyId=""
22
signing.password=""
33

44
MODULE_PACKAGE_NAME=com.myunidays
5-
MODULE_VERSION_NUMBER=0.3.2
5+
MODULE_VERSION_NUMBER=0.3.3
66
MODULE_NAME=segmenkt
77

88
OPEN_SOURCE_REPO=https://oss.sonatype.org/service/local/staging/deploy/maven2/
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33

4+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
5+
<uses-permission android:name="android.permission.INTERNET" />
6+
<uses-permission android:name="android.permission.WAKE_LOCK" />
47
</manifest>

library/src/androidMain/kotlin/com/myunidays/segmenkt/FirebaseIntegration.kt

Lines changed: 249 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
package com.myunidays.segmenkt
22

3+
import android.Manifest
34
import android.app.Activity
5+
import android.content.Context
6+
import android.content.pm.PackageManager
47
import android.os.Bundle
8+
import com.google.firebase.analytics.FirebaseAnalytics
59
import com.myunidays.segmenkt.integrations.AliasPayload
610
import com.myunidays.segmenkt.integrations.GroupPayload
711
import com.myunidays.segmenkt.integrations.IdentifyPayload
812
import com.myunidays.segmenkt.integrations.Integration
913
import com.myunidays.segmenkt.integrations.IntegrationFactory
1014
import com.myunidays.segmenkt.integrations.ScreenPayload
1115
import com.myunidays.segmenkt.integrations.TrackPayload
16+
import com.segment.analytics.Analytics
17+
import com.segment.analytics.Properties
18+
import com.segment.analytics.ValueMap
19+
import com.segment.analytics.integrations.Logger
20+
import com.segment.analytics.internal.Utils
21+
import kotlin.math.min
1222

1323
@Suppress("TooManyFunctions")
1424
actual class FirebaseIntegration internal constructor(
15-
private val android: com.segment.analytics.integrations.Integration<*>
25+
private val android: AndroidFirebaseIntegration
1626
) : Integration<FirebaseIntegration> {
1727
fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) =
1828
android.onActivityCreated(activity, savedInstanceState)
@@ -38,6 +48,243 @@ actual class FirebaseIntegration internal constructor(
3848
actual fun factory(
3949
delegate: Any?,
4050
deeplinkHandler: Any?
41-
): IntegrationFactory = com.segment.analytics.android.integrations.firebase.FirebaseIntegration.FACTORY
51+
): IntegrationFactory = AndroidFirebaseIntegration.FACTORY
52+
}
53+
}
54+
55+
/**
56+
* A kotlin converted implementation of FirebaseIntegration https://github.com/segment-integrations/analytics-android-integration-firebase
57+
*/
58+
internal class AndroidFirebaseIntegration(context: Context?, private val logger: Logger) :
59+
com.segment.analytics.integrations.Integration<FirebaseAnalytics?>() {
60+
private val firebaseAnalytics = FirebaseAnalytics.getInstance(context!!)
61+
private var currentActivity: Activity? = null
62+
63+
override fun onActivityResumed(activity: Activity?) {
64+
super.onActivityResumed(activity)
65+
66+
val packageManager = activity?.packageManager
67+
try {
68+
val info =
69+
packageManager?.getActivityInfo(activity.componentName, PackageManager.GET_META_DATA)
70+
val activityLabel = info?.loadLabel(packageManager).toString()
71+
firebaseAnalytics.setCurrentScreen(activity!!, activityLabel, null)
72+
logger.verbose("firebaseAnalytics.setCurrentScreen(activity, %s, null);", activityLabel)
73+
} catch (e: PackageManager.NameNotFoundException) {
74+
throw AssertionError("Activity Not Found: $e")
75+
}
76+
}
77+
78+
override fun onActivityStarted(activity: Activity?) {
79+
super.onActivityStarted(activity)
80+
81+
this.currentActivity = activity
82+
}
83+
84+
override fun onActivityStopped(activity: Activity?) {
85+
super.onActivityStopped(activity)
86+
87+
this.currentActivity = null
88+
}
89+
90+
override fun identify(identify: com.segment.analytics.integrations.IdentifyPayload) {
91+
super.identify(identify)
92+
93+
if (!Utils.isNullOrEmpty(identify.userId())) {
94+
firebaseAnalytics.setUserId(identify.userId())
95+
}
96+
val traits: Map<String?, Any> = identify.traits()
97+
for (entry in traits.entries) {
98+
var trait = entry.key
99+
val value = entry.value.toString()
100+
trait = makeKey(trait)
101+
firebaseAnalytics.setUserProperty(trait, value)
102+
logger.verbose("firebaseAnalytics.setUserProperty(%s, %s);", trait, value)
103+
}
104+
}
105+
106+
override fun track(track: com.segment.analytics.integrations.TrackPayload) {
107+
super.track(track)
108+
109+
val event = track.event()
110+
val eventName = if (EVENT_MAPPER.containsKey(event)) {
111+
EVENT_MAPPER[event]
112+
} else {
113+
makeKey(event)
114+
}
115+
val properties = track.properties()
116+
val formattedProperties = formatProperties(properties)
117+
firebaseAnalytics.logEvent(eventName!!, formattedProperties)
118+
logger.verbose("firebaseAnalytics.logEvent(%s, %s);", eventName, formattedProperties)
119+
}
120+
121+
override fun screen(screen: com.segment.analytics.integrations.ScreenPayload) {
122+
super.screen(screen)
123+
124+
val propertiesBundle = Bundle()
125+
for ((key, value) in screen) {
126+
propertiesBundle.putString(key, value.toString())
127+
}
128+
propertiesBundle.putString(FirebaseAnalytics.Param.SCREEN_NAME, screen.name())
129+
firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, propertiesBundle)
130+
logger.verbose("firebaseAnalytics.screen(activity, %s, null);", screen.name())
131+
}
132+
133+
companion object {
134+
val FACTORY: Factory = object : Factory {
135+
@Suppress("ReturnCount")
136+
override fun create(
137+
settings: ValueMap,
138+
analytics: Analytics
139+
): com.segment.analytics.integrations.Integration<*>? {
140+
val logger = analytics.logger(FIREBASE_ANALYTICS_KEY)
141+
if (!Utils.hasPermission(
142+
analytics.application, Manifest.permission.ACCESS_NETWORK_STATE
143+
)
144+
) {
145+
logger.debug("ACCESS_NETWORK_STATE is required for Firebase Analytics.")
146+
return null
147+
}
148+
if (!Utils.hasPermission(analytics.application, Manifest.permission.WAKE_LOCK)) {
149+
logger.debug("WAKE_LOCK is required for Firebase Analytics.")
150+
return null
151+
}
152+
153+
val context: Context = analytics.application
154+
155+
return AndroidFirebaseIntegration(context, logger)
156+
}
157+
158+
override fun key(): String {
159+
return FIREBASE_ANALYTICS_KEY
160+
}
161+
}
162+
163+
private const val FIREBASE_ANALYTICS_KEY = "Firebase"
164+
private val EVENT_MAPPER = createEventMap()
165+
private fun createEventMap(): Map<String, String> {
166+
val EVENT_MAPPER: MutableMap<String, String> = HashMap()
167+
EVENT_MAPPER["Product Added"] = FirebaseAnalytics.Event.ADD_TO_CART
168+
EVENT_MAPPER["Checkout Started"] = FirebaseAnalytics.Event.BEGIN_CHECKOUT
169+
EVENT_MAPPER["Order Completed"] = FirebaseAnalytics.Event.PURCHASE
170+
EVENT_MAPPER["Order Refunded"] = FirebaseAnalytics.Event.REFUND
171+
EVENT_MAPPER["Product Viewed"] = FirebaseAnalytics.Event.VIEW_ITEM
172+
EVENT_MAPPER["Product List Viewed"] = FirebaseAnalytics.Event.VIEW_ITEM_LIST
173+
EVENT_MAPPER["Payment Info Entered"] = FirebaseAnalytics.Event.ADD_PAYMENT_INFO
174+
EVENT_MAPPER["Promotion Viewed"] = FirebaseAnalytics.Event.VIEW_PROMOTION
175+
EVENT_MAPPER["Product Added to Wishlist"] = FirebaseAnalytics.Event.ADD_TO_WISHLIST
176+
EVENT_MAPPER["Product Shared"] = FirebaseAnalytics.Event.SHARE
177+
EVENT_MAPPER["Product Clicked"] = FirebaseAnalytics.Event.SELECT_CONTENT
178+
EVENT_MAPPER["Products Searched"] = FirebaseAnalytics.Event.SEARCH
179+
return EVENT_MAPPER
180+
}
181+
182+
private val PROPERTY_MAPPER = createPropertyMap()
183+
184+
private fun createPropertyMap(): Map<String?, String> {
185+
val PROPERTY_MAPPER: MutableMap<String?, String> = HashMap()
186+
PROPERTY_MAPPER["category"] = FirebaseAnalytics.Param.ITEM_CATEGORY
187+
PROPERTY_MAPPER["product_id"] = FirebaseAnalytics.Param.ITEM_ID
188+
PROPERTY_MAPPER["name"] = FirebaseAnalytics.Param.ITEM_NAME
189+
PROPERTY_MAPPER["price"] = FirebaseAnalytics.Param.PRICE
190+
PROPERTY_MAPPER["quantity"] = FirebaseAnalytics.Param.QUANTITY
191+
PROPERTY_MAPPER["query"] = FirebaseAnalytics.Param.SEARCH_TERM
192+
PROPERTY_MAPPER["shipping"] = FirebaseAnalytics.Param.SHIPPING
193+
PROPERTY_MAPPER["tax"] = FirebaseAnalytics.Param.TAX
194+
PROPERTY_MAPPER["total"] = FirebaseAnalytics.Param.VALUE
195+
PROPERTY_MAPPER["revenue"] = FirebaseAnalytics.Param.VALUE
196+
PROPERTY_MAPPER["order_id"] = FirebaseAnalytics.Param.TRANSACTION_ID
197+
PROPERTY_MAPPER["currency"] = FirebaseAnalytics.Param.CURRENCY
198+
PROPERTY_MAPPER["products"] = FirebaseAnalytics.Param.ITEMS
199+
return PROPERTY_MAPPER
200+
}
201+
202+
private val PRODUCT_MAPPER = createProductMap()
203+
204+
private fun createProductMap(): Map<String?, String> {
205+
val MAPPER: MutableMap<String?, String> = HashMap()
206+
MAPPER["category"] = FirebaseAnalytics.Param.ITEM_CATEGORY
207+
MAPPER["product_id"] = FirebaseAnalytics.Param.ITEM_ID
208+
MAPPER["id"] = FirebaseAnalytics.Param.ITEM_ID
209+
MAPPER["name"] = FirebaseAnalytics.Param.ITEM_NAME
210+
MAPPER["price"] = FirebaseAnalytics.Param.PRICE
211+
MAPPER["quantity"] = FirebaseAnalytics.Param.QUANTITY
212+
return MAPPER
213+
}
214+
215+
private fun formatProperties(properties: Properties): Bundle {
216+
val bundle = Bundle()
217+
if ((properties.revenue() != 0.0 || properties.total() != 0.0) && Utils.isNullOrEmpty(properties.currency())
218+
) {
219+
bundle.putString(FirebaseAnalytics.Param.CURRENCY, "USD")
220+
}
221+
for (entry in properties.entries) {
222+
val value = entry.value
223+
var property = entry.key
224+
property = if (PROPERTY_MAPPER.containsKey(property)) {
225+
PROPERTY_MAPPER[property]
226+
} else {
227+
makeKey(property)
228+
}
229+
if (property == FirebaseAnalytics.Param.ITEMS && value != null) {
230+
val products = properties.getList(
231+
"products",
232+
ValueMap::class.java
233+
)
234+
val mappedProducts = formatProducts(products)
235+
bundle.putParcelableArrayList(property, mappedProducts)
236+
} else {
237+
putValue(bundle, property, value)
238+
}
239+
}
240+
return bundle
241+
}
242+
243+
private fun formatProducts(products: List<ValueMap>?): ArrayList<Bundle> {
244+
val mappedProducts = ArrayList<Bundle>()
245+
if (products == null) return mappedProducts
246+
247+
for (product in products) {
248+
val mappedProduct = Bundle()
249+
for (innerEntry in product.entries) {
250+
var key = innerEntry.key
251+
val value = innerEntry.value
252+
key = if (PRODUCT_MAPPER.containsKey(key)) {
253+
PRODUCT_MAPPER[key]
254+
} else {
255+
makeKey(key)
256+
}
257+
putValue(mappedProduct, key, value)
258+
}
259+
mappedProducts.add(mappedProduct)
260+
}
261+
return mappedProducts
262+
}
263+
264+
private fun putValue(bundle: Bundle, key: String?, value: Any?) {
265+
if (value is Int) {
266+
bundle.putInt(key, value)
267+
} else if (value is Double) {
268+
bundle.putDouble(key, value)
269+
} else if (value is Long) {
270+
bundle.putLong(key, value)
271+
} else {
272+
val stringValue = value.toString()
273+
bundle.putString(key, stringValue)
274+
}
275+
}
276+
277+
@Suppress("MagicNumber")
278+
fun makeKey(key: String?): String {
279+
var key = key
280+
val forbiddenChars = arrayOf(".", "-", " ", ":")
281+
for (forbidden in forbiddenChars) {
282+
if (key!!.contains(forbidden)) {
283+
key = key.trim { it <= ' ' }.replace(forbidden, "_")
284+
}
285+
}
286+
287+
return key!!.substring(0, min(key.length.toDouble(), 40.0).toInt())
288+
}
42289
}
43290
}

settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import java.util.Properties
22

33
dependencyResolutionManagement {
44
versionCatalogs {
5-
val kotlinVersion = "1.9.10"
5+
val kotlinVersion = "1.9.23"
66
create("libs") {
77
version("kotlin", kotlinVersion)
88

0 commit comments

Comments
 (0)