1+ package com.segment.analytics.destinations.plugins
2+
3+ import android.app.Activity
4+ import android.content.Context
5+ import com.segment.analytics.kotlin.core.*
6+ import com.segment.analytics.kotlin.core.platform.DestinationPlugin
7+ import com.segment.analytics.kotlin.core.platform.Plugin
8+ import kotlinx.serialization.Serializable
9+ import com.appsflyer.AppsFlyerLib
10+ import com.appsflyer.AppsFlyerConversionListener
11+ import android.content.SharedPreferences
12+ import android.os.Bundle
13+ import com.appsflyer.AFInAppEventParameterName
14+ import com.segment.analytics.kotlin.android.plugins.AndroidLifecycle
15+ import com.segment.analytics.kotlin.core.platform.plugins.LogType
16+ import com.segment.analytics.kotlin.core.platform.plugins.log
17+ import com.appsflyer.deeplink.DeepLinkListener
18+ import com.segment.analytics.kotlin.core.utilities.getString
19+ import com.segment.analytics.kotlin.core.utilities.mapTransform
20+ import com.segment.analytics.kotlin.core.utilities.toContent
21+ import kotlinx.serialization.json.*
22+
23+ /*
24+ This is an example of the AppsFlyer device-mode destination plugin that can be integrated with
25+ Segment analytics.
26+ Note: This plugin is NOT SUPPORTED by Segment. It is here merely as an example,
27+ and for your convenience should you find it useful.
28+ To use it in your codebase, we suggest copying this file over and include the following
29+ dependencies in your `build.gradle` file:
30+ ```
31+ dependencies {
32+ ...
33+ implementation 'com.appsflyer:af-android-sdk:6.3.2'
34+ implementation 'com.android.installreferrer:installreferrer:2.2'
35+ }
36+ ```
37+ MIT License
38+ Copyright (c) 2021 Segment
39+ Permission is hereby granted, free of charge, to any person obtaining a copy
40+ of this software and associated documentation files (the "Software"), to deal
41+ in the Software without restriction, including without limitation the rights
42+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
43+ copies of the Software, and to permit persons to whom the Software is
44+ furnished to do so, subject to the following conditions:
45+ The above copyright notice and this permission notice shall be included in all
46+ copies or substantial portions of the Software.
47+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
48+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
49+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
50+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
51+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
52+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
53+ SOFTWARE.
54+ */
55+
56+ @Serializable
57+ data class AppsFlyerSettings (
58+ var appsFlyerDevKey : String ,
59+ var trackAttributionData : Boolean = false
60+ )
61+
62+ class AppsFlyerDestination (
63+ private val applicationContext : Context ,
64+ private var isDebug : Boolean = false
65+ ) : DestinationPlugin(), AndroidLifecycle {
66+
67+ internal var settings: AppsFlyerSettings ? = null
68+ internal var appsflyer: AppsFlyerLib ? = null
69+
70+ internal var customerUserId: String = " "
71+ internal var currencyCode: String = " "
72+ var conversionListener: ExternalAppsFlyerConversionListener ? = null
73+ var deepLinkListener: ExternalDeepLinkListener ? = null
74+
75+ override val key: String = " AppsFlyer"
76+
77+ override fun setup (analytics : Analytics ) {
78+ super .setup(analytics)
79+ }
80+
81+ override fun update (settings : Settings , type : Plugin .UpdateType ) {
82+ super .update(settings, type)
83+ if (settings.isDestinationEnabled(key)) {
84+ analytics.log(" Appsflyer Destination is enabled" )
85+ this .settings = settings.destinationSettings(key)
86+ if (type == Plugin .UpdateType .Initial ) {
87+ appsflyer = AppsFlyerLib .getInstance()
88+ analytics.log(" Appsflyer Destination loaded" )
89+ var listener: AppsFlyerConversionListener ? = null
90+ this .settings?.let {
91+ if (it.trackAttributionData) {
92+ listener = ConversionListener ()
93+ }
94+ appsflyer?.setDebugLog(isDebug)
95+ appsflyer?.init (it.appsFlyerDevKey, listener, applicationContext)
96+ }
97+ }
98+ deepLinkListener?.let {
99+ appsflyer?.subscribeForDeepLink(it)
100+ }
101+ }
102+ }
103+
104+ override fun identify (payload : IdentifyEvent ): BaseEvent ? {
105+ val userId: String = payload.userId
106+ val traits: JsonObject = payload.traits
107+
108+ customerUserId = userId
109+ currencyCode = traits.getString(" currencyCode" ) ? : " "
110+
111+ updateEndUserAttributes()
112+
113+ return payload
114+ }
115+
116+ override fun track (payload : TrackEvent ): BaseEvent ? {
117+ val event: String = payload.event
118+ val properties: Properties = payload.properties
119+
120+ val afProperties = properties.mapTransform(propertiesMapper).mapValues { (_, v) -> v.toContent() }
121+
122+ appsflyer?.logEvent(applicationContext, event, afProperties)
123+ analytics.log(" appsflyer.logEvent(context, $event , $properties )" , type = LogType .INFO )
124+ return payload
125+ }
126+
127+ override fun onActivityCreated (activity : Activity ? , savedInstanceState : Bundle ? ) {
128+ super .onActivityCreated(activity, savedInstanceState)
129+ if (activity != null ) {
130+ AppsFlyerLib .getInstance().start(activity)
131+ analytics.log(" AppsFlyerLib.getInstance().start($activity )" )
132+ }
133+ updateEndUserAttributes()
134+ }
135+
136+ private fun updateEndUserAttributes () {
137+ appsflyer?.setCustomerUserId(customerUserId)
138+ analytics.log(" appsflyer.setCustomerUserId($customerUserId )" , type = LogType .INFO )
139+ appsflyer?.setCurrencyCode(currencyCode)
140+ analytics.log(" appsflyer.setCurrencyCode($currencyCode )" , type = LogType .INFO )
141+ appsflyer?.setDebugLog(isDebug)
142+ analytics.log(" appsflyer.setDebugLog($isDebug )" , type = LogType .INFO )
143+ }
144+
145+
146+ companion object {
147+ const val AF_SEGMENT_SHARED_PREF = " appsflyer-segment-data"
148+ const val CONV_KEY = " AF_onConversion_Data"
149+ }
150+
151+ private val propertiesMapper = mapOf (
152+ " revenue" to AFInAppEventParameterName .REVENUE ,
153+ " price" to AFInAppEventParameterName .PRICE ,
154+ " currency" to AFInAppEventParameterName .CURRENCY
155+ )
156+
157+ interface ExternalAppsFlyerConversionListener : AppsFlyerConversionListener
158+ interface ExternalDeepLinkListener : DeepLinkListener
159+
160+ inner class ConversionListener : AppsFlyerConversionListener {
161+ override fun onConversionDataSuccess (conversionData : Map <String , Any >) {
162+ if (! getFlag(CONV_KEY )) {
163+ trackInstallAttributed(conversionData)
164+ setFlag(CONV_KEY , true )
165+ }
166+ conversionListener?.onConversionDataSuccess(conversionData)
167+ }
168+
169+ override fun onConversionDataFail (errorMessage : String ) {
170+ conversionListener?.onConversionDataFail(errorMessage)
171+ }
172+
173+ override fun onAppOpenAttribution (attributionData : Map <String , String >) {
174+ conversionListener?.onAppOpenAttribution(attributionData)
175+ }
176+
177+ override fun onAttributionFailure (errorMessage : String ) {
178+ conversionListener?.onAttributionFailure(errorMessage)
179+ }
180+
181+ private fun convertToPrimitive (value : Any? ): JsonElement {
182+ return when (value) {
183+ is Boolean -> JsonPrimitive (value)
184+ is Number -> JsonPrimitive (value)
185+ is String -> JsonPrimitive (value)
186+ is Map <* , * > -> buildJsonObject {
187+ value.forEach { (k, v) ->
188+ put(k.toString(), convertToPrimitive(v))
189+ }
190+ }
191+ is List <* > -> buildJsonArray {
192+ value.forEach { v ->
193+ add(convertToPrimitive(v))
194+ }
195+ }
196+ is Array <* > -> buildJsonArray {
197+ value.forEach { v ->
198+ add(convertToPrimitive(v))
199+ }
200+ }
201+ else -> JsonPrimitive (value.toString())
202+ }
203+ }
204+
205+ private fun trackInstallAttributed (attributionData : Map <String , Any >) {
206+ // See https://segment.com/docs/spec/mobile/#install-attributed.
207+ val properties = buildJsonObject {
208+ put(" provider" , key)
209+ attributionData.forEach { (k, v) ->
210+ if (k !in setOf (" media_source" , " adgroup" )) {
211+ put(k, convertToPrimitive(v))
212+ }
213+ }
214+ put(" campaign" , buildJsonObject {
215+ put(" source" , convertToPrimitive(attributionData[" media_source" ]))
216+ put(" name" , convertToPrimitive(attributionData[" campaign" ]))
217+ put(" ad_group" , convertToPrimitive(attributionData[" adgroup" ]))
218+ })
219+ }
220+
221+ // If you are working with networks that don't allow passing user level data to 3rd parties,
222+ // you will need to apply code to filter out these networks before calling
223+ // `analytics.track("Install Attributed", properties);`
224+ analytics.track(" Install Attributed" , properties)
225+ }
226+
227+ private fun getFlag (key : String ): Boolean {
228+ val sharedPreferences: SharedPreferences =
229+ applicationContext.getSharedPreferences(AF_SEGMENT_SHARED_PREF , 0 )
230+ return sharedPreferences.getBoolean(key, false )
231+ }
232+
233+ private fun setFlag (key : String , value : Boolean ) {
234+ val sharedPreferences: SharedPreferences =
235+ applicationContext.getSharedPreferences(AF_SEGMENT_SHARED_PREF , 0 )
236+ val editor = sharedPreferences.edit()
237+ editor.putBoolean(key, value).apply ()
238+ }
239+ }
240+
241+ }
0 commit comments