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 authored Jun 13, 2022
1 parent 79ebdec commit ab3cd18
Show file tree
Hide file tree
Showing 28 changed files with 484 additions and 140 deletions.
Original file line number Diff line number Diff line change
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
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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()
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit ab3cd18

Please sign in to comment.