Skip to content

Commit

Permalink
fix: track opens fcm notification payload
Browse files Browse the repository at this point in the history
  • Loading branch information
levibostian committed Jun 13, 2022
1 parent 79ebdec commit ab3cd18
Show file tree
Hide file tree
Showing 28 changed files with 484 additions and 140 deletions.
Expand Up @@ -18,6 +18,7 @@ fun Date.add(unit: Long, type: TimeUnit): Date {
return Date(this.time + type.toMillis(unit))
}

fun Date.subtract(unit: Double, type: TimeUnit): Date = this.subtract(unit.toLong(), type)
fun Date.subtract(unit: Int, type: TimeUnit): Date = this.subtract(unit.toLong(), type)
fun Date.subtract(unit: Long, type: TimeUnit): Date {
return Date(this.time - type.toMillis(unit))
Expand All @@ -26,3 +27,7 @@ fun Date.subtract(unit: Long, type: TimeUnit): Date {
fun Date.hasPassed(): Boolean {
return this.time < Date().time
}

fun Date.isOlderThan(otherDate: Date): Boolean {
return this.time < otherDate.time
}
Expand Up @@ -3,9 +3,12 @@ package io.customer.base.extensions
import io.customer.base.extenstions.add
import io.customer.base.extenstions.getUnixTimestamp
import io.customer.base.extenstions.hasPassed
import io.customer.base.extenstions.isOlderThan
import io.customer.base.extenstions.subtract
import io.customer.base.extenstions.unixTimeToDate
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.junit.Test
import java.text.SimpleDateFormat
import java.util.*
Expand Down Expand Up @@ -61,4 +64,14 @@ class DateExtensionsTest {
fun hasPassed_givenDateInFuture_expectFalse() {
Date().add(1, TimeUnit.MINUTES).hasPassed() shouldBeEqualTo false
}

@Test
fun isOlderThan_givenDateThatIsOlder_expectTrue() {
Date().subtract(2, TimeUnit.DAYS).isOlderThan(Date().subtract(1, TimeUnit.DAYS)).shouldBeTrue()
}

@Test
fun isOlderThan_givenDateThatIsNewer_expectFalse() {
Date().subtract(1, TimeUnit.DAYS).isOlderThan(Date().subtract(2, TimeUnit.DAYS)).shouldBeFalse()
}
}
17 changes: 12 additions & 5 deletions common-test/src/main/java/io/customer/common_test/BaseTest.kt
Expand Up @@ -4,14 +4,16 @@ import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import io.customer.common_test.util.DispatchersProviderStub
import io.customer.sdk.CustomerIOConfig
import io.customer.sdk.data.model.Region
import io.customer.sdk.data.store.DeviceStore
import io.customer.sdk.di.CustomerIOComponent
import io.customer.sdk.util.CioLogLevel
import io.customer.sdk.util.DateUtil
import io.customer.sdk.util.DispatchersProvider
import io.customer.sdk.util.JsonAdapter
import kotlinx.coroutines.test.TestCoroutineDispatcher
import io.customer.sdk.util.Seconds
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
Expand All @@ -36,9 +38,7 @@ abstract class BaseTest {
protected lateinit var cioConfig: CustomerIOConfig

protected val deviceStore: DeviceStore = DeviceStoreStub().deviceStore

// when you need a CoroutineDispatcher in a test function, use this as it runs your tests synchronous.
protected val testDispatcher = TestCoroutineDispatcher()
protected lateinit var dispatchersProviderStub: DispatchersProviderStub

protected lateinit var di: CustomerIOComponent
protected val jsonAdapter: JsonAdapter
Expand All @@ -53,13 +53,16 @@ abstract class BaseTest {

@Before
open fun setup() {
cioConfig = CustomerIOConfig(siteId, "xyz", Region.EU, 100, null, true, true, 10, 30.0, CioLogLevel.DEBUG, null)
cioConfig = CustomerIOConfig(siteId, "xyz", Region.EU, 100, null, true, true, 10, 30.0, Seconds.fromDays(3).value, CioLogLevel.DEBUG, null)

// Initialize the mock web server before constructing DI graph as dependencies may require information such as hostname.
mockWebServer = MockWebServer().apply {
start()
}
cioConfig.trackingApiUrl = mockWebServer.url("/").toString()
if (!cioConfig.trackingApiUrl!!.contains("localhost")) {
throw RuntimeException("server didnt' start ${cioConfig.trackingApiUrl}")
}

di = CustomerIOComponent(
sdkConfig = cioConfig,
Expand All @@ -71,10 +74,14 @@ abstract class BaseTest {
dateUtilStub = DateUtilStub().also {
di.overrideDependency(DateUtil::class.java, it)
}
dispatchersProviderStub = DispatchersProviderStub().also {
di.overrideDependency(DispatchersProvider::class.java, it)
}
}

@After
open fun teardown() {
mockWebServer.shutdown()
di.reset()
}
}
@@ -1,22 +1,20 @@
package io.customer.common_test

import io.customer.base.extenstions.unixTimeToDate
import io.customer.base.extenstions.getUnixTimestamp
import io.customer.sdk.util.DateUtil
import java.util.*

/**
* Convenient alternative to mocking [DateUtil] in your test since the code is boilerplate.
*/
class DateUtilStub : DateUtil {
// modify this value in your test class if you need to.
var givenDateMillis = 1646238885L

val givenDate: Date
get() = givenDateMillis.unixTimeToDate()
// modify this value in your test class if you need to.
var givenDate: Date = Date(1646238885L)

override val now: Date
get() = givenDate

override val nowUnixTimestamp: Long
get() = givenDateMillis
get() = now.getUnixTimestamp()
}
@@ -0,0 +1,29 @@
package io.customer.common_test.util

import io.customer.sdk.util.DispatchersProvider
import io.customer.sdk.util.SdkDispatchers
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher

class DispatchersProviderStub : DispatchersProvider {
private var overrideBackground: CoroutineDispatcher? = null
private var overrideMain: CoroutineDispatcher? = null

// If your test function requires real dispatchers to be used, call this function.
// the default behavior is test dispatchers because they are fast and synchronous for more predictable test execution.
fun setRealDispatchers() {
SdkDispatchers().also {
overrideBackground = it.background
overrideMain = it.main
}
}

@OptIn(ExperimentalCoroutinesApi::class)
override val background: CoroutineDispatcher
get() = overrideBackground ?: TestCoroutineDispatcher()

@OptIn(ExperimentalCoroutinesApi::class)
override val main: CoroutineDispatcher
get() = overrideMain ?: TestCoroutineDispatcher()
}
Expand Up @@ -16,6 +16,8 @@ import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.customer.sdk.data.request.MetricEvent
import io.customer.sdk.CustomerIO
import io.customer.sdk.util.PushTrackingUtilImpl.Companion.DELIVERY_ID_KEY
import io.customer.sdk.util.PushTrackingUtilImpl.Companion.DELIVERY_TOKEN_KEY
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
Expand All @@ -27,9 +29,6 @@ internal class CustomerIOPushNotificationHandler(private val remoteMessage: Remo
companion object {
private const val TAG = "NotificationHandler:"

const val DELIVERY_ID = "CIO-Delivery-ID"
const val DELIVERY_TOKEN = "CIO-Delivery-Token"

const val DEEP_LINK_KEY = "link"
const val IMAGE_KEY = "image"
const val TITLE_KEY = "title"
Expand Down Expand Up @@ -60,19 +59,15 @@ internal class CustomerIOPushNotificationHandler(private val remoteMessage: Remo
// Customer.io push notifications include data regarding the push
// message in the data part of the payload which can be used to send
// feedback into our system.
val deliveryId = bundle.getString(DELIVERY_ID)
val deliveryToken = bundle.getString(DELIVERY_TOKEN)
val deliveryId = bundle.getString(DELIVERY_ID_KEY)
val deliveryToken = bundle.getString(DELIVERY_TOKEN_KEY)

if (deliveryId != null && deliveryToken != null) {
try {
CustomerIO.instance().trackMetric(
deliveryID = deliveryId,
deviceToken = deliveryToken,
event = MetricEvent.delivered
)
} catch (exception: Exception) {
Log.e(TAG, "Error while handling message: ${exception.message}")
}
CustomerIO.instance().trackMetric(
deliveryID = deliveryId,
deviceToken = deliveryToken,
event = MetricEvent.delivered
)
} else {
// not a CIO push notification
return false
Expand Down
Expand Up @@ -7,13 +7,13 @@ import android.content.Intent
import android.net.Uri
import android.util.Log
import io.customer.messagingpush.CustomerIOPushNotificationHandler.Companion.DEEP_LINK_KEY
import io.customer.messagingpush.CustomerIOPushNotificationHandler.Companion.DELIVERY_ID
import io.customer.messagingpush.CustomerIOPushNotificationHandler.Companion.DELIVERY_TOKEN
import io.customer.messagingpush.CustomerIOPushNotificationHandler.Companion.NOTIFICATION_REQUEST_CODE
import io.customer.sdk.data.request.MetricEvent
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOConfig
import io.customer.sdk.di.CustomerIOComponent
import io.customer.sdk.util.PushTrackingUtilImpl.Companion.DELIVERY_ID_KEY
import io.customer.sdk.util.PushTrackingUtilImpl.Companion.DELIVERY_TOKEN_KEY

internal class CustomerIOPushReceiver : BroadcastReceiver() {

Expand All @@ -40,8 +40,8 @@ internal class CustomerIOPushReceiver : BroadcastReceiver() {
mNotificationManager.cancel(requestCode)

val bundle = intent.extras
val deliveryId = bundle?.getString(DELIVERY_ID)
val deliveryToken = bundle?.getString(DELIVERY_TOKEN)
val deliveryId = bundle?.getString(DELIVERY_ID_KEY)
val deliveryToken = bundle?.getString(DELIVERY_TOKEN_KEY)

if (deliveryId != null && deliveryToken != null) {
CustomerIO.instance().trackMetric(deliveryId, MetricEvent.opened, deliveryToken)
Expand Down
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="run Android tests" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<configuration default="false" name="run Android tests" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests" singleton="true">
<module name="Customer.io_SDK.sdk" />
<option name="TESTING_TYPE" value="1" />
<option name="METHOD_NAME" value="" />
Expand All @@ -11,9 +11,9 @@
<option name="RETENTION_ENABLED" value="No" />
<option name="RETENTION_MAX_SNAPSHOTS" value="2" />
<option name="RETENTION_COMPRESS_SNAPSHOTS" value="false" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="CLEAR_LOGCAT" value="true" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="false" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
Expand Down
22 changes: 20 additions & 2 deletions sdk/src/main/java/io/customer/sdk/CustomerIO.kt
Expand Up @@ -8,11 +8,15 @@ import io.customer.sdk.data.communication.CustomerIOUrlHandler
import io.customer.sdk.data.model.Region
import io.customer.sdk.data.request.MetricEvent
import io.customer.sdk.di.CustomerIOComponent
import io.customer.sdk.repository.CleanupRepository
import io.customer.sdk.extensions.getScreenNameFromActivity
import io.customer.sdk.repository.DeviceRepository
import io.customer.sdk.repository.ProfileRepository
import io.customer.sdk.repository.TrackRepository
import io.customer.sdk.util.CioLogLevel
import io.customer.sdk.util.Seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/**
* Allows mocking of [CustomerIO] for your automated tests in your project. Mock [CustomerIO] to assert your code is calling functions
Expand Down Expand Up @@ -105,6 +109,7 @@ class CustomerIO internal constructor(
private var autoTrackDeviceAttributes: Boolean = true
private var modules: MutableMap<String, CustomerIOModule> = mutableMapOf()
private var logLevel = CioLogLevel.ERROR
internal var overrideDiGraph: CustomerIOComponent? = null // set for automated tests
private var trackingApiUrl: String? = null

private lateinit var activityLifecycleCallback: CustomerIOActivityLifecycleCallbacks
Expand Down Expand Up @@ -178,15 +183,16 @@ class CustomerIO internal constructor(
autoTrackDeviceAttributes = autoTrackDeviceAttributes,
backgroundQueueMinNumberOfTasks = 10,
backgroundQueueSecondsDelay = 30.0,
backgroundQueueTaskExpiredSeconds = Seconds.fromDays(3).value,
logLevel = logLevel,
trackingApiUrl = trackingApiUrl
)

val diGraph = CustomerIOComponent(sdkConfig = config, context = appContext)
val diGraph = overrideDiGraph ?: CustomerIOComponent(sdkConfig = config, context = appContext)
val client = CustomerIO(diGraph)
val logger = diGraph.logger

activityLifecycleCallback = CustomerIOActivityLifecycleCallbacks(client, config)
activityLifecycleCallback = CustomerIOActivityLifecycleCallbacks(client, config, diGraph.pushTrackingUtil)
appContext.registerActivityLifecycleCallbacks(activityLifecycleCallback)

instance = client
Expand All @@ -196,6 +202,8 @@ class CustomerIO internal constructor(
it.value.initialize()
}

client.postInitialize()

return client
}
}
Expand All @@ -215,6 +223,16 @@ class CustomerIO internal constructor(
override val sdkVersion: String
get() = Version.version

private val cleanupRepository: CleanupRepository
get() = diGraph.cleanupRepository

private fun postInitialize() {
// run cleanup asynchronously in background to prevent taking up the main/UI thread
CoroutineScope(diGraph.dispatchersProvider.background).launch {
cleanupRepository.cleanup()
}
}

/**
* Identify a customer (aka: Add or update a profile).
* [Learn more](https://customer.io/docs/identifying-people/) about identifying a customer in Customer.io
Expand Down
Expand Up @@ -3,13 +3,18 @@ package io.customer.sdk
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import io.customer.sdk.util.PushTrackingUtil

class CustomerIOActivityLifecycleCallbacks internal constructor(
private val customerIO: CustomerIO,
private val config: CustomerIOConfig
private val config: CustomerIOConfig,
private val pushTrackingUtil: PushTrackingUtil
) : ActivityLifecycleCallbacks {

override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
val intentArguments = activity.intent.extras ?: return

pushTrackingUtil.parseLaunchedActivityForTracking(intentArguments)
}

override fun onActivityStarted(activity: Activity) {
Expand Down
5 changes: 5 additions & 0 deletions sdk/src/main/java/io/customer/sdk/CustomerIOConfig.kt
Expand Up @@ -24,6 +24,11 @@ data class CustomerIOConfig(
*/

val backgroundQueueSecondsDelay: Double,
/**
* The number of seconds old a queue task is when it is "expired" and should be deleted.
* We do not recommend modifying this value because it risks losing data or taking up too much space on the user's device.
*/
val backgroundQueueTaskExpiredSeconds: Double,
val logLevel: CioLogLevel,
var trackingApiUrl: String?,
) {
Expand Down

0 comments on commit ab3cd18

Please sign in to comment.