Skip to content

Commit 3acfe26

Browse files
feat: Allow Client to Manually Call Appsflyer Start
1 parent 541195a commit 3acfe26

3 files changed

Lines changed: 229 additions & 81 deletions

File tree

src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ class AppsFlyerKit :
6262
MParticle.getInstance()?.environment == MParticle.Environment.Development,
6363
)
6464
settings[DEV_KEY]?.let { AppsFlyerLib.getInstance().init(it, this, context) }
65-
setting?.get(SHARING_FILTER_FOR_PARTNERS)?.let {
66-
applySharingFilterForPartners(it)
67-
}
6865
val userConsentState = currentUser?.consentState
6966
setConsent(userConsentState)
70-
AppsFlyerLib.getInstance().start(context.applicationContext)
67+
updateCustomerUserIdFromMpid()
68+
if (!isManualStart()) {
69+
AppsFlyerLib.getInstance().start(context.applicationContext)
70+
}
7171
AppsFlyerLib.getInstance().setCollectAndroidID(MParticle.isAndroidIdEnabled())
7272
val integrationAttributes = HashMap<String, String?>(1)
7373
integrationAttributes[APPSFLYERID_INTEGRATION_KEY] =
@@ -307,7 +307,9 @@ class AppsFlyerKit :
307307

308308
override fun removeUserIdentity(identityType: MParticle.IdentityType) {
309309
with(instance) {
310-
if (MParticle.IdentityType.CustomerId == identityType) {
310+
if (isUserIdentificationMpid()) {
311+
updateCustomerUserIdFromMpid()
312+
} else if (MParticle.IdentityType.CustomerId == identityType) {
311313
setCustomerUserId("")
312314
} else if (MParticle.IdentityType.Email == identityType) {
313315
setUserEmails(AppsFlyerProperties.EmailsCryptType.NONE, "")
@@ -320,7 +322,9 @@ class AppsFlyerKit :
320322
identity: String,
321323
) {
322324
with(instance) {
323-
if (MParticle.IdentityType.CustomerId == identityType) {
325+
if (isUserIdentificationMpid()) {
326+
updateCustomerUserIdFromMpid()
327+
} else if (MParticle.IdentityType.CustomerId == identityType) {
324328
setCustomerUserId(identity)
325329
} else if (MParticle.IdentityType.Email == identityType) {
326330
setUserEmails(AppsFlyerProperties.EmailsCryptType.NONE, identity)
@@ -559,7 +563,9 @@ class AppsFlyerKit :
559563
activity: Activity,
560564
bundle: Bundle?,
561565
): List<ReportingMessage> {
562-
instance.start(activity)
566+
if (!isManualStart()) {
567+
instance.start(activity)
568+
}
563569
return emptyList()
564570
}
565571

@@ -578,30 +584,16 @@ class AppsFlyerKit :
578584

579585
override fun onActivityDestroyed(activity: Activity): List<ReportingMessage> = emptyList()
580586

581-
override fun onSettingsUpdated(settings: Map<String, String>) {
582-
settings[SHARING_FILTER_FOR_PARTNERS]?.let { applySharingFilterForPartners(it) }
583-
}
587+
private fun isManualStart(): Boolean =
588+
settings[MANUAL_START]?.lowercase() == "true"
584589

585-
private fun applySharingFilterForPartners(jsonValue: String) {
586-
val partners = parseSharingFilterForPartners(jsonValue)
587-
if (!partners.isNullOrEmpty()) {
588-
instance.setSharingFilterForPartners(*partners.toTypedArray())
589-
}
590-
}
590+
private fun isUserIdentificationMpid(): Boolean =
591+
settings[USER_IDENTIFICATION_TYPE] == USER_IDENTIFICATION_MPID
591592

592-
private fun parseSharingFilterForPartners(json: String?): List<String>? {
593-
if (json.isNullOrEmpty()) return null
594-
return try {
595-
val jsonWithFormat = json.replace("\\", "")
596-
val array = JSONArray(jsonWithFormat)
597-
List(array.length()) { i -> array.getString(i) }
598-
} catch (e: JSONException) {
599-
Logger.warning(
600-
"AppsFlyer kit: failed to parse sharingFilterForPartners, " +
601-
"consent filter for partners will not be applied. Error: ${e.message}",
602-
)
603-
null
604-
}
593+
private fun updateCustomerUserIdFromMpid() {
594+
if (!isUserIdentificationMpid()) return
595+
val mpid = currentUser?.id ?: return
596+
instance.setCustomerUserId(mpid.toString())
605597
}
606598

607599
companion object {
@@ -630,7 +622,9 @@ class AppsFlyerKit :
630622
}
631623
}
632624

633-
private const val SHARING_FILTER_FOR_PARTNERS = "sharingFilterForPartners"
625+
const val MANUAL_START = "manualStart"
626+
const val USER_IDENTIFICATION_TYPE = "userIdentificationType"
627+
const val USER_IDENTIFICATION_MPID = "MPID"
634628
private const val CONSENT_MAPPING = "consentMapping"
635629

636630
@Suppress("ktlint:standard:property-naming")

src/test/kotlin/com/appsflyer/AppsFlyerLib.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,36 @@ import android.content.Context
55
class AppsFlyerLib {
66
private var consentData: AppsFlyerConsent? = null
77

8+
var startCallCount = 0
9+
private set
10+
11+
var customerUserId: String? = null
12+
private set
13+
814
fun setConsentData(consent: AppsFlyerConsent) {
915
consentData = consent
1016
}
1117

1218
fun getConsentData(): AppsFlyerConsent? = consentData
1319

20+
fun start(context: Context) {
21+
startCallCount++
22+
}
23+
24+
fun setCustomerUserId(id: String?) {
25+
customerUserId = id
26+
}
27+
28+
fun init(devKey: String, conversionListener: Any?, context: Context) {}
29+
30+
fun setCollectAndroidID(collect: Boolean) {}
31+
32+
fun getAppsFlyerUID(context: Context): String = "test-appsflyer-uid"
33+
34+
fun subscribeForDeepLink(listener: Any?) {}
35+
36+
fun setDebugLog(debug: Boolean) {}
37+
1438
fun getConsentState(): MutableMap<Any, Any> {
1539
val stateMap = mutableMapOf<Any, Any>()
1640
consentData?.let { consent ->

src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt

Lines changed: 181 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -76,57 +76,6 @@ class AppsflyerKitTests {
7676
Assert.assertTrue(name.isNotEmpty())
7777
}
7878

79-
@Test
80-
@Throws(Exception::class)
81-
fun testParseSharingFilterForPartners_returnsListForValidJson() {
82-
val method =
83-
AppsFlyerKit::class.java.getDeclaredMethod(
84-
"parseSharingFilterForPartners",
85-
String::class.java,
86-
)
87-
method.isAccessible = true
88-
val result = method.invoke(kit, """["partner_1", "partner_2"]""")
89-
Assert.assertEquals(listOf("partner_1", "partner_2"), result)
90-
}
91-
92-
@Test
93-
@Throws(Exception::class)
94-
fun testParseSharingFilterForPartners_returnsNullForEmptyInput() {
95-
val method =
96-
AppsFlyerKit::class.java.getDeclaredMethod(
97-
"parseSharingFilterForPartners",
98-
String::class.java,
99-
)
100-
method.isAccessible = true
101-
Assert.assertNull(method.invoke(kit, ""))
102-
Assert.assertNull(method.invoke(kit, null))
103-
}
104-
105-
@Test
106-
@Throws(Exception::class)
107-
fun testParseSharingFilterForPartners_returnsNullForInvalidJson() {
108-
val method =
109-
AppsFlyerKit::class.java.getDeclaredMethod(
110-
"parseSharingFilterForPartners",
111-
String::class.java,
112-
)
113-
method.isAccessible = true
114-
Assert.assertNull(method.invoke(kit, "not a json array"))
115-
}
116-
117-
@Test
118-
@Throws(Exception::class)
119-
fun testParseSharingFilterForPartners_stripsBackslashes() {
120-
val method =
121-
AppsFlyerKit::class.java.getDeclaredMethod(
122-
"parseSharingFilterForPartners",
123-
String::class.java,
124-
)
125-
method.isAccessible = true
126-
val result = method.invoke(kit, """[\"test_1\", \"test_2\"]""")
127-
Assert.assertEquals(listOf("test_1", "test_2"), result)
128-
}
129-
13079
/**
13180
* Kit *should* throw an exception when they're initialized with the wrong settings.
13281
*
@@ -776,6 +725,187 @@ class AppsflyerKitTests {
776725
Assert.assertEquals(emptyMap<String, String>(), result)
777726
}
778727

728+
// region manualStart and userIdentificationType (MPID) tests
729+
730+
@Test
731+
@Throws(Exception::class)
732+
fun testIsManualStart_returnsTrueWhenSettingIsTrue() {
733+
setKitSettings(mapOf(AppsFlyerKit.MANUAL_START to "true"))
734+
val method = AppsFlyerKit::class.java.getDeclaredMethod("isManualStart")
735+
method.isAccessible = true
736+
Assert.assertTrue(method.invoke(kit) as Boolean)
737+
}
738+
739+
@Test
740+
@Throws(Exception::class)
741+
fun testIsManualStart_returnsFalseWhenSettingIsFalse() {
742+
setKitSettings(mapOf(AppsFlyerKit.MANUAL_START to "false"))
743+
val method = AppsFlyerKit::class.java.getDeclaredMethod("isManualStart")
744+
method.isAccessible = true
745+
Assert.assertFalse(method.invoke(kit) as Boolean)
746+
}
747+
748+
@Test
749+
@Throws(Exception::class)
750+
fun testIsManualStart_returnsFalseWhenSettingIsAbsent() {
751+
setKitSettings(emptyMap())
752+
val method = AppsFlyerKit::class.java.getDeclaredMethod("isManualStart")
753+
method.isAccessible = true
754+
Assert.assertFalse(method.invoke(kit) as Boolean)
755+
}
756+
757+
@Test
758+
@Throws(Exception::class)
759+
fun testIsUserIdentificationMpid_returnsTrueWhenSettingIsMPID() {
760+
setKitSettings(
761+
mapOf(
762+
AppsFlyerKit.USER_IDENTIFICATION_TYPE to AppsFlyerKit.USER_IDENTIFICATION_MPID,
763+
),
764+
)
765+
val method =
766+
AppsFlyerKit::class.java.getDeclaredMethod("isUserIdentificationMpid")
767+
method.isAccessible = true
768+
Assert.assertTrue(method.invoke(kit) as Boolean)
769+
}
770+
771+
@Test
772+
@Throws(Exception::class)
773+
fun testIsUserIdentificationMpid_returnsFalseWhenSettingIsAbsent() {
774+
setKitSettings(emptyMap())
775+
val method =
776+
AppsFlyerKit::class.java.getDeclaredMethod("isUserIdentificationMpid")
777+
method.isAccessible = true
778+
Assert.assertFalse(method.invoke(kit) as Boolean)
779+
}
780+
781+
@Test
782+
@Throws(Exception::class)
783+
fun testIsUserIdentificationMpid_returnsFalseWhenSettingIsOther() {
784+
setKitSettings(mapOf(AppsFlyerKit.USER_IDENTIFICATION_TYPE to "CustomerId"))
785+
val method =
786+
AppsFlyerKit::class.java.getDeclaredMethod("isUserIdentificationMpid")
787+
method.isAccessible = true
788+
Assert.assertFalse(method.invoke(kit) as Boolean)
789+
}
790+
791+
@Test
792+
@Throws(Exception::class)
793+
fun testOnKitCreate_withManualStartTrue_doesNotCallStart() {
794+
val settings = hashMapOf<String?, String?>()
795+
settings[AppsFlyerKit.DEV_KEY] = "testDevKey"
796+
settings[AppsFlyerKit.MANUAL_START] = "true"
797+
MParticle.setInstance(mock(MParticle::class.java))
798+
Mockito.`when`(MParticle.getInstance()?.environment)
799+
.thenReturn(MParticle.Environment.Production)
800+
Mockito.`when`(MParticle.isAndroidIdEnabled()).thenReturn(false)
801+
802+
kit.onKitCreate(settings, mock(Context::class.java))
803+
804+
Assert.assertEquals(0, appsflyer.startCallCount)
805+
}
806+
807+
@Test
808+
@Throws(Exception::class)
809+
fun testOnKitCreate_withManualStartFalse_callsStart() {
810+
val settings = hashMapOf<String?, String?>()
811+
settings[AppsFlyerKit.DEV_KEY] = "testDevKey"
812+
settings[AppsFlyerKit.MANUAL_START] = "false"
813+
MParticle.setInstance(mock(MParticle::class.java))
814+
Mockito.`when`(MParticle.getInstance()?.environment)
815+
.thenReturn(MParticle.Environment.Production)
816+
Mockito.`when`(MParticle.isAndroidIdEnabled()).thenReturn(false)
817+
818+
kit.onKitCreate(settings, mock(Context::class.java))
819+
820+
Assert.assertEquals(1, appsflyer.startCallCount)
821+
}
822+
823+
@Test
824+
@Throws(Exception::class)
825+
fun testOnKitCreate_withManualStartUnset_callsStart() {
826+
val settings = hashMapOf<String?, String?>()
827+
settings[AppsFlyerKit.DEV_KEY] = "testDevKey"
828+
MParticle.setInstance(mock(MParticle::class.java))
829+
Mockito.`when`(MParticle.getInstance()?.environment)
830+
.thenReturn(MParticle.Environment.Production)
831+
Mockito.`when`(MParticle.isAndroidIdEnabled()).thenReturn(false)
832+
833+
kit.onKitCreate(settings, mock(Context::class.java))
834+
835+
Assert.assertEquals(1, appsflyer.startCallCount)
836+
}
837+
838+
@Test
839+
@Throws(Exception::class)
840+
fun testOnActivityCreated_withManualStartTrue_doesNotCallStart() {
841+
setKitSettings(
842+
mapOf(
843+
AppsFlyerKit.MANUAL_START to "true",
844+
),
845+
)
846+
kit.onActivityCreated(mock(Activity::class.java), null)
847+
Assert.assertEquals(0, appsflyer.startCallCount)
848+
}
849+
850+
@Test
851+
@Throws(Exception::class)
852+
fun testOnActivityCreated_withManualStartFalse_callsStart() {
853+
setKitSettings(
854+
mapOf(
855+
AppsFlyerKit.MANUAL_START to "false",
856+
),
857+
)
858+
kit.onActivityCreated(mock(Activity::class.java), null)
859+
Assert.assertEquals(1, appsflyer.startCallCount)
860+
}
861+
862+
@Test
863+
@Throws(Exception::class)
864+
fun testSetUserIdentity_withUserIdentificationMpid_setsCustomerUserIdToMpid() {
865+
setKitSettings(
866+
mapOf(
867+
AppsFlyerKit.USER_IDENTIFICATION_TYPE to AppsFlyerKit.USER_IDENTIFICATION_MPID,
868+
),
869+
)
870+
val mpid = 12345L
871+
Mockito.`when`(user.getId()).thenReturn(mpid)
872+
filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit)
873+
setKitCurrentUser(filteredMParticleUser)
874+
875+
kit.setUserIdentity(MParticle.IdentityType.CustomerId, "custom-client-id")
876+
877+
Assert.assertEquals("12345", appsflyer.customerUserId)
878+
}
879+
880+
@Test
881+
@Throws(Exception::class)
882+
fun testSetUserIdentity_withoutUserIdentificationMpid_setsCustomerUserIdToIdentity() {
883+
setKitSettings(emptyMap())
884+
setKitCurrentUser(null)
885+
886+
kit.setUserIdentity(MParticle.IdentityType.CustomerId, "custom-client-id")
887+
888+
Assert.assertEquals("custom-client-id", appsflyer.customerUserId)
889+
}
890+
891+
@Throws(Exception::class)
892+
private fun setKitSettings(settings: Map<String, String>) {
893+
val settingsField = KitIntegration::class.java.getDeclaredField("settings")
894+
settingsField.isAccessible = true
895+
val settingsMap = mutableMapOf<String?, String?>()
896+
settings.forEach { (k, v) -> settingsMap[k] = v }
897+
settingsField.set(kit, settingsMap)
898+
}
899+
900+
@Throws(Exception::class)
901+
private fun setKitCurrentUser(user: FilteredMParticleUser?) {
902+
val field = KitIntegration::class.java.getDeclaredField("currentUser")
903+
field.isAccessible = true
904+
field.set(kit, user)
905+
}
906+
907+
// endregion
908+
779909
private var emptyCoreCallbacks: CoreCallbacks =
780910
object : CoreCallbacks {
781911
var activity = Activity()

0 commit comments

Comments
 (0)