Skip to content

Commit 0d09344

Browse files
committed
Merge branch 'master' into release/7.0.0
2 parents 49a80ec + 645ea41 commit 0d09344

File tree

9 files changed

+520
-30
lines changed

9 files changed

+520
-30
lines changed

AndroidSDKCore/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ dependencies {
4242
api "androidx.legacy:legacy-support-v4:1.0.0"
4343
api "androidx.appcompat:appcompat:${APPCOMPAT_LIBRARY_VERSION}"
4444

45-
api "com.clevertap.android:clevertap-android-sdk:4.6.4"
45+
api "com.clevertap.android:clevertap-android-sdk:4.6.5"
4646
}
4747

4848
task generateJavadoc(type: Javadoc) {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.clevertap.android.sdk
2+
3+
import android.annotation.SuppressLint
4+
import com.clevertap.android.sdk.task.CTExecutorFactory
5+
import com.clevertap.android.sdk.task.Task
6+
7+
object CTUtils {
8+
9+
@SuppressLint("RestrictedApi")
10+
fun ensureLocalDataStoreValue(key: String, cleverTapApi: CleverTapAPI) {
11+
val value = cleverTapApi.coreState.localDataStore.getProfileValueForKey(key)
12+
if (value == null) {
13+
cleverTapApi.coreState.localDataStore.setProfileField(key, "")
14+
}
15+
}
16+
17+
@SuppressLint("RestrictedApi")
18+
fun addMultiValueForKey(key: String, value: String, cleverTapApi: CleverTapAPI) {
19+
CTExecutorFactory
20+
.executors(cleverTapApi.coreState.config)
21+
.postAsyncSafelyTask<Task<Void>>()
22+
.execute("CTUtils") {
23+
ensureLocalDataStoreValue(key, cleverTapApi)
24+
cleverTapApi.addMultiValueForKey(key, value)
25+
null
26+
}
27+
}
28+
29+
}

AndroidSDKCore/src/main/java/com/leanplum/migration/MigrationConstants.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import com.leanplum.internal.Log
2727
object MigrationConstants {
2828
const val IDENTITY = "Identity"
2929
const val STATE_PREFIX = "state_"
30+
const val ANONYMOUS_DEVICE_PROPERTY = "lp_device"
31+
const val DEVICES_USER_PROPERTY = "lp_devices"
3032

3133
const val CHARGED_EVENT_PARAM = "event"
3234
const val VALUE_PARAM = "value"

AndroidSDKCore/src/main/java/com/leanplum/migration/wrapper/CTWrapper.kt

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import android.app.Application
2525
import android.content.Context
2626
import android.text.TextUtils
2727
import com.clevertap.android.sdk.ActivityLifecycleCallback
28+
import com.clevertap.android.sdk.CTUtils
2829
import com.clevertap.android.sdk.CleverTapAPI
2930
import com.clevertap.android.sdk.CleverTapInstanceConfig
3031
import com.clevertap.android.sdk.pushnotification.PushConstants
@@ -54,8 +55,8 @@ internal class CTWrapper(
5455
private var cleverTapInstance: CleverTapAPI? = null
5556
private var instanceCallback: CleverTapInstanceCallback? = null
5657

57-
private var firstTimeStart = IdentityManager.isStateUndefined()
5858
private var identityManager = IdentityManager(deviceId, userId ?: deviceId)
59+
private var firstTimeStart = identityManager.isFirstTimeStart()
5960

6061
override fun launch(context: Context, callback: CleverTapInstanceCallback?) {
6162
instanceCallback = callback
@@ -84,9 +85,11 @@ internal class CTWrapper(
8485
}
8586
if (identityManager.isAnonymous()) {
8687
Log.d("Wrapper: identity not set for anonymous user")
88+
setAnonymousDeviceProperty()
8789
} else {
8890
Log.d("Wrapper: will call onUserLogin with $profile and __h$cleverTapId")
8991
onUserLogin(profile, cleverTapId)
92+
setDevicesProperty()
9093
}
9194
Log.d("Wrapper: CleverTap instance created by Leanplum")
9295
}
@@ -142,15 +145,34 @@ internal class CTWrapper(
142145

143146
override fun setUserId(userId: String?) {
144147
if (userId == null || userId.isEmpty()) return
145-
if (identityManager.getUserId() == userId) return
146148

147-
identityManager.setUserId(userId)
149+
if (!identityManager.setUserId(userId)) {
150+
// trying to set same userId
151+
return
152+
}
148153

149154
val cleverTapId = identityManager.cleverTapId()
150155
val profile = identityManager.profile()
151156

152157
Log.d("Wrapper: Leanplum.setUserId will call onUserLogin with $profile and __h$cleverTapId")
153158
cleverTapInstance?.onUserLogin(profile, cleverTapId)
159+
cleverTapInstance?.setDevicesProperty()
160+
}
161+
162+
private fun CleverTapAPI.setAnonymousDeviceProperty() {
163+
if (identityManager.isDeviceIdHashed()) {
164+
val deviceId = identityManager.getOriginalDeviceId()
165+
Log.d("Wrapper: property ${MigrationConstants.ANONYMOUS_DEVICE_PROPERTY} set $deviceId")
166+
pushProfile(mapOf(MigrationConstants.ANONYMOUS_DEVICE_PROPERTY to deviceId))
167+
}
168+
}
169+
170+
private fun CleverTapAPI.setDevicesProperty() {
171+
if (identityManager.isDeviceIdHashed()) {
172+
val deviceId = identityManager.getOriginalDeviceId()
173+
Log.d("Wrapper: property ${MigrationConstants.DEVICES_USER_PROPERTY} add $deviceId")
174+
CTUtils.addMultiValueForKey(MigrationConstants.DEVICES_USER_PROPERTY, deviceId, this)
175+
}
154176
}
155177

156178
/**

AndroidSDKCore/src/main/java/com/leanplum/migration/wrapper/IdentityManager.kt

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,17 @@ import com.leanplum.internal.Log
2525
import com.leanplum.migration.MigrationConstants
2626
import com.leanplum.utils.StringPreference
2727
import com.leanplum.utils.StringPreferenceNullable
28+
import kotlin.properties.ReadWriteProperty
29+
30+
private const val UNDEFINED = "undefined"
31+
private const val ANONYMOUS = "anonymous"
32+
private const val IDENTIFIED = "identified"
2833

2934
/**
3035
* Scheme for migrating user profile is as follows:
3136
* - anonymous is translated to <CTID=deviceId, Identity=null>
32-
* - non-anonymous to <CTID=deviceId_userId, Identity=userId>
37+
* - non-anonymous to <CTID=deviceId_hash(userId), Identity=userId>
38+
* Where deviceId is also hashed if it is longer than 50 characters or contains invalid symbols.
3339
*
3440
* When you login, but previous profile is anonymous, a merge should happen. CT SDK allows merges
3541
* only when the CTID remains the same, meaning that the merged profile would get the anonymous
@@ -42,7 +48,7 @@ import com.leanplum.utils.StringPreferenceNullable
4248
* 1. "undefined" state
4349
*
4450
* Wrapper hasn't been started even once, meaning that anonymous profile doesn't exist, so use the
45-
* "deviceId_userId" scheme.
51+
* "deviceId_hash(userId)" scheme.
4652
*
4753
* 2. "anonymous" state
4854
*
@@ -51,32 +57,33 @@ import com.leanplum.utils.StringPreferenceNullable
5157
*
5258
* 3. "identified" state
5359
*
54-
* Wrapper has been started and previous user is not anonymous - use the "deviceId_userId" scheme.
60+
* Wrapper has been started and previous user is not anonymous - use the "deviceId_hash(userId)"
61+
* scheme.
5562
*/
56-
internal class IdentityManager(
57-
private val deviceId: String,
58-
private var userId: String
63+
class IdentityManager(
64+
deviceId: String,
65+
userId: String,
66+
stateDelegate: ReadWriteProperty<Any, String> = StringPreference("ct_login_state", UNDEFINED),
67+
mergeUserDelegate: ReadWriteProperty<Any, String?> = StringPreferenceNullable("ct_anon_merge_userid"),
5968
) {
6069

61-
companion object {
62-
private const val UNDEFINED = "undefined"
63-
private const val ANONYMOUS = "anonymous"
64-
private const val IDENTIFIED = "identified"
65-
66-
private var anonymousMergeUserId: String? by StringPreferenceNullable("ct_anon_merge_userid")
67-
private var state: String by StringPreference("ct_login_state", UNDEFINED)
68-
fun isStateUndefined() = state == UNDEFINED
69-
}
70+
private val identity: LPIdentity = LPIdentity(deviceId = deviceId, userId = userId)
71+
private var state: String by stateDelegate
72+
private val startState: String
73+
private var anonymousMergeUserId: String? by mergeUserDelegate
7074

7175
init {
76+
startState = state
7277
if (isAnonymous()) {
7378
loginAnonymously()
7479
} else {
7580
loginIdentified()
7681
}
7782
}
7883

79-
fun isAnonymous() = userId == deviceId
84+
fun isAnonymous() = identity.isAnonymous()
85+
86+
fun isFirstTimeStart() = startState == UNDEFINED
8087

8188
private fun loginAnonymously() {
8289
state = ANONYMOUS
@@ -87,31 +94,40 @@ internal class IdentityManager(
8794
state = IDENTIFIED
8895
}
8996
else if (state == ANONYMOUS) {
90-
anonymousMergeUserId = userId
97+
anonymousMergeUserId = identity.userId()
9198
Log.d("Wrapper: anonymous data will be merged to $anonymousMergeUserId")
9299
state = IDENTIFIED
93100
}
94101
}
95102

96103
fun cleverTapId(): String {
97-
return when (userId) {
98-
deviceId -> deviceId
99-
anonymousMergeUserId -> deviceId
100-
else -> "${deviceId}_${userId}"
104+
if (identity.isAnonymous()) {
105+
return identity.deviceId()
106+
} else if (identity.userId() == anonymousMergeUserId) {
107+
return identity.deviceId()
108+
} else {
109+
return "${identity.deviceId()}_${identity.userId()}"
101110
}
102111
}
103112

104-
fun profile() = mapOf(MigrationConstants.IDENTITY to userId)
113+
fun profile() = mapOf(MigrationConstants.IDENTITY to identity.originalUserId())
114+
115+
fun setUserId(userId: String): Boolean {
116+
if (!identity.setUserId(userId)) {
117+
// trying to set same userId
118+
return false
119+
}
105120

106-
fun setUserId(userId: String) {
107121
if (state == ANONYMOUS) {
108-
anonymousMergeUserId = userId
122+
anonymousMergeUserId = identity.userId()
109123
Log.d("Wrapper: anonymous data will be merged to $anonymousMergeUserId")
110124
state = IDENTIFIED
111125
}
112-
this.userId = userId
126+
return true;
113127
}
114128

115-
fun getUserId() = userId
129+
fun isDeviceIdHashed() = identity.originalDeviceId() != identity.deviceId()
130+
131+
fun getOriginalDeviceId() = identity.originalDeviceId()
116132

117133
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.leanplum.migration.wrapper
2+
3+
import com.clevertap.android.sdk.Utils
4+
import com.leanplum.utils.HashUtil
5+
6+
private const val DEVICE_ID_MAX_LENGTH = 50
7+
8+
class LPIdentity(
9+
private val deviceId: String,
10+
private var userId: String
11+
) {
12+
13+
private var deviceIdHash: String? = null
14+
private var userIdHash: String? = null
15+
16+
init {
17+
if (deviceId.length > DEVICE_ID_MAX_LENGTH || !Utils.validateCTID(deviceId)) {
18+
deviceIdHash = HashUtil.sha256_200(deviceId)
19+
}
20+
userIdHash = HashUtil.sha256_40(userId)
21+
}
22+
23+
fun deviceId() = deviceIdHash ?: deviceId
24+
fun originalDeviceId() = deviceId
25+
fun userId() = userIdHash
26+
fun originalUserId() = userId
27+
fun isAnonymous() = userId == deviceId
28+
29+
fun setUserId(userId: String): Boolean {
30+
if (this.userId == userId) {
31+
return false
32+
}
33+
this.userId = userId
34+
this.userIdHash = HashUtil.sha256_40(userId)
35+
return true
36+
}
37+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.leanplum.utils
2+
3+
import java.security.MessageDigest
4+
5+
object HashUtil {
6+
7+
private fun ByteArray.toHex(limit: Int): String =
8+
joinToString(separator = "", limit = limit, truncated = "") { eachByte ->
9+
"%02x".format(eachByte)
10+
}
11+
12+
/**
13+
* Returns hexadecimal representation of sha256 on the input string.
14+
*/
15+
fun sha256(text: String, limit: Int = 32): String {
16+
return MessageDigest
17+
.getInstance("SHA-256")
18+
.digest(text.toByteArray(Charsets.UTF_8))
19+
.toHex(limit)
20+
}
21+
22+
/**
23+
* Get first 10 symbols from hexadecimal representation of sha256.
24+
*/
25+
fun sha256_40(text: String) = sha256(text, 5)
26+
27+
/**
28+
* Get first 50 symbols from hexadecimal representation of sha256.
29+
*/
30+
fun sha256_200(text: String) = sha256(text, 25)
31+
}

0 commit comments

Comments
 (0)