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