From 8e5d9315bf7c9ca2fe699701af69a9bf9d426ad1 Mon Sep 17 00:00:00 2001 From: Rehan Date: Thu, 12 Oct 2023 17:49:37 +0500 Subject: [PATCH] added CustomerIOAnalytics --- .../messaginginapp/ModuleMessagingInApp.kt | 6 +- messagingpush/api/messagingpush.api | 7 +- .../device/DeviceTokenProvider.kt | 0 .../messagingpush/di/DiGraphMessagingPush.kt | 4 +- .../processor/PushMessageProcessorImpl.kt | 4 +- .../messagingpush/util/PushTrackingUtil.kt | 4 +- sdk/api/sdk.api | 100 +++---- .../main/java/io/customer/sdk/CustomerIO.kt | 54 ++-- .../java/io/customer/sdk/CustomerIOShared.kt | 10 + .../io/customer/sdk/api/HttpRequestRunner.kt | 131 --------- .../io/customer/sdk/api/HttpRetryPolicy.kt | 37 --- .../io/customer/sdk/api/TrackingHttpClient.kt | 66 ----- .../api/interceptors/HeadersInterceptor.kt | 33 --- .../sdk/api/service/CustomerIOService.kt | 52 ---- .../data/moshi/adapter/BigDecimalAdapter.kt | 13 - .../moshi/adapter/CustomAttributesAdapter.kt | 107 ------- .../sdk/data/moshi/adapter/UnixDateAdapter.kt | 34 --- .../sdk/data/store/ApplicationStore.kt | 38 --- .../io/customer/sdk/data/store/BuildStore.kt | 36 --- .../sdk/data/store/CustomerIOStore.kt | 5 - .../io/customer/sdk/data/store/DeviceStore.kt | 71 ----- .../io/customer/sdk/data/store/FileStorage.kt | 98 ------- .../io/customer/sdk/data/store/FileType.kt | 21 -- .../io/customer/sdk/di/CustomerIOComponent.kt | 190 +------------ .../sdk/error/CustomerIOApiErrorResponse.kt | 43 --- .../io/customer/sdk/error/CustomerIOError.kt | 13 - .../sdk/extensions/RetrofitExtensions.kt | 27 -- .../CustomerIOActivityLifecycleCallbacks.kt | 5 +- .../io/customer/sdk/module/AnalyticsModule.kt | 61 ++++ .../sdk/module/CustomerIOAnalytics.kt | 3 + .../main/java/io/customer/sdk/queue/Queue.kt | 269 ------------------ .../io/customer/sdk/queue/QueueQueryRunner.kt | 63 ---- .../io/customer/sdk/queue/QueueRunRequest.kt | 89 ------ .../java/io/customer/sdk/queue/QueueRunner.kt | 89 ------ .../io/customer/sdk/queue/QueueStorage.kt | 172 ----------- .../DeletePushNotificationQueueTaskData.kt | 9 - .../taskdata/IdentifyProfileQueueTaskData.kt | 9 - .../RegisterPushNotificationQueueTaskData.kt | 11 - .../queue/taskdata/TrackEventQueueTaskData.kt | 10 - .../customer/sdk/queue/type/QueueInventory.kt | 3 - .../sdk/queue/type/QueueModifyResult.kt | 7 - .../sdk/queue/type/QueueRunTaskResult.kt | 4 - .../io/customer/sdk/queue/type/QueueStatus.kt | 6 - .../io/customer/sdk/queue/type/QueueTask.kt | 17 -- .../customer/sdk/queue/type/QueueTaskGroup.kt | 19 -- .../sdk/queue/type/QueueTaskMetadata.kt | 28 -- .../sdk/queue/type/QueueTaskRunResults.kt | 11 - .../customer/sdk/queue/type/QueueTaskType.kt | 11 - .../sdk/repository/CleanupRepository.kt | 19 -- .../sdk/repository/DeviceRepository.kt | 105 ------- .../sdk/repository/ProfileRepository.kt | 119 -------- .../sdk/repository/TrackRepository.kt | 89 ------ .../java/io/customer/sdk/util/JsonAdapter.kt | 109 ------- .../main/java/io/customer/sdk/util/Seconds.kt | 33 --- .../java/io/customer/sdk/util/SimpleTimer.kt | 104 ------- 55 files changed, 169 insertions(+), 2509 deletions(-) rename {sdk/src/main/java/io/customer/sdk => messagingpush/src/main/java/io/customer/messagingpush}/device/DeviceTokenProvider.kt (100%) delete mode 100644 sdk/src/main/java/io/customer/sdk/api/HttpRequestRunner.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/api/HttpRetryPolicy.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/api/TrackingHttpClient.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/api/interceptors/HeadersInterceptor.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/api/service/CustomerIOService.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/data/moshi/adapter/BigDecimalAdapter.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/data/moshi/adapter/CustomAttributesAdapter.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/data/moshi/adapter/UnixDateAdapter.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/data/store/ApplicationStore.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/data/store/BuildStore.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/data/store/CustomerIOStore.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/data/store/DeviceStore.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/data/store/FileStorage.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/data/store/FileType.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/error/CustomerIOApiErrorResponse.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/error/CustomerIOError.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/extensions/RetrofitExtensions.kt rename sdk/src/main/java/io/customer/sdk/{ => lifecycle}/CustomerIOActivityLifecycleCallbacks.kt (97%) create mode 100644 sdk/src/main/java/io/customer/sdk/module/AnalyticsModule.kt create mode 100644 sdk/src/main/java/io/customer/sdk/module/CustomerIOAnalytics.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/Queue.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/QueueQueryRunner.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/QueueRunRequest.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/QueueRunner.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/QueueStorage.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/taskdata/DeletePushNotificationQueueTaskData.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/taskdata/IdentifyProfileQueueTaskData.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/taskdata/RegisterPushNotificationQueueTaskData.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/taskdata/TrackEventQueueTaskData.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/type/QueueInventory.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/type/QueueModifyResult.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/type/QueueRunTaskResult.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/type/QueueStatus.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/type/QueueTask.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskGroup.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskMetadata.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskRunResults.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskType.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/repository/CleanupRepository.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/repository/DeviceRepository.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/repository/ProfileRepository.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/repository/TrackRepository.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/util/JsonAdapter.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/util/Seconds.kt delete mode 100644 sdk/src/main/java/io/customer/sdk/util/SimpleTimer.kt diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/ModuleMessagingInApp.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/ModuleMessagingInApp.kt index 958c270a9..0e4af70ba 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/ModuleMessagingInApp.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/ModuleMessagingInApp.kt @@ -11,8 +11,8 @@ import io.customer.sdk.data.request.MetricEvent import io.customer.sdk.di.CustomerIOComponent import io.customer.sdk.hooks.HookModule import io.customer.sdk.hooks.HooksManager +import io.customer.sdk.module.CustomerIOAnalytics import io.customer.sdk.module.CustomerIOModule -import io.customer.sdk.repository.TrackRepository class ModuleMessagingInApp @VisibleForTesting @InternalCustomerIOApi @@ -47,8 +47,8 @@ internal constructor( private val diGraph: CustomerIOComponent get() = overrideDiGraph ?: CustomerIO.instance().diGraph - private val trackRepository: TrackRepository - get() = diGraph.trackRepository + private val trackRepository: CustomerIOAnalytics + get() = diGraph.analyticsModule private val identifier: String? get() = diGraph.sitePreferenceRepository.getIdentifier() diff --git a/messagingpush/api/messagingpush.api b/messagingpush/api/messagingpush.api index 528131b7f..3704ffe22 100644 --- a/messagingpush/api/messagingpush.api +++ b/messagingpush/api/messagingpush.api @@ -141,7 +141,12 @@ public final class io/customer/messagingpush/util/PushTrackingUtil$Companion { } public final class io/customer/messagingpush/util/PushTrackingUtilImpl : io/customer/messagingpush/util/PushTrackingUtil { - public fun (Lio/customer/sdk/repository/TrackRepository;)V + public fun (Lio/customer/sdk/module/AnalyticsModule;)V public fun parseLaunchedActivityForTracking (Landroid/os/Bundle;)Z } +public abstract interface class io/customer/sdk/device/DeviceTokenProvider { + public abstract fun getCurrentToken (Lkotlin/jvm/functions/Function1;)V + public abstract fun isValidForThisDevice (Landroid/content/Context;)Z +} + diff --git a/sdk/src/main/java/io/customer/sdk/device/DeviceTokenProvider.kt b/messagingpush/src/main/java/io/customer/messagingpush/device/DeviceTokenProvider.kt similarity index 100% rename from sdk/src/main/java/io/customer/sdk/device/DeviceTokenProvider.kt rename to messagingpush/src/main/java/io/customer/messagingpush/device/DeviceTokenProvider.kt diff --git a/messagingpush/src/main/java/io/customer/messagingpush/di/DiGraphMessagingPush.kt b/messagingpush/src/main/java/io/customer/messagingpush/di/DiGraphMessagingPush.kt index a515ec2e8..c2ad3598a 100644 --- a/messagingpush/src/main/java/io/customer/messagingpush/di/DiGraphMessagingPush.kt +++ b/messagingpush/src/main/java/io/customer/messagingpush/di/DiGraphMessagingPush.kt @@ -33,14 +33,14 @@ internal val CustomerIOComponent.deepLinkUtil: DeepLinkUtil @InternalCustomerIOApi val CustomerIOComponent.pushTrackingUtil: PushTrackingUtil - get() = override() ?: PushTrackingUtilImpl(trackRepository) + get() = override() ?: PushTrackingUtilImpl(analyticsModule) internal val CustomerIOComponent.pushMessageProcessor: PushMessageProcessor get() = override() ?: getSingletonInstanceCreate { PushMessageProcessorImpl( logger = logger, moduleConfig = moduleConfig, - trackRepository = trackRepository + trackRepository = analyticsModule ) } diff --git a/messagingpush/src/main/java/io/customer/messagingpush/processor/PushMessageProcessorImpl.kt b/messagingpush/src/main/java/io/customer/messagingpush/processor/PushMessageProcessorImpl.kt index c8dee535f..914f4db00 100644 --- a/messagingpush/src/main/java/io/customer/messagingpush/processor/PushMessageProcessorImpl.kt +++ b/messagingpush/src/main/java/io/customer/messagingpush/processor/PushMessageProcessorImpl.kt @@ -5,13 +5,13 @@ import androidx.annotation.VisibleForTesting import io.customer.messagingpush.MessagingPushModuleConfig import io.customer.messagingpush.util.PushTrackingUtil import io.customer.sdk.data.request.MetricEvent -import io.customer.sdk.repository.TrackRepository +import io.customer.sdk.module.CustomerIOAnalytics import io.customer.sdk.util.Logger internal class PushMessageProcessorImpl( private val logger: Logger, private val moduleConfig: MessagingPushModuleConfig, - private val trackRepository: TrackRepository + private val trackRepository: CustomerIOAnalytics ) : PushMessageProcessor { /** diff --git a/messagingpush/src/main/java/io/customer/messagingpush/util/PushTrackingUtil.kt b/messagingpush/src/main/java/io/customer/messagingpush/util/PushTrackingUtil.kt index 56345b49c..2743cb6a6 100644 --- a/messagingpush/src/main/java/io/customer/messagingpush/util/PushTrackingUtil.kt +++ b/messagingpush/src/main/java/io/customer/messagingpush/util/PushTrackingUtil.kt @@ -2,7 +2,7 @@ package io.customer.messagingpush.util import android.os.Bundle import io.customer.sdk.data.request.MetricEvent -import io.customer.sdk.repository.TrackRepository +import io.customer.sdk.module.CustomerIOAnalytics interface PushTrackingUtil { fun parseLaunchedActivityForTracking(bundle: Bundle): Boolean @@ -14,7 +14,7 @@ interface PushTrackingUtil { } class PushTrackingUtilImpl( - private val trackRepository: TrackRepository + private val trackRepository: CustomerIOAnalytics ) : PushTrackingUtil { override fun parseLaunchedActivityForTracking(bundle: Bundle): Boolean { diff --git a/sdk/api/sdk.api b/sdk/api/sdk.api index 21ae46179..9ae5c634f 100644 --- a/sdk/api/sdk.api +++ b/sdk/api/sdk.api @@ -34,6 +34,7 @@ public final class io/customer/sdk/CustomerIO$Builder { public final fun autoTrackScreenViews (Z)Lio/customer/sdk/CustomerIO$Builder; public final fun build ()Lio/customer/sdk/CustomerIO; public final fun getOverrideDiGraph ()Lio/customer/sdk/di/CustomerIOComponent; + public final fun setAnalyticsTracking (Lio/customer/sdk/module/AnalyticsModule;)Lio/customer/sdk/CustomerIO$Builder; public final fun setBackgroundQueueMinNumberOfTasks (I)Lio/customer/sdk/CustomerIO$Builder; public final fun setBackgroundQueueSecondsDelay (D)Lio/customer/sdk/CustomerIO$Builder; public final fun setClient (Lio/customer/sdk/data/store/Client;)Lio/customer/sdk/CustomerIO$Builder; @@ -48,17 +49,6 @@ public final class io/customer/sdk/CustomerIO$Companion { public final fun instance ()Lio/customer/sdk/CustomerIO; } -public final class io/customer/sdk/CustomerIOActivityLifecycleCallbacks : android/app/Application$ActivityLifecycleCallbacks { - public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V - public fun onActivityDestroyed (Landroid/app/Activity;)V - public fun onActivityPaused (Landroid/app/Activity;)V - public fun onActivityResumed (Landroid/app/Activity;)V - public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V - public fun onActivityStarted (Landroid/app/Activity;)V - public fun onActivityStopped (Landroid/app/Activity;)V - public final fun registerCallback (Lio/customer/sdk/lifecycle/LifecycleCallback;)V -} - public final class io/customer/sdk/CustomerIOConfig { public static final field Companion Lio/customer/sdk/CustomerIOConfig$Companion; public fun (Lio/customer/sdk/data/store/Client;Ljava/lang/String;Ljava/lang/String;Lio/customer/sdk/data/model/Region;JZZIDDLio/customer/sdk/util/CioLogLevel;Ljava/lang/String;Ljava/util/Map;)V @@ -155,7 +145,9 @@ public final class io/customer/sdk/CustomerIOShared { public final fun getDiStaticGraph ()Lio/customer/sdk/di/CustomerIOStaticComponent; public final fun initializeAndGetSharedComponent (Landroid/content/Context;)Lio/customer/sdk/di/CustomerIOSharedComponent; public static final fun instance ()Lio/customer/sdk/CustomerIOShared; + public final fun registerModule (Lio/customer/sdk/module/CustomerIOModule;)V public final fun setDiSharedGraph (Lio/customer/sdk/di/CustomerIOSharedComponent;)V + public final fun unregisterModule (Lio/customer/sdk/module/CustomerIOModule;)V } public final class io/customer/sdk/CustomerIOShared$Companion { @@ -229,34 +221,17 @@ public final class io/customer/sdk/data/request/MetricEvent$Companion { public final fun getEvent (Ljava/lang/String;)Lio/customer/sdk/data/request/MetricEvent; } -public abstract interface class io/customer/sdk/device/DeviceTokenProvider { - public abstract fun getCurrentToken (Lkotlin/jvm/functions/Function1;)V - public abstract fun isValidForThisDevice (Landroid/content/Context;)Z -} - public final class io/customer/sdk/di/CustomerIOComponent : io/customer/sdk/di/DiGraph { - public fun (Lio/customer/sdk/di/CustomerIOStaticComponent;Landroid/content/Context;Lio/customer/sdk/CustomerIOConfig;)V - public final fun getActivityLifecycleCallbacks ()Lio/customer/sdk/CustomerIOActivityLifecycleCallbacks; - public final fun getCioHttpRetryPolicy ()Lio/customer/sdk/api/HttpRetryPolicy; + public fun (Lio/customer/sdk/di/CustomerIOStaticComponent;Landroid/content/Context;Lio/customer/sdk/module/AnalyticsModule;Lio/customer/sdk/CustomerIOConfig;)V + public final fun getActivityLifecycleCallbacks ()Lio/customer/sdk/lifecycle/CustomerIOActivityLifecycleCallbacks; + public final fun getAnalyticsModule ()Lio/customer/sdk/module/AnalyticsModule; public final fun getContext ()Landroid/content/Context; public final fun getDateUtil ()Lio/customer/sdk/util/DateUtil; - public final fun getDeviceRepository ()Lio/customer/sdk/repository/DeviceRepository; public final fun getDispatchersProvider ()Lio/customer/sdk/util/DispatchersProvider; - public final fun getFileStorage ()Lio/customer/sdk/data/store/FileStorage; public final fun getHooksManager ()Lio/customer/sdk/hooks/HooksManager; - public final fun getJsonAdapter ()Lio/customer/sdk/util/JsonAdapter; public final fun getLogger ()Lio/customer/sdk/util/Logger; - public final fun getMoshi ()Lcom/squareup/moshi/Moshi; - public final fun getProfileRepository ()Lio/customer/sdk/repository/ProfileRepository; - public final fun getQueue ()Lio/customer/sdk/queue/Queue; - public final fun getQueueQueryRunner ()Lio/customer/sdk/queue/QueueQueryRunner; - public final fun getQueueRunRequest ()Lio/customer/sdk/queue/QueueRunRequest; - public final fun getQueueRunner ()Lio/customer/sdk/queue/QueueRunner; - public final fun getQueueStorage ()Lio/customer/sdk/queue/QueueStorage; public final fun getSdkConfig ()Lio/customer/sdk/CustomerIOConfig; public final fun getSitePreferenceRepository ()Lio/customer/sdk/repository/preference/SitePreferenceRepository; - public final fun getTimer ()Lio/customer/sdk/util/SimpleTimer; - public final fun getTrackRepository ()Lio/customer/sdk/repository/TrackRepository; } public final class io/customer/sdk/di/CustomerIOSharedComponent : io/customer/sdk/di/DiGraph { @@ -315,6 +290,17 @@ public abstract class io/customer/sdk/hooks/ModuleHookProvider { public fun screenTrackedHook (Lio/customer/sdk/hooks/ModuleHook$ScreenTrackedHook;)V } +public final class io/customer/sdk/lifecycle/CustomerIOActivityLifecycleCallbacks : android/app/Application$ActivityLifecycleCallbacks { + public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityDestroyed (Landroid/app/Activity;)V + public fun onActivityPaused (Landroid/app/Activity;)V + public fun onActivityResumed (Landroid/app/Activity;)V + public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityStarted (Landroid/app/Activity;)V + public fun onActivityStopped (Landroid/app/Activity;)V + public final fun registerCallback (Lio/customer/sdk/lifecycle/LifecycleCallback;)V +} + public abstract interface class io/customer/sdk/lifecycle/LifecycleCallback { public abstract fun getEventsToObserve ()Ljava/util/List; public abstract fun onEventChanged (Landroidx/lifecycle/Lifecycle$Event;Landroid/app/Activity;Landroid/os/Bundle;)V @@ -325,6 +311,34 @@ public final class io/customer/sdk/lifecycle/LifecycleCallback$DefaultImpls { public static synthetic fun onEventChanged$default (Lio/customer/sdk/lifecycle/LifecycleCallback;Landroidx/lifecycle/Lifecycle$Event;Landroid/app/Activity;Landroid/os/Bundle;ILjava/lang/Object;)V } +public abstract interface class io/customer/sdk/module/AnalyticsModule : io/customer/sdk/module/CustomerIOModule { + public abstract fun addCustomDeviceAttributes (Ljava/util/Map;)V + public abstract fun addCustomProfileAttributes (Ljava/util/Map;)V + public abstract fun cleanup (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun clearIdentify ()V + public abstract fun deleteDeviceToken ()V + public abstract fun getDeviceAttributes ()Ljava/util/Map; + public abstract fun getProfileAttributes ()Ljava/util/Map; + public abstract fun getRegisteredDeviceToken ()Ljava/lang/String; + public abstract fun identify (Ljava/lang/String;)V + public abstract fun identify (Ljava/lang/String;Ljava/util/Map;)V + public abstract fun registerDeviceToken (Ljava/lang/String;Ljava/util/Map;)V + public abstract fun screen (Landroid/app/Activity;)V + public abstract fun screen (Landroid/app/Activity;Ljava/util/Map;)V + public abstract fun screen (Ljava/lang/String;)V + public abstract fun screen (Ljava/lang/String;Ljava/util/Map;)V + public abstract fun setDeviceAttributes (Ljava/util/Map;)V + public abstract fun setProfileAttributes (Ljava/util/Map;)V + public abstract fun track (Ljava/lang/String;)V + public abstract fun track (Ljava/lang/String;Ljava/util/Map;)V + public abstract fun trackInAppMetric (Ljava/lang/String;Lio/customer/sdk/data/request/MetricEvent;Ljava/util/Map;)V + public abstract fun trackMetric (Ljava/lang/String;Lio/customer/sdk/data/request/MetricEvent;Ljava/lang/String;)V +} + +public final class io/customer/sdk/module/AnalyticsModule$DefaultImpls { + public static synthetic fun trackInAppMetric$default (Lio/customer/sdk/module/AnalyticsModule;Ljava/lang/String;Lio/customer/sdk/data/request/MetricEvent;Ljava/util/Map;ILjava/lang/Object;)V +} + public abstract interface class io/customer/sdk/module/CustomerIOModule { public abstract fun getModuleConfig ()Lio/customer/sdk/module/CustomerIOModuleConfig; public abstract fun getModuleName ()Ljava/lang/String; @@ -338,30 +352,6 @@ public abstract interface class io/customer/sdk/module/CustomerIOModuleConfig$Bu public abstract fun build ()Lio/customer/sdk/module/CustomerIOModuleConfig; } -public abstract interface class io/customer/sdk/repository/DeviceRepository { - public abstract fun addCustomDeviceAttributes (Ljava/util/Map;)V - public abstract fun deleteDeviceToken ()V - public abstract fun getDeviceToken ()Ljava/lang/String; - public abstract fun registerDeviceToken (Ljava/lang/String;Ljava/util/Map;)V -} - -public abstract interface class io/customer/sdk/repository/ProfileRepository { - public abstract fun addCustomProfileAttributes (Ljava/util/Map;)V - public abstract fun clearIdentify ()V - public abstract fun identify (Ljava/lang/String;Ljava/util/Map;)V -} - -public abstract interface class io/customer/sdk/repository/TrackRepository { - public abstract fun screen (Ljava/lang/String;Ljava/util/Map;)V - public abstract fun track (Ljava/lang/String;Ljava/util/Map;)V - public abstract fun trackInAppMetric (Ljava/lang/String;Lio/customer/sdk/data/request/MetricEvent;Ljava/util/Map;)V - public abstract fun trackMetric (Ljava/lang/String;Lio/customer/sdk/data/request/MetricEvent;Ljava/lang/String;)V -} - -public final class io/customer/sdk/repository/TrackRepository$DefaultImpls { - public static synthetic fun trackInAppMetric$default (Lio/customer/sdk/repository/TrackRepository;Ljava/lang/String;Lio/customer/sdk/data/request/MetricEvent;Ljava/util/Map;ILjava/lang/Object;)V -} - public abstract interface class io/customer/sdk/repository/preference/SitePreferenceRepository { public abstract fun clearAll ()V public abstract fun getDeviceToken ()Ljava/lang/String; diff --git a/sdk/src/main/java/io/customer/sdk/CustomerIO.kt b/sdk/src/main/java/io/customer/sdk/CustomerIO.kt index e6ed74e2f..23429496f 100644 --- a/sdk/src/main/java/io/customer/sdk/CustomerIO.kt +++ b/sdk/src/main/java/io/customer/sdk/CustomerIO.kt @@ -12,16 +12,13 @@ import io.customer.sdk.data.request.MetricEvent import io.customer.sdk.data.store.Client import io.customer.sdk.di.CustomerIOComponent import io.customer.sdk.extensions.* +import io.customer.sdk.module.AnalyticsModule +import io.customer.sdk.module.CustomerIOAnalytics import io.customer.sdk.module.CustomerIOModule import io.customer.sdk.module.CustomerIOModuleConfig -import io.customer.sdk.repository.CleanupRepository -import io.customer.sdk.repository.DeviceRepository -import io.customer.sdk.repository.ProfileRepository -import io.customer.sdk.repository.TrackRepository import io.customer.sdk.repository.preference.CustomerIOStoredValues import io.customer.sdk.repository.preference.doesExist import io.customer.sdk.util.CioLogLevel -import io.customer.sdk.util.Seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -110,7 +107,7 @@ class CustomerIO internal constructor( // Cancelling queue timer as the new queue timer will take care of running queue tasks. // If we do not cancel old timer, it results in multiple timers being run and accessing // the same tasks. - it.diGraph.queue.cancelTimer() +// it.diGraph.queue.cancelTimer() instance = null } } @@ -191,6 +188,7 @@ class CustomerIO internal constructor( CustomerIOConfig.Companion.AnalyticsConstants.SHOULD_AUTO_RECORD_SCREEN_VIEWS private var autoTrackDeviceAttributes: Boolean = CustomerIOConfig.Companion.AnalyticsConstants.AUTO_TRACK_DEVICE_ATTRIBUTES + private var analyticsModule: CustomerIOAnalytics? = null private val modules: MutableMap> = mutableMapOf() private var logLevel: CioLogLevel = @@ -318,6 +316,12 @@ class CustomerIO internal constructor( return this } + fun setAnalyticsTracking(module: AnalyticsModule): Builder { + analyticsModule = module + modules[module.moduleName] = module + return this + } + fun addCustomerIOModule(module: CustomerIOModule): Builder { modules[module.moduleName] = module return this @@ -342,7 +346,7 @@ class CustomerIO internal constructor( autoTrackDeviceAttributes = autoTrackDeviceAttributes, backgroundQueueMinNumberOfTasks = backgroundQueueMinNumberOfTasks, backgroundQueueSecondsDelay = backgroundQueueSecondsDelay, - backgroundQueueTaskExpiredSeconds = Seconds.fromDays(3).value, + backgroundQueueTaskExpiredSeconds = 3 * 24 * 60 * 60.0, logLevel = logLevel, trackingApiUrl = trackingApiUrl, modules = modules.entries.associate { entry -> entry.key to entry.value } @@ -352,6 +356,7 @@ class CustomerIO internal constructor( val diGraph = overrideDiGraph ?: CustomerIOComponent( staticComponent = sharedInstance.diStaticGraph, sdkConfig = config, + analyticsModule = analyticsModule!!, context = appContext ) val client = CustomerIO(diGraph) @@ -375,28 +380,19 @@ class CustomerIO internal constructor( } } - private val trackRepository: TrackRepository - get() = diGraph.trackRepository - - private val deviceRepository: DeviceRepository - get() = diGraph.deviceRepository - - private val profileRepository: ProfileRepository - get() = diGraph.profileRepository - override val siteId: String get() = diGraph.sdkConfig.siteId override val sdkVersion: String get() = Version.version - private val cleanupRepository: CleanupRepository - get() = diGraph.cleanupRepository + private val analyticsModule: CustomerIOAnalytics + get() = diGraph.analyticsModule private fun postInitialize() { // run cleanup asynchronously in background to prevent taking up the main/UI thread CoroutineScope(diGraph.dispatchersProvider.background).launch { - cleanupRepository.cleanup() + analyticsModule.cleanup() } } @@ -426,7 +422,7 @@ class CustomerIO internal constructor( override fun identify( identifier: String, attributes: CustomAttributes - ) = profileRepository.identify(identifier, attributes) + ) = analyticsModule.identify(identifier, attributes) /** * Track an event @@ -445,7 +441,7 @@ class CustomerIO internal constructor( override fun track( name: String, attributes: CustomAttributes - ) = trackRepository.track(name, attributes) + ) = analyticsModule.track(name, attributes) /** * Track screen @@ -463,7 +459,7 @@ class CustomerIO internal constructor( override fun screen( name: String, attributes: CustomAttributes - ) = trackRepository.screen(name, attributes) + ) = analyticsModule.screen(name, attributes) /** * Track activity screen, `label` added for this activity in `manifest` will be utilized for tracking @@ -491,7 +487,7 @@ class CustomerIO internal constructor( * If no profile has been identified yet, this function will ignore your request. */ override fun clearIdentify() { - profileRepository.clearIdentify() + analyticsModule.clearIdentify() } /** @@ -499,12 +495,12 @@ class CustomerIO internal constructor( * is no active customer, this will fail to register the device */ override fun registerDeviceToken(deviceToken: String) = - deviceRepository.registerDeviceToken(deviceToken, deviceAttributes) + analyticsModule.registerDeviceToken(deviceToken, deviceAttributes) /** * Delete the currently registered device token */ - override fun deleteDeviceToken() = deviceRepository.deleteDeviceToken() + override fun deleteDeviceToken() = analyticsModule.deleteDeviceToken() /** * Track a push metric @@ -513,7 +509,7 @@ class CustomerIO internal constructor( deliveryID: String, event: MetricEvent, deviceToken: String - ) = trackRepository.trackMetric( + ) = analyticsModule.trackMetric( deliveryID = deliveryID, event = event, deviceToken = deviceToken @@ -526,7 +522,7 @@ class CustomerIO internal constructor( */ override var profileAttributes: CustomAttributes = emptyMap() set(value) { - profileRepository.addCustomProfileAttributes(value) + analyticsModule.addCustomProfileAttributes(value) } /** @@ -537,11 +533,11 @@ class CustomerIO internal constructor( set(value) { field = value - deviceRepository.addCustomDeviceAttributes(value) + analyticsModule.addCustomDeviceAttributes(value) } override val registeredDeviceToken: String? - get() = deviceRepository.getDeviceToken() + get() = analyticsModule.registeredDeviceToken private fun recordScreenViews(activity: Activity, attributes: CustomAttributes) { val packageManager = activity.packageManager diff --git a/sdk/src/main/java/io/customer/sdk/CustomerIOShared.kt b/sdk/src/main/java/io/customer/sdk/CustomerIOShared.kt index 2bcc6462b..852eb9394 100644 --- a/sdk/src/main/java/io/customer/sdk/CustomerIOShared.kt +++ b/sdk/src/main/java/io/customer/sdk/CustomerIOShared.kt @@ -6,6 +6,7 @@ import io.customer.base.internal.InternalCustomerIOApi import io.customer.sdk.CustomerIOShared.Companion.instance import io.customer.sdk.di.CustomerIOSharedComponent import io.customer.sdk.di.CustomerIOStaticComponent +import io.customer.sdk.module.CustomerIOGenericModule import io.customer.sdk.repository.preference.CustomerIOStoredValues import io.customer.sdk.util.LogcatLogger @@ -29,6 +30,15 @@ class CustomerIOShared private constructor( ) { var diSharedGraph: CustomerIOSharedComponent? = null + private val modules: MutableSet = mutableSetOf() + + fun registerModule(module: CustomerIOGenericModule) { + modules.add(module) + } + + fun unregisterModule(module: CustomerIOGenericModule) { + modules.remove(module) + } @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) fun initializeAndGetSharedComponent(context: Context): CustomerIOSharedComponent { diff --git a/sdk/src/main/java/io/customer/sdk/api/HttpRequestRunner.kt b/sdk/src/main/java/io/customer/sdk/api/HttpRequestRunner.kt deleted file mode 100644 index fa2d1d938..000000000 --- a/sdk/src/main/java/io/customer/sdk/api/HttpRequestRunner.kt +++ /dev/null @@ -1,131 +0,0 @@ -package io.customer.sdk.api - -import io.customer.base.extenstions.add -import io.customer.base.extenstions.hasPassed -import io.customer.sdk.error.CustomerIOApiErrorResponse -import io.customer.sdk.error.CustomerIOApiErrorsResponse -import io.customer.sdk.error.CustomerIOError -import io.customer.sdk.repository.preference.SitePreferenceRepository -import io.customer.sdk.util.JsonAdapter -import io.customer.sdk.util.Logger -import java.util.* -import java.util.concurrent.TimeUnit -import kotlinx.coroutines.delay -import retrofit2.Response - -internal interface HttpRequestRunner { - suspend fun performAndProcessRequest(makeRequest: suspend () -> Response): Result -} - -/** - * Where HTTP response processing occurs. - */ -internal class HttpRequestRunnerImpl( - private val prefsRepository: SitePreferenceRepository, - private val logger: Logger, - private val retryPolicy: HttpRetryPolicy, - private val jsonAdapter: JsonAdapter -) : HttpRequestRunner { - - override suspend fun performAndProcessRequest(makeRequest: suspend () -> Response): Result { - prefsRepository.httpRequestsPauseEnds?.let { httpPauseEnds -> - if (!httpPauseEnds.hasPassed()) { - logger.debug("HTTP request ignored because requests are still paused.") - return Result.failure(CustomerIOError.HttpRequestsPaused()) - } - } - - var response: Response? = null - try { - response = makeRequest() - } catch (e: Throwable) { - // HTTP request was not able to be made. Probably an Internet connection issue - logger.debug("HTTP request failed. Error: ${e.message}") - } - - if (response == null) { - return Result.failure(CustomerIOError.NoHttpRequestMade()) - } - - val responseBody = response.body() - if (response.isSuccessful && responseBody != null) { - prepareForNextRequest() // after successful HTTP request, reset to prepare for the next HTTP request - - return Result.success(responseBody) - } - - return processUnsuccessfulResponse(response, makeRequest) - } - - suspend fun processUnsuccessfulResponse( - response: Response, - makeRequest: suspend () -> Response - ): Result { - // Note: calling .string(), you are not able to get the error body again. retrofit clears the error body after calling .string() - // That's why we get the value here for the whole function body to use. - val httpResponseErrorBodyString = response.errorBody()?.string() - - // parse the server response for use later. - // First, try to get a parsed version of the HTTP response body. Then, use the raw JSON response. If that also fails, return a generic default message to the customer. - val parsedCustomerIOServerResponse = parseCustomerIOErrorBody(httpResponseErrorBodyString)?.message ?: httpResponseErrorBodyString ?: "(server did not give a response)" - - when (val statusCode = response.code()) { - in 500 until 600 -> { - val sleepTime = retryPolicy.nextSleepTime - return if (sleepTime != null) { - logger.debug("Encountered $statusCode HTTP response. Sleeping $sleepTime seconds and then retrying.") - - delay(sleepTime.toMilliseconds.value) - - this.performAndProcessRequest(makeRequest) - } else { - pauseHttpRequests() - prepareForNextRequest() // after retry policy is finished, reset to prepare for the next HTTP request - - Result.failure(CustomerIOError.ServerDown()) - } - } - 401 -> { - pauseHttpRequests() - - return Result.failure(CustomerIOError.Unauthorized()) - } - 400 -> { - return Result.failure(CustomerIOError.BadRequest400(parsedCustomerIOServerResponse)) - } - else -> { - val customerIOError = CustomerIOError.UnsuccessfulStatusCode(statusCode, parsedCustomerIOServerResponse) - - logger.error("4xx HTTP status code response. Probably a bug? $parsedCustomerIOServerResponse") - - return Result.failure(customerIOError) - } - } - } - - internal fun parseCustomerIOErrorBody(errorBody: String?): Throwable? { - if (errorBody == null) return null - - return jsonAdapter.fromJsonOrNull(errorBody)?.throwable - ?: jsonAdapter.fromJsonOrNull(errorBody)?.throwable - } - - private fun prepareForNextRequest() { - retryPolicy.reset() // in case retry policy was used, reset it so the next request can use it. - // do not edit the HTTP pausing. Let that get modified somewhere else and get reset by timing out. - } - - // In certain scenarios, it makes sense for us to pause making any HTTP requests to the - // Customer.io API. Because HTTP requests are performed by the background queue, there is - // a chance that the background queue could make a lot of HTTP requests in - // a short amount of time from lots of devices. This makes a performance impact on our API. - // By pausing HTTP requests, we mitigate the chance of customer devices causing harm to our API. - internal fun pauseHttpRequests() { - val minutesToPause = 5 - logger.info("All HTTP requests to Customer.io API have been paused for $minutesToPause minutes") - - val dateToEndPause = Date().add(minutesToPause, TimeUnit.MINUTES) - - prefsRepository.httpRequestsPauseEnds = dateToEndPause - } -} diff --git a/sdk/src/main/java/io/customer/sdk/api/HttpRetryPolicy.kt b/sdk/src/main/java/io/customer/sdk/api/HttpRetryPolicy.kt deleted file mode 100644 index e9e3edc55..000000000 --- a/sdk/src/main/java/io/customer/sdk/api/HttpRetryPolicy.kt +++ /dev/null @@ -1,37 +0,0 @@ -package io.customer.sdk.api - -import io.customer.sdk.util.Seconds -import io.customer.sdk.util.toSeconds - -interface HttpRetryPolicy { - val nextSleepTime: Seconds? - fun reset() -} - -internal class CustomerIOApiRetryPolicy : HttpRetryPolicy { - - companion object { - internal val retryPolicy: List = listOf( - 0.1, - 0.2, - 0.4, - 0.8, - 1.6, - 3.2 - ).map { it.toSeconds() } - } - - private var retriesLeft: MutableList = mutableListOf() - - init { - reset() // to populate fields and be ready for first request. - } - - override val nextSleepTime: Seconds? - get() = retriesLeft.removeFirstOrNull() - - // where all fields are populated in class. Single source of truth for initial values. - override fun reset() { - retriesLeft = retryPolicy.toMutableList() - } -} diff --git a/sdk/src/main/java/io/customer/sdk/api/TrackingHttpClient.kt b/sdk/src/main/java/io/customer/sdk/api/TrackingHttpClient.kt deleted file mode 100644 index 07d449c61..000000000 --- a/sdk/src/main/java/io/customer/sdk/api/TrackingHttpClient.kt +++ /dev/null @@ -1,66 +0,0 @@ -package io.customer.sdk.api - -import io.customer.sdk.api.service.CustomerIOService -import io.customer.sdk.data.model.CustomAttributes -import io.customer.sdk.data.request.* -import io.customer.sdk.data.request.DeliveryEvent -import io.customer.sdk.data.request.DeviceRequest -import io.customer.sdk.data.request.Event -import io.customer.sdk.data.request.Metric - -/** - * Wrapper around Retrofit to encapsulate Retrofit if we decide to change how we perform HTTP requests in the future. - */ -internal interface TrackingHttpClient { - suspend fun identifyProfile(identifier: String, attributes: CustomAttributes): Result - suspend fun track(identifier: String, body: Event): Result - suspend fun registerDevice(identifier: String, device: Device): Result - suspend fun deleteDevice(identifier: String, deviceToken: String): Result - suspend fun trackPushMetrics(metric: Metric): Result - suspend fun trackDeliveryEvents(event: DeliveryEvent): Result -} - -internal class RetrofitTrackingHttpClient( - private val retrofitService: CustomerIOService, - private val httpRequestRunner: HttpRequestRunner -) : TrackingHttpClient { - - override suspend fun identifyProfile( - identifier: String, - attributes: CustomAttributes - ): Result { - return httpRequestRunner.performAndProcessRequest { - retrofitService.identifyCustomer(identifier, attributes) - } - } - - override suspend fun track(identifier: String, body: Event): Result { - return httpRequestRunner.performAndProcessRequest { - retrofitService.track(identifier, body) - } - } - - override suspend fun registerDevice(identifier: String, device: Device): Result { - return httpRequestRunner.performAndProcessRequest { - retrofitService.addDevice(identifier, DeviceRequest(device)) - } - } - - override suspend fun deleteDevice(identifier: String, deviceToken: String): Result { - return httpRequestRunner.performAndProcessRequest { - retrofitService.removeDevice(identifier, deviceToken) - } - } - - override suspend fun trackPushMetrics(metric: Metric): Result { - return httpRequestRunner.performAndProcessRequest { - retrofitService.trackMetric(metric) - } - } - - override suspend fun trackDeliveryEvents(event: DeliveryEvent): Result { - return httpRequestRunner.performAndProcessRequest { - retrofitService.trackDeliveryEvents(event) - } - } -} diff --git a/sdk/src/main/java/io/customer/sdk/api/interceptors/HeadersInterceptor.kt b/sdk/src/main/java/io/customer/sdk/api/interceptors/HeadersInterceptor.kt deleted file mode 100644 index 0f9c5f037..000000000 --- a/sdk/src/main/java/io/customer/sdk/api/interceptors/HeadersInterceptor.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.customer.sdk.api.interceptors - -import android.util.Base64 -import io.customer.sdk.CustomerIOConfig -import io.customer.sdk.data.store.CustomerIOStore -import java.nio.charset.StandardCharsets -import okhttp3.Interceptor -import okhttp3.Response - -internal class HeadersInterceptor( - private val store: CustomerIOStore, - private val config: CustomerIOConfig -) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val token by lazy { "Basic ${getBasicAuthHeaderString()}" } - val userAgent by lazy { store.deviceStore.buildUserAgent() } - - val request = chain.request() - .newBuilder() - .addHeader("Content-Type", "application/json; charset=utf-8") - .addHeader("Authorization", token) - .addHeader("User-Agent", userAgent) - .build() - return chain.proceed(request) - } - - private fun getBasicAuthHeaderString(): String { - val apiKey = config.apiKey - val siteId = config.siteId - val rawHeader = "$siteId:$apiKey" - return Base64.encodeToString(rawHeader.toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/api/service/CustomerIOService.kt b/sdk/src/main/java/io/customer/sdk/api/service/CustomerIOService.kt deleted file mode 100644 index 0114957bf..000000000 --- a/sdk/src/main/java/io/customer/sdk/api/service/CustomerIOService.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.customer.sdk.api.service - -import io.customer.sdk.data.model.CustomAttributes -import io.customer.sdk.data.request.DeliveryEvent -import io.customer.sdk.data.request.DeviceRequest -import io.customer.sdk.data.request.Event -import io.customer.sdk.data.request.Metric -import retrofit2.Response -import retrofit2.http.* - -internal interface CustomerIOService { - - @JvmSuppressWildcards - @PUT("api/v1/customers/{identifier}") - suspend fun identifyCustomer( - @Path("identifier") identifier: String, - @Body body: CustomAttributes - ): Response - - @JvmSuppressWildcards - @POST("api/v1/customers/{identifier}/events") - suspend fun track( - @Path("identifier") identifier: String, - @Body body: Event - ): Response - - @JvmSuppressWildcards - @POST("push/events") - suspend fun trackMetric( - @Body body: Metric - ): Response - - @JvmSuppressWildcards - @POST("api/v1/cio_deliveries/events") - suspend fun trackDeliveryEvents( - @Body body: DeliveryEvent - ): Response - - @JvmSuppressWildcards - @PUT("api/v1/customers/{identifier}/devices") - suspend fun addDevice( - @Path("identifier") identifier: String, - @Body body: DeviceRequest - ): Response - - @JvmSuppressWildcards - @DELETE("api/v1/customers/{identifier}/devices/{token}") - suspend fun removeDevice( - @Path("identifier") identifier: String, - @Path("token") token: String - ): Response -} diff --git a/sdk/src/main/java/io/customer/sdk/data/moshi/adapter/BigDecimalAdapter.kt b/sdk/src/main/java/io/customer/sdk/data/moshi/adapter/BigDecimalAdapter.kt deleted file mode 100644 index 856efd66f..000000000 --- a/sdk/src/main/java/io/customer/sdk/data/moshi/adapter/BigDecimalAdapter.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.customer.sdk.data.moshi.adapter - -import com.squareup.moshi.FromJson -import com.squareup.moshi.ToJson -import java.math.BigDecimal - -internal class BigDecimalAdapter { - @FromJson - fun fromJson(string: String) = BigDecimal(string) - - @ToJson - fun toJson(value: BigDecimal) = value.toString() -} diff --git a/sdk/src/main/java/io/customer/sdk/data/moshi/adapter/CustomAttributesAdapter.kt b/sdk/src/main/java/io/customer/sdk/data/moshi/adapter/CustomAttributesAdapter.kt deleted file mode 100644 index 9c7e9d220..000000000 --- a/sdk/src/main/java/io/customer/sdk/data/moshi/adapter/CustomAttributesAdapter.kt +++ /dev/null @@ -1,107 +0,0 @@ -package io.customer.sdk.data.moshi.adapter - -import com.squareup.moshi.* -import io.customer.base.extenstions.getUnixTimestamp -import io.customer.sdk.data.model.CustomAttributes -import java.lang.reflect.Type -import java.math.BigDecimal -import java.util.* - -internal class CustomAttributesFactory : JsonAdapter.Factory { - override fun create( - type: Type, - annotations: MutableSet, - moshi: Moshi - ): JsonAdapter<*>? { - if (Types.getRawType(type) != Map::class.java) { - return null - } - return CustomAttributesAdapter(moshi).nullSafe() - } -} - -/** - * Moshi [JsonAdapter] for supporting [CustomAttributes] for the SDK. - * - * Duties of the JsonAdapter to support: - * * For numbers inside of JSON, parse them to [BigDecimal]. Moshi by default uses [Double] which may not be big enough for a customer's use. - * * Convert [Date] to [Long] (unix timestamp) as that's what the Customer.IO API expects. - * * Convert [Enum] to [String] data type. - * * Because the Map allows Any, filter out values that Moshi does not support. This lessons the chance of sending attributes to the API that the API cannot understand. This filtering is done *before* JSON parsing. Therefore, use the included [CustomAttributes.verify] extensions before using the [io.customer.sdk.util.JsonAdapter]. - */ -internal class CustomAttributesAdapter(moshi: Moshi) : - JsonAdapter() { - - private val elementAdapter: JsonAdapter = moshi.adapter(Any::class.java) - private val elementBigDecimalAdapter: JsonAdapter = - moshi.adapter(BigDecimal::class.java) - - override fun fromJson(reader: JsonReader): CustomAttributes { - val result = mutableMapOf() - reader.beginObject() - while (reader.peek() != JsonReader.Token.END_OBJECT) { - try { - val name = reader.nextName() - val peeked = reader.peekJson() - if (peeked.peek() == JsonReader.Token.NUMBER) { - result[name] = elementBigDecimalAdapter.fromJson(peeked)!! - } else { - result[name] = elementAdapter.fromJson(peeked)!! - } - } catch (ignored: JsonDataException) { - } - reader.skipValue() - } - reader.endObject() - return result - } - - override fun toJson(writer: JsonWriter, value: CustomAttributes?) { - if (value == null) { - throw NullPointerException("value was null! Wrap in .nullSafe() to write nullable values.") - } - val value = verifyCustomAttributes(value) - - writer.beginObject() - - value.forEach { - try { - /** - * If moshi can't serialize an object, it will throw an exception and crash the SDK. - * To avoid the SDK crashing, try to see if Moshi is able to serialize the object. - * If it is able to, then let's use the JSON writer to write the object to the JSON string. Else, ignore the attribute. - */ - elementAdapter.toJson(it.value) // our test. will throw exception if can't serialize. - - // Write to json string since we are confident that it's able to be serialized now. - writer.name(it.key) - elementAdapter.toJson(writer, it.value) - } catch (e: Throwable) { - // Ignore element if it can't be serialized. - // Have automated tests written against SDK objects to assert the SDK models are able to - // serialize. - } - } - - writer.endObject() - } - - /** - * Convert data types of the Map to data types the Customer.io API can understand. We want to run this function before Moshi converts our Map into a JSON string that then gets sent to theAPI. - */ - private fun verifyCustomAttributes(value: CustomAttributes): CustomAttributes { - fun getValidValue(any: Any): Any { - return when (any) { - is Date -> any.getUnixTimestamp() // The API expects dates to be in Unix time format. - is Enum<*> -> any.name // Convert Enum data types to String. - else -> any - } - } - - val validMap = mutableMapOf() - value.entries.forEach { - validMap[it.key] = getValidValue(it.value) - } - return validMap.toMap() - } -} diff --git a/sdk/src/main/java/io/customer/sdk/data/moshi/adapter/UnixDateAdapter.kt b/sdk/src/main/java/io/customer/sdk/data/moshi/adapter/UnixDateAdapter.kt deleted file mode 100644 index 1a7fc2615..000000000 --- a/sdk/src/main/java/io/customer/sdk/data/moshi/adapter/UnixDateAdapter.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.customer.sdk.data.moshi.adapter - -import com.squareup.moshi.* -import io.customer.base.extenstions.getUnixTimestamp -import io.customer.base.extenstions.unixTimeToDate -import java.io.IOException -import java.util.* -import org.json.JSONObject.NULL - -// Help from: https://github.com/square/moshi/blob/fd128875c308a90288e705162e03a835220a74d9/moshi-adapters/src/main/java/com/squareup/moshi/adapters/Rfc3339DateJsonAdapter.kt -// The Customer.io API uses unix date format. Use this date format for all Dates with JSON. -internal class UnixDateAdapter : JsonAdapter() { - @Synchronized - @Throws(IOException::class) - @FromJson - override fun fromJson(reader: JsonReader): Date? { - if (reader.peek() == NULL) { - return reader.nextNull() - } - val string = reader.nextString() - return string.toLongOrNull()?.unixTimeToDate() - } - - @Synchronized - @Throws(IOException::class) - @ToJson - override fun toJson(writer: JsonWriter, value: Date?) { - if (value == null) { - writer.nullValue() - } else { - writer.value(value.getUnixTimestamp()) - } - } -} diff --git a/sdk/src/main/java/io/customer/sdk/data/store/ApplicationStore.kt b/sdk/src/main/java/io/customer/sdk/data/store/ApplicationStore.kt deleted file mode 100644 index 868a57a58..000000000 --- a/sdk/src/main/java/io/customer/sdk/data/store/ApplicationStore.kt +++ /dev/null @@ -1,38 +0,0 @@ -package io.customer.sdk.data.store - -import android.content.Context -import androidx.core.app.NotificationManagerCompat - -interface ApplicationStore { - // Customer App information - - val customerAppName: String? - val customerAppVersion: String? - val customerPackageName: String - - val isPushEnabled: Boolean -} - -internal class ApplicationStoreImp(val context: Context) : ApplicationStore { - - override val customerAppName: String? - get() = tryGetValueOrNull { - context.applicationInfo.loadLabel(context.packageManager).toString() - } - override val customerAppVersion: String? - get() = tryGetValueOrNull { - context.packageManager.getPackageInfo(context.packageName, 0).versionName - } - override val customerPackageName: String - get() = context.packageName - override val isPushEnabled: Boolean - get() = NotificationManagerCompat.from(context).areNotificationsEnabled() - - private fun tryGetValueOrNull(tryGetValue: () -> String): String? { - return try { - tryGetValue() - } catch (e: Exception) { - null - } - } -} diff --git a/sdk/src/main/java/io/customer/sdk/data/store/BuildStore.kt b/sdk/src/main/java/io/customer/sdk/data/store/BuildStore.kt deleted file mode 100644 index 365ccecdc..000000000 --- a/sdk/src/main/java/io/customer/sdk/data/store/BuildStore.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.customer.sdk.data.store - -import android.os.Build -import java.util.* - -interface BuildStore { - - // Brand : Google - val deviceBrand: String - - // Device model: Pixel - val deviceModel: String - - // Hardware manufacturer: Samsung - val deviceManufacturer: String - - // Android SDK Version: 21 - val deviceOSVersion: Int - - // Device locale: en-US - val deviceLocale: String -} - -internal class BuildStoreImp : BuildStore { - - override val deviceBrand: String - get() = Build.BRAND - override val deviceModel: String - get() = Build.MODEL - override val deviceManufacturer: String - get() = Build.MANUFACTURER - override val deviceOSVersion: Int - get() = Build.VERSION.SDK_INT - override val deviceLocale: String - get() = Locale.getDefault().toLanguageTag() -} diff --git a/sdk/src/main/java/io/customer/sdk/data/store/CustomerIOStore.kt b/sdk/src/main/java/io/customer/sdk/data/store/CustomerIOStore.kt deleted file mode 100644 index c6be01f4e..000000000 --- a/sdk/src/main/java/io/customer/sdk/data/store/CustomerIOStore.kt +++ /dev/null @@ -1,5 +0,0 @@ -package io.customer.sdk.data.store - -interface CustomerIOStore { - val deviceStore: DeviceStore -} diff --git a/sdk/src/main/java/io/customer/sdk/data/store/DeviceStore.kt b/sdk/src/main/java/io/customer/sdk/data/store/DeviceStore.kt deleted file mode 100644 index 7f41e9c40..000000000 --- a/sdk/src/main/java/io/customer/sdk/data/store/DeviceStore.kt +++ /dev/null @@ -1,71 +0,0 @@ -package io.customer.sdk.data.store - -import io.customer.sdk.CustomerIOConfig - -interface DeviceStore : BuildStore, ApplicationStore { - - // SDK version - val customerIOVersion: String - - /** - * buildUserAgent - To get `user-agent` header value. This value depends on SDK version - * and device detail such as OS version, device model, customer's app name etc - * - * If the device and OS information is available, it will return in following format : - * `Customer.io Android Client/1.0.0-alpha.6 (Google Pixel 6; 30) User App/1.0` - * - * Otherwise will return - * `Customer.io Android Client/1.0.0-alpha.6` - */ - fun buildUserAgent(): String - fun buildDeviceAttributes(): Map -} - -class DeviceStoreImp( - private val sdkConfig: CustomerIOConfig, - private val buildStore: BuildStore, - private val applicationStore: ApplicationStore, - private val version: String -) : DeviceStore { - - override val deviceBrand: String - get() = buildStore.deviceBrand - override val deviceModel: String - get() = buildStore.deviceModel - override val deviceManufacturer: String - get() = buildStore.deviceManufacturer - override val deviceOSVersion: Int - get() = buildStore.deviceOSVersion - override val deviceLocale: String - get() = buildStore.deviceLocale - override val customerAppName: String? - get() = applicationStore.customerAppName - override val customerAppVersion: String? - get() = applicationStore.customerAppVersion - override val isPushEnabled: Boolean - get() = applicationStore.isPushEnabled - override val customerPackageName: String - get() = applicationStore.customerPackageName - override val customerIOVersion: String - get() = version - - override fun buildUserAgent(): String { - return buildString { - append("Customer.io ${sdkConfig.client}") - append(" ($deviceManufacturer $deviceModel; $deviceOSVersion)") - append(" $customerPackageName/${customerAppVersion ?: "0.0.0"}") - } - } - - override fun buildDeviceAttributes(): Map { - return mapOf( - "device_os" to deviceOSVersion, - "device_model" to deviceModel, - "device_manufacturer" to deviceManufacturer, - "app_version" to (customerAppVersion ?: ""), - "cio_sdk_version" to customerIOVersion, - "device_locale" to deviceLocale, - "push_enabled" to isPushEnabled - ) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/data/store/FileStorage.kt b/sdk/src/main/java/io/customer/sdk/data/store/FileStorage.kt deleted file mode 100644 index f5fe46ca5..000000000 --- a/sdk/src/main/java/io/customer/sdk/data/store/FileStorage.kt +++ /dev/null @@ -1,98 +0,0 @@ -package io.customer.sdk.data.store - -import android.content.Context -import io.customer.sdk.CustomerIOConfig -import io.customer.sdk.util.Logger -import java.io.File - -/* - Save data to a file on the device file system. - - Responsibilities: - * Be able to mock so we can use in unit tests without using the real file system - * Be the 1 source of truth for where certain types of files are stored. - Making code less prone to errors and typos for file paths. - - Way that files are stored in this class: - ``` - Internal storage (since we don't need to ask for permissions and its private to the app) - io.customer/ // Create a directory to separate SDK files from other app files. - / // sandbox all files in the SDK to it's site-id. - queue/ - inventory.json - tasks/ - .json - ``` - - Notice that we are using the as a way to isolate files from each other. - The file tree remains the same for all site ids. - */ -class FileStorage internal constructor( - private val config: CustomerIOConfig, - private val context: Context, - private val logger: Logger -) { - - val sdkRootDirectoryPath = File(context.filesDir, "io.customer") // All files in the SDK exist in here. - val siteIdRootDirectoryPath: File - get() = File(sdkRootDirectoryPath, config.siteId) // Root directory for given - - fun save(type: FileType, contents: String): Boolean { - val parentFilePath = type.getFilePath(siteIdRootDirectoryPath) - val filePath = File(parentFilePath, type.getFileName()) - - try { - parentFilePath.mkdirs() - filePath.createNewFile() - filePath.writeText(contents) - } catch (e: Throwable) { - logger.error("error while saving file $type. path ${filePath.absolutePath}. message: ${e.message}") - return false - } - - return true - } - - fun get(type: FileType): String? { - val parentFilePath = type.getFilePath(siteIdRootDirectoryPath) - val filePath = File(parentFilePath, type.getFileName()) - - if (!filePath.exists()) return null - - // Even though we check file existence before reading them, there were still a few instances reported where - // the files were deleted before they are read completely. - // Reading them in a try-catch block helps preventing crashes occurring in customer apps in such scenarios - val fileContents = try { - filePath.readText() - } catch (ex: Exception) { - logger.error("error while reading file $type. path ${filePath.absolutePath}. message: ${ex.message}") - null - } - if (fileContents.isNullOrBlank()) return null - - return fileContents - } - - fun delete(type: FileType): Boolean { - val parentFilePath = type.getFilePath(siteIdRootDirectoryPath) - val filePath = File(parentFilePath, type.getFileName()) - - return try { - filePath.delete() - } catch (e: Throwable) { - logger.error("error while deleting file $type. path ${filePath.absolutePath}. message: ${e.message}") - false - } - } - - // Used for tests to run between tests for a clean file system. - fun deleteAllSdkFiles(path: File = sdkRootDirectoryPath) { - if (path.isDirectory) { - path.list()?.forEach { child -> - deleteAllSdkFiles(File(path, child)) - } - } else { - path.delete() - } - } -} diff --git a/sdk/src/main/java/io/customer/sdk/data/store/FileType.kt b/sdk/src/main/java/io/customer/sdk/data/store/FileType.kt deleted file mode 100644 index 0eb18f3b7..000000000 --- a/sdk/src/main/java/io/customer/sdk/data/store/FileType.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.customer.sdk.data.store - -import java.io.File - -/** - * Type of file being read/written with [FileStorage]. - */ -sealed interface FileType { - fun getFilePath(existingPath: File): File - fun getFileName(): String - - class QueueInventory : FileType { - override fun getFileName(): String = "inventory.json" - override fun getFilePath(existingPath: File): File = File(existingPath, "queue") - } - - class QueueTask(private val fileId: String) : FileType { - override fun getFileName(): String = "$fileId.json" - override fun getFilePath(existingPath: File): File = File(File(existingPath, "queue"), "tasks") - } -} diff --git a/sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt b/sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt index 01af55dcc..9f83b439d 100644 --- a/sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt +++ b/sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt @@ -1,27 +1,17 @@ package io.customer.sdk.di import android.content.Context -import com.squareup.moshi.Moshi -import io.customer.sdk.CustomerIOActivityLifecycleCallbacks import io.customer.sdk.CustomerIOConfig -import io.customer.sdk.api.* -import io.customer.sdk.api.interceptors.HeadersInterceptor -import io.customer.sdk.data.moshi.adapter.BigDecimalAdapter -import io.customer.sdk.data.moshi.adapter.CustomAttributesFactory -import io.customer.sdk.data.moshi.adapter.UnixDateAdapter -import io.customer.sdk.data.store.* import io.customer.sdk.hooks.CioHooksManager import io.customer.sdk.hooks.HooksManager -import io.customer.sdk.queue.* -import io.customer.sdk.repository.* +import io.customer.sdk.lifecycle.CustomerIOActivityLifecycleCallbacks +import io.customer.sdk.module.CustomerIOAnalytics import io.customer.sdk.repository.preference.SitePreferenceRepository import io.customer.sdk.repository.preference.SitePreferenceRepositoryImpl -import io.customer.sdk.util.* -import java.util.concurrent.TimeUnit -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory +import io.customer.sdk.util.DateUtil +import io.customer.sdk.util.DateUtilImpl +import io.customer.sdk.util.DispatchersProvider +import io.customer.sdk.util.Logger /** * Configuration class to configure/initialize low-level operations and objects. @@ -29,197 +19,31 @@ import retrofit2.converter.moshi.MoshiConverterFactory class CustomerIOComponent( private val staticComponent: CustomerIOStaticComponent, val context: Context, + val analyticsModule: CustomerIOAnalytics, val sdkConfig: CustomerIOConfig ) : DiGraph() { - val fileStorage: FileStorage - get() = override() ?: FileStorage(config = sdkConfig, context = context, logger = logger) - - val jsonAdapter: JsonAdapter - get() = override() ?: JsonAdapter(moshi = moshi) - - val queueStorage: QueueStorage - get() = override() ?: QueueStorageImpl( - sdkConfig = sdkConfig, - fileStorage = fileStorage, - jsonAdapter = jsonAdapter, - dateUtil = dateUtil, - logger = logger - ) - - val queueRunner: QueueRunner - get() = override() ?: QueueRunnerImpl( - jsonAdapter = jsonAdapter, - cioHttpClient = cioHttpClient, - logger = logger - ) - val dispatchersProvider: DispatchersProvider get() = override() ?: staticComponent.dispatchersProvider - val queue: Queue - get() = override() ?: getSingletonInstanceCreate { - QueueImpl( - dispatchersProvider = dispatchersProvider, - storage = queueStorage, - runRequest = queueRunRequest, - jsonAdapter = jsonAdapter, - sdkConfig = sdkConfig, - queueTimer = timer, - logger = logger, - dateUtil = dateUtil - ) - } - - internal val cleanupRepository: CleanupRepository - get() = override() ?: CleanupRepositoryImpl(queue = queue) - - val queueQueryRunner: QueueQueryRunner - get() = override() ?: QueueQueryRunnerImpl(logger = logger) - - val queueRunRequest: QueueRunRequest - get() = override() ?: QueueRunRequestImpl( - runner = queueRunner, - queueStorage = queueStorage, - logger = logger, - queryRunner = queueQueryRunner - ) - val logger: Logger get() = override() ?: staticComponent.logger val hooksManager: HooksManager get() = override() ?: getSingletonInstanceCreate { CioHooksManager() } - private val cioHttpClient: TrackingHttpClient - get() = override() ?: RetrofitTrackingHttpClient( - retrofitService = buildRetrofitApi(), - httpRequestRunner = httpRequestRunner - ) - - private val httpRequestRunner: HttpRequestRunner - get() = HttpRequestRunnerImpl( - prefsRepository = sitePreferenceRepository, - logger = logger, - retryPolicy = cioHttpRetryPolicy, - jsonAdapter = jsonAdapter - ) - - val cioHttpRetryPolicy: HttpRetryPolicy - get() = override() ?: CustomerIOApiRetryPolicy() - val dateUtil: DateUtil get() = override() ?: DateUtilImpl() - val timer: SimpleTimer - get() = override() ?: AndroidSimpleTimer( - logger = logger, - dispatchersProvider = dispatchersProvider - ) - - val trackRepository: TrackRepository - get() = override() ?: TrackRepositoryImpl( - sitePreferenceRepository = sitePreferenceRepository, - backgroundQueue = queue, - logger = logger, - hooksManager = hooksManager - ) - - val profileRepository: ProfileRepository - get() = override() ?: ProfileRepositoryImpl( - deviceRepository = deviceRepository, - sitePreferenceRepository = sitePreferenceRepository, - backgroundQueue = queue, - logger = logger, - hooksManager = hooksManager - ) - - val deviceRepository: DeviceRepository - get() = override() ?: DeviceRepositoryImpl( - config = sdkConfig, - deviceStore = buildStore().deviceStore, - sitePreferenceRepository = sitePreferenceRepository, - backgroundQueue = queue, - dateUtil = dateUtil, - logger = logger - ) - val activityLifecycleCallbacks: CustomerIOActivityLifecycleCallbacks get() = override() ?: getSingletonInstanceCreate { CustomerIOActivityLifecycleCallbacks(config = sdkConfig) } - private fun buildStore(): CustomerIOStore { - return override() ?: object : CustomerIOStore { - override val deviceStore: DeviceStore by lazy { - DeviceStoreImp( - sdkConfig = sdkConfig, - buildStore = BuildStoreImp(), - applicationStore = ApplicationStoreImp(context), - version = sdkConfig.client.sdkVersion - ) - } - } - } - val sitePreferenceRepository: SitePreferenceRepository by lazy { override() ?: SitePreferenceRepositoryImpl( context = context, config = sdkConfig ) } - - private inline fun buildRetrofitApi(): T { - val apiClass = T::class.java - return override() ?: buildRetrofit( - endpoint = sdkConfig.trackingApiHostname, - timeout = sdkConfig.timeout - ).create(apiClass) - } - - private val httpLoggingInterceptor by lazy { - override() ?: HttpLoggingInterceptor().apply { - if (staticComponent.staticSettingsProvider.isDebuggable) { - level = HttpLoggingInterceptor.Level.BODY - } - } - } - - // performance improvement to keep created moshi instance for re-use. - val moshi: Moshi by lazy { - override() ?: Moshi.Builder() - .add(UnixDateAdapter()) - .add(BigDecimalAdapter()) - .add(CustomAttributesFactory()) - .build() - } - - private fun buildRetrofit( - endpoint: String, - timeout: Long - ): Retrofit { - val okHttpClient = clientBuilder(timeout).build() - return override() ?: Retrofit.Builder() - .baseUrl(endpoint) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .client(okHttpClient) - .build() - } - - private val baseClient: OkHttpClient by lazy { override() ?: OkHttpClient() } - - private fun baseClientBuilder(): OkHttpClient.Builder = override() ?: baseClient.newBuilder() - - private fun clientBuilder( - timeout: Long - ): OkHttpClient.Builder { - return override() ?: baseClientBuilder() - // timeouts - .connectTimeout(timeout, TimeUnit.MILLISECONDS) - .writeTimeout(timeout, TimeUnit.MILLISECONDS) - .readTimeout(timeout, TimeUnit.MILLISECONDS) - // interceptors - .addInterceptor(HeadersInterceptor(buildStore(), sdkConfig)) - .addInterceptor(httpLoggingInterceptor) - } } diff --git a/sdk/src/main/java/io/customer/sdk/error/CustomerIOApiErrorResponse.kt b/sdk/src/main/java/io/customer/sdk/error/CustomerIOApiErrorResponse.kt deleted file mode 100644 index 36055826a..000000000 --- a/sdk/src/main/java/io/customer/sdk/error/CustomerIOApiErrorResponse.kt +++ /dev/null @@ -1,43 +0,0 @@ -package io.customer.sdk.error - -import com.squareup.moshi.JsonClass - -/** -The API returns error response bodies in the format: -``` -{"meta": { "error": "invalid id" }} -``` - */ -@JsonClass(generateAdapter = true) -internal data class CustomerIOApiErrorResponse( - val meta: Meta -) { - - // created property because Moshi cannot create adapter that extends Throwable - val throwable: Throwable = Throwable(meta.error) - - @JsonClass(generateAdapter = true) - data class Meta( - val error: String - ) -} - -/** -The API returns error response bodies in the format: -``` -{"meta": { "errors": ["invalid id"] }} -``` - */ -@JsonClass(generateAdapter = true) -internal data class CustomerIOApiErrorsResponse( - val meta: Meta -) { - - // created property because Moshi cannot create adapter that extends Throwable - val throwable: Throwable = Throwable(meta.errors.joinToString(", ")) - - @JsonClass(generateAdapter = true) - data class Meta( - val errors: List - ) -} diff --git a/sdk/src/main/java/io/customer/sdk/error/CustomerIOError.kt b/sdk/src/main/java/io/customer/sdk/error/CustomerIOError.kt deleted file mode 100644 index 6b0ebd696..000000000 --- a/sdk/src/main/java/io/customer/sdk/error/CustomerIOError.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.customer.sdk.error - -/** - * Public facing errors that the CustomerIO SDK can create. - */ -internal sealed class CustomerIOError(message: String) : Throwable(message) { - class Unauthorized : CustomerIOError("HTTP request responded with 401. Configure the SDK with valid credentials.") - class HttpRequestsPaused : CustomerIOError("HTTP request skipped. All HTTP requests are paused for the time being.") - class NoHttpRequestMade : CustomerIOError("HTTP request was not able to be made. It might be an Internet connection issue. Try again later.") - class BadRequest400(messageFromServer: String) : CustomerIOError("HTTP request responded with 400 - $messageFromServer") - class ServerDown : CustomerIOError("Customer.io API server unavailable. It's best to wait and try again later.") - data class UnsuccessfulStatusCode(val code: Int, val apiMessage: String) : CustomerIOError("Customer.io API server response - code: $code, error message: $apiMessage") -} diff --git a/sdk/src/main/java/io/customer/sdk/extensions/RetrofitExtensions.kt b/sdk/src/main/java/io/customer/sdk/extensions/RetrofitExtensions.kt deleted file mode 100644 index 19eef5566..000000000 --- a/sdk/src/main/java/io/customer/sdk/extensions/RetrofitExtensions.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.customer.sdk.extensions - -import retrofit2.HttpException -import retrofit2.Response - -private fun Response.bodyOrThrow(): T { - if (!isSuccessful) throw HttpException(this) - return body()!! -} - -private fun Response.toException() = HttpException(this) - -fun Response.toResultUnit(): Result = toResult { } - -fun Response.toResult(): Result = toResult { it } - -fun Response.toResult(mapper: (T) -> E): Result { - return try { - if (isSuccessful) { - Result.success(mapper(bodyOrThrow())) - } else { - Result.failure(toException()) - } - } catch (e: Exception) { - Result.failure(e) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/CustomerIOActivityLifecycleCallbacks.kt b/sdk/src/main/java/io/customer/sdk/lifecycle/CustomerIOActivityLifecycleCallbacks.kt similarity index 97% rename from sdk/src/main/java/io/customer/sdk/CustomerIOActivityLifecycleCallbacks.kt rename to sdk/src/main/java/io/customer/sdk/lifecycle/CustomerIOActivityLifecycleCallbacks.kt index 8337bb88f..424bcd559 100644 --- a/sdk/src/main/java/io/customer/sdk/CustomerIOActivityLifecycleCallbacks.kt +++ b/sdk/src/main/java/io/customer/sdk/lifecycle/CustomerIOActivityLifecycleCallbacks.kt @@ -1,4 +1,4 @@ -package io.customer.sdk +package io.customer.sdk.lifecycle import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks @@ -6,7 +6,8 @@ import android.os.Bundle import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import io.customer.base.internal.InternalCustomerIOApi -import io.customer.sdk.lifecycle.LifecycleCallback +import io.customer.sdk.CustomerIO +import io.customer.sdk.CustomerIOConfig class CustomerIOActivityLifecycleCallbacks internal constructor( private val config: CustomerIOConfig diff --git a/sdk/src/main/java/io/customer/sdk/module/AnalyticsModule.kt b/sdk/src/main/java/io/customer/sdk/module/AnalyticsModule.kt new file mode 100644 index 000000000..d5b036331 --- /dev/null +++ b/sdk/src/main/java/io/customer/sdk/module/AnalyticsModule.kt @@ -0,0 +1,61 @@ +package io.customer.sdk.module + +import android.app.Activity +import io.customer.sdk.data.model.CustomAttributes +import io.customer.sdk.data.request.MetricEvent + +interface AnalyticsModule : CustomerIOModule { + suspend fun cleanup() + var profileAttributes: CustomAttributes + var deviceAttributes: CustomAttributes + + val registeredDeviceToken: String? + + fun identify(identifier: String) + + fun identify( + identifier: String, + attributes: Map + ) + + fun track(name: String) + + fun track( + name: String, + attributes: Map + ) + + fun screen(name: String) + + fun screen( + name: String, + attributes: Map + ) + + fun screen(activity: Activity) + + fun screen( + activity: Activity, + attributes: Map + ) + + fun clearIdentify() + + fun registerDeviceToken(deviceToken: String, deviceAttributes: CustomAttributes) + + fun deleteDeviceToken() + + fun trackMetric( + deliveryID: String, + event: MetricEvent, + deviceToken: String + ) + + fun addCustomDeviceAttributes(deviceAttributes: CustomAttributes) + fun addCustomProfileAttributes(deviceAttributes: CustomAttributes) + fun trackInAppMetric( + deliveryID: String, + event: MetricEvent, + metadata: Map = emptyMap() + ) +} diff --git a/sdk/src/main/java/io/customer/sdk/module/CustomerIOAnalytics.kt b/sdk/src/main/java/io/customer/sdk/module/CustomerIOAnalytics.kt new file mode 100644 index 000000000..3bb558003 --- /dev/null +++ b/sdk/src/main/java/io/customer/sdk/module/CustomerIOAnalytics.kt @@ -0,0 +1,3 @@ +package io.customer.sdk.module + +typealias CustomerIOAnalytics = AnalyticsModule diff --git a/sdk/src/main/java/io/customer/sdk/queue/Queue.kt b/sdk/src/main/java/io/customer/sdk/queue/Queue.kt deleted file mode 100644 index e6a2a4545..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/Queue.kt +++ /dev/null @@ -1,269 +0,0 @@ -package io.customer.sdk.queue - -import io.customer.sdk.CustomerIOConfig -import io.customer.sdk.data.model.CustomAttributes -import io.customer.sdk.data.model.EventType -import io.customer.sdk.data.request.* -import io.customer.sdk.queue.taskdata.DeletePushNotificationQueueTaskData -import io.customer.sdk.queue.taskdata.IdentifyProfileQueueTaskData -import io.customer.sdk.queue.taskdata.RegisterPushNotificationQueueTaskData -import io.customer.sdk.queue.taskdata.TrackEventQueueTaskData -import io.customer.sdk.queue.type.QueueModifyResult -import io.customer.sdk.queue.type.QueueStatus -import io.customer.sdk.queue.type.QueueTaskGroup -import io.customer.sdk.queue.type.QueueTaskType -import io.customer.sdk.util.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -interface Queue { - fun queueIdentifyProfile( - newIdentifier: String, - oldIdentifier: String?, - attributes: CustomAttributes - ): QueueModifyResult - - fun queueTrack( - identifiedProfileId: String, - name: String, - eventType: EventType, - attributes: CustomAttributes - ): QueueModifyResult - - fun queueRegisterDevice(identifiedProfileId: String, device: Device): QueueModifyResult - fun queueDeletePushToken(identifiedProfileId: String, deviceToken: String): QueueModifyResult - fun queueTrackMetric( - deliveryId: String, - deviceToken: String, - event: MetricEvent - ): QueueModifyResult - - fun queueTrackInAppMetric( - deliveryId: String, - event: MetricEvent, - metadata: Map - ): QueueModifyResult - - fun , TaskData : Any> addTask( - type: TaskType, - data: TaskData, - groupStart: QueueTaskGroup? = null, - blockingGroups: List? = null - ): QueueModifyResult - - suspend fun run() - - fun deleteExpiredTasks() - - fun cancelTimer() -} - -internal class QueueImpl internal constructor( - private val dispatchersProvider: DispatchersProvider, - private val storage: QueueStorage, - private val runRequest: QueueRunRequest, - private val jsonAdapter: JsonAdapter, - private val sdkConfig: CustomerIOConfig, - private val queueTimer: SimpleTimer, - private val logger: Logger, - private val dateUtil: DateUtil -) : Queue { - - private val numberSecondsToScheduleTimer: Seconds - get() = Seconds(sdkConfig.backgroundQueueSecondsDelay) - - @Volatile - var isRunningRequest: Boolean = false - - override fun , TaskData : Any> addTask( - type: TaskType, - data: TaskData, - groupStart: QueueTaskGroup?, - blockingGroups: List? - ): QueueModifyResult { - synchronized(this) { - logger.info("adding queue task $type") - - val taskDataString = jsonAdapter.toJson(data) - - // What do we do if a queue task doesn't successfully get added to the queue? - // - val createTaskResult = storage.create( - type = type.name, - data = taskDataString, - groupStart = groupStart, - blockingGroups = blockingGroups - ) - logger.debug("added queue task data $taskDataString") - - processQueueStatus(createTaskResult.queueStatus) - - return createTaskResult - } - } - - override suspend fun run() { - synchronized(this) { - queueTimer.cancel() - - val isQueueRunningRequest = isRunningRequest - if (isQueueRunningRequest) return - - isRunningRequest = true - } - - runRequest.run() - - synchronized(this) { - // reset queue to be able to run again - isRunningRequest = false - } - } - - internal fun runAsync() { - CoroutineScope(dispatchersProvider.background).launch { - run() - } - } - - private fun processQueueStatus(queueStatus: QueueStatus) { - logger.debug("processing queue status $queueStatus") - val isManyTasksInQueue = - queueStatus.numTasksInQueue >= sdkConfig.backgroundQueueMinNumberOfTasks - - if (isManyTasksInQueue) { - logger.info("queue met criteria to run automatically") - - runAsync() - } else { - // Not enough tasks in the queue yet to run it now, so let's schedule them to run in the future. - // It's expected that only 1 timer instance exists and is running in the SDK. - val didSchedule = queueTimer.scheduleIfNotAlready(numberSecondsToScheduleTimer) { - logger.info("queue timer: now running queue") - - runAsync() - } - - if (didSchedule) logger.info("queue timer: scheduled to run queue in $numberSecondsToScheduleTimer seconds") - } - } - - override fun queueRegisterDevice( - identifiedProfileId: String, - device: Device - ): QueueModifyResult { - return addTask( - QueueTaskType.RegisterDeviceToken, - RegisterPushNotificationQueueTaskData(identifiedProfileId, device), - groupStart = QueueTaskGroup.RegisterPushToken(device.token), - blockingGroups = listOf(QueueTaskGroup.IdentifyProfile(identifiedProfileId)) - ) - } - - override fun queueDeletePushToken( - identifiedProfileId: String, - deviceToken: String - ): QueueModifyResult { - return addTask( - QueueTaskType.DeletePushToken, - DeletePushNotificationQueueTaskData(identifiedProfileId, deviceToken), - // only delete a device token after it has successfully been registered. - blockingGroups = listOf(QueueTaskGroup.RegisterPushToken(deviceToken)) - ) - } - - override fun queueTrack( - identifiedProfileId: String, - name: String, - eventType: EventType, - attributes: CustomAttributes - ): QueueModifyResult { - val event = Event( - name = name, - type = eventType, - data = attributes, - timestamp = dateUtil.nowUnixTimestamp - ) - - return addTask( - QueueTaskType.TrackEvent, - TrackEventQueueTaskData(identifiedProfileId, event), - blockingGroups = listOf(QueueTaskGroup.IdentifyProfile(identifiedProfileId)) - ) - } - - override fun queueTrackMetric( - deliveryId: String, - deviceToken: String, - event: MetricEvent - ): QueueModifyResult { - return addTask( - QueueTaskType.TrackPushMetric, - Metric( - deliveryID = deliveryId, - deviceToken = deviceToken, - event = event, - timestamp = dateUtil.now - ), - blockingGroups = listOf(QueueTaskGroup.RegisterPushToken(deviceToken)) - ) - } - - override fun queueTrackInAppMetric( - deliveryId: String, - event: MetricEvent, - metadata: Map - ): QueueModifyResult { - return addTask( - QueueTaskType.TrackDeliveryEvent, - DeliveryEvent( - type = DeliveryType.in_app, - payload = DeliveryPayload( - deliveryID = deliveryId, - event = event, - timestamp = dateUtil.now, - metaData = metadata - ) - ), - blockingGroups = emptyList() - ) - } - - override fun queueIdentifyProfile( - newIdentifier: String, - oldIdentifier: String?, - attributes: CustomAttributes - ): QueueModifyResult { - val isFirstTimeIdentifying = oldIdentifier == null - val isChangingIdentifiedProfile = oldIdentifier != null && oldIdentifier != newIdentifier - - // If SDK previously identified profile X and X is being identified again, no use blocking the queue with a queue group. - val queueGroupStart = - if (isFirstTimeIdentifying || isChangingIdentifiedProfile) { - QueueTaskGroup.IdentifyProfile( - newIdentifier - ) - } else { - null - } - // If there was a previously identified profile, or, we are just adding attributes to an existing profile, we need to wait for - // this operation until the previous identify runs successfully. - val blockingGroups = - if (!isFirstTimeIdentifying) listOf(QueueTaskGroup.IdentifyProfile(oldIdentifier!!)) else null - - return addTask( - QueueTaskType.IdentifyProfile, - IdentifyProfileQueueTaskData(newIdentifier, attributes), - groupStart = queueGroupStart, - blockingGroups = blockingGroups - ) - } - - override fun deleteExpiredTasks() { - storage.deleteExpired() - } - - override fun cancelTimer() { - queueTimer.cancel() - } -} diff --git a/sdk/src/main/java/io/customer/sdk/queue/QueueQueryRunner.kt b/sdk/src/main/java/io/customer/sdk/queue/QueueQueryRunner.kt deleted file mode 100644 index e66eccab5..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/QueueQueryRunner.kt +++ /dev/null @@ -1,63 +0,0 @@ -package io.customer.sdk.queue - -import io.customer.sdk.queue.type.QueueTaskMetadata -import io.customer.sdk.util.Logger - -interface QueueQueryRunner { - fun getNextTask(queue: List, lastFailedTask: QueueTaskMetadata?): QueueTaskMetadata? - fun reset() -} - -internal class QueueQueryRunnerImpl( - private val logger: Logger -) : QueueQueryRunner { - internal val queryCriteria = QueueQueryCriteria() - - override fun getNextTask(queue: List, lastFailedTask: QueueTaskMetadata?): QueueTaskMetadata? { - if (queue.isEmpty()) return null - if (lastFailedTask != null) updateCriteria(lastFailedTask) - - // log *after* updating the criteria - logger.debug("queue querying next task. criteria: $queryCriteria") - - return queue.firstOrNull { doesTaskPassCriteria(it) } - } - - internal fun updateCriteria(lastFailedTask: QueueTaskMetadata) { - lastFailedTask.groupStart?.let { queueGroupName -> - queryCriteria.excludeGroups.add(queueGroupName) - } - } - - private fun doesTaskPassCriteria(task: QueueTaskMetadata): Boolean { - // At this time, function only contains 1 query criteria. If more were added in the future, we can chain them together: - // queryCriteria.doesTaskX() ?: queryCriteria.doesTaskY() - return !doesTaskBelongToExcludedGroup(task) - } - - private fun doesTaskBelongToExcludedGroup(task: QueueTaskMetadata): Boolean { - task.groupMember?.let { groupsTaskBelongsTo -> - queryCriteria.excludeGroups.forEach { groupToExclude -> - if (groupsTaskBelongsTo.contains(groupToExclude)) { - return true - } - } - } - - return false - } - - override fun reset() { - logger.debug("resetting queue tasks query criteria") - - queryCriteria.reset() - } - - internal data class QueueQueryCriteria( - val excludeGroups: MutableSet = mutableSetOf() - ) { - fun reset() { - excludeGroups.clear() - } - } -} diff --git a/sdk/src/main/java/io/customer/sdk/queue/QueueRunRequest.kt b/sdk/src/main/java/io/customer/sdk/queue/QueueRunRequest.kt deleted file mode 100644 index 862e8e48d..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/QueueRunRequest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package io.customer.sdk.queue - -import io.customer.sdk.error.CustomerIOError -import io.customer.sdk.queue.type.QueueTaskMetadata -import io.customer.sdk.util.Logger - -interface QueueRunRequest { - suspend fun run() -} - -internal class QueueRunRequestImpl internal constructor( - private val runner: QueueRunner, - private val queueStorage: QueueStorage, - private val logger: Logger, - private val queryRunner: QueueQueryRunner -) : QueueRunRequest { - - override suspend fun run() { - logger.debug("queue starting to run tasks...") - val inventory = queueStorage.getInventory() - val tasksToRun = inventory.toMutableList() - - var lastFailedTask: QueueTaskMetadata? = null - - while (tasksToRun.isNotEmpty()) { - // get the next task to run using the query criteria - val currentTaskMetadata = queryRunner.getNextTask(tasksToRun, lastFailedTask) - if (currentTaskMetadata == null) { - logger.debug("queue out of tasks to run...") - break - } - - tasksToRun.remove(currentTaskMetadata) - - val taskStorageId = currentTaskMetadata.taskPersistedId - val taskToRun = queueStorage.get(taskStorageId) - - if (taskToRun == null) { - logger.error("Tried to get queue task with storage id: $taskStorageId but storage couldn't find it.") - // The task failed to execute because it couldn't be found. Gracefully handle the scenario by - // behaving the same way a failed HTTP request does. Run next task with an updated `lastFailedTask`. - lastFailedTask = currentTaskMetadata - continue - } - - logger.debug("queue tasks left to run: ${tasksToRun.size}") - logger.debug("queue next task to run: $taskStorageId, ${taskToRun.type}, ${taskToRun.data}, ${taskToRun.runResults}") - - val result = runner.runTask(taskToRun) - - when { - result.isSuccess -> { - logger.debug("queue task $taskStorageId ran successfully") - logger.debug("queue deleting task $taskStorageId") - queueStorage.delete(taskStorageId) - } - - result.isFailure -> { - val error = result.exceptionOrNull() - logger.debug("queue task $taskStorageId run failed $error") - - when (error as? CustomerIOError) { - is CustomerIOError.HttpRequestsPaused, is CustomerIOError.NoHttpRequestMade -> { - logger.info("queue is quitting early because ${error.message})") - tasksToRun.clear() // clear the list to stop processing the next tasks - break - } - - is CustomerIOError.BadRequest400 -> { - logger.error("Received HTTP 400 response while trying to run ${taskToRun.type}. 400 responses never succeed and therefore, the SDK is deleting this SDK request and not retry. Error message from API: ${error.message}, request data sent: ${taskToRun.data}") - queueStorage.delete(taskStorageId) - } - - else -> { - val previousRunResults = taskToRun.runResults - val newRunResults = - taskToRun.runResults.copy(totalRuns = previousRunResults.totalRuns + 1) - logger.debug("queue task $taskStorageId, updating run history from: $previousRunResults to: $newRunResults") - queueStorage.update(taskStorageId, newRunResults) - } - } - lastFailedTask = currentTaskMetadata - } - } - } - logger.debug("queue done running tasks") - queryRunner.reset() - } -} diff --git a/sdk/src/main/java/io/customer/sdk/queue/QueueRunner.kt b/sdk/src/main/java/io/customer/sdk/queue/QueueRunner.kt deleted file mode 100644 index b50e16768..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/QueueRunner.kt +++ /dev/null @@ -1,89 +0,0 @@ -package io.customer.sdk.queue - -import io.customer.sdk.api.TrackingHttpClient -import io.customer.sdk.data.request.DeliveryEvent -import io.customer.sdk.data.request.Metric -import io.customer.sdk.extensions.valueOfOrNull -import io.customer.sdk.queue.taskdata.DeletePushNotificationQueueTaskData -import io.customer.sdk.queue.taskdata.IdentifyProfileQueueTaskData -import io.customer.sdk.queue.taskdata.RegisterPushNotificationQueueTaskData -import io.customer.sdk.queue.taskdata.TrackEventQueueTaskData -import io.customer.sdk.queue.type.QueueRunTaskResult -import io.customer.sdk.queue.type.QueueTask -import io.customer.sdk.queue.type.QueueTaskType -import io.customer.sdk.util.JsonAdapter -import io.customer.sdk.util.Logger -import java.io.IOException - -interface QueueRunner { - suspend fun runTask(task: QueueTask): QueueRunTaskResult -} - -internal class QueueRunnerImpl( - private val jsonAdapter: JsonAdapter, - private val cioHttpClient: TrackingHttpClient, - private val logger: Logger -) : QueueRunner { - override suspend fun runTask(task: QueueTask): QueueRunTaskResult { - val taskResult = when (valueOfOrNull(task.type)) { - QueueTaskType.IdentifyProfile -> identifyProfile(task) - QueueTaskType.TrackEvent -> trackEvent(task) - QueueTaskType.RegisterDeviceToken -> registerDeviceToken(task) - QueueTaskType.DeletePushToken -> deleteDeviceToken(task) - QueueTaskType.TrackPushMetric -> trackPushMetrics(task) - QueueTaskType.TrackDeliveryEvent -> trackDeliveryEvents(task) - null -> null - } - return if (taskResult != null) { - taskResult - } else { - val errorMessage = - "Queue task ${task.type} could not find an enum to map to. Could not run task." - logger.error(errorMessage) - Result.failure(RuntimeException(errorMessage)) - } - } - - private suspend fun identifyProfile(task: QueueTask): QueueRunTaskResult? { - val taskData: IdentifyProfileQueueTaskData = - jsonAdapter.fromJsonOrNull(task.data) ?: return null - - return cioHttpClient.identifyProfile(taskData.identifier, taskData.attributes) - } - - private suspend fun trackEvent(task: QueueTask): QueueRunTaskResult? { - val taskData: TrackEventQueueTaskData = jsonAdapter.fromJsonOrNull(task.data) ?: return null - - return cioHttpClient.track(taskData.identifier, taskData.event) - } - - private suspend fun deleteDeviceToken(task: QueueTask): QueueRunTaskResult? { - val taskData: DeletePushNotificationQueueTaskData = - jsonAdapter.fromJsonOrNull(task.data) ?: return null - - return cioHttpClient.deleteDevice(taskData.profileIdentified, taskData.deviceToken) - } - - private suspend fun registerDeviceToken(task: QueueTask): QueueRunTaskResult? { - val taskData: RegisterPushNotificationQueueTaskData = - jsonAdapter.fromJsonOrNull(task.data) ?: return null - - return cioHttpClient.registerDevice( - taskData.profileIdentified, - taskData.device - ) - } - - private suspend fun trackPushMetrics(task: QueueTask): QueueRunTaskResult? { - val taskData: Metric = jsonAdapter.fromJsonOrNull(task.data) ?: return null - - return cioHttpClient.trackPushMetrics(taskData) - } - - @Throws(IOException::class, RuntimeException::class) - private suspend fun trackDeliveryEvents(task: QueueTask): QueueRunTaskResult? { - val taskData: DeliveryEvent = jsonAdapter.fromJsonOrNull(task.data) ?: return null - - return cioHttpClient.trackDeliveryEvents(taskData) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/queue/QueueStorage.kt b/sdk/src/main/java/io/customer/sdk/queue/QueueStorage.kt deleted file mode 100644 index bb3dcd12f..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/QueueStorage.kt +++ /dev/null @@ -1,172 +0,0 @@ -package io.customer.sdk.queue - -import io.customer.base.extenstions.isOlderThan -import io.customer.base.extenstions.subtract -import io.customer.sdk.CustomerIOConfig -import io.customer.sdk.data.store.FileStorage -import io.customer.sdk.data.store.FileType -import io.customer.sdk.queue.type.* -import io.customer.sdk.util.DateUtil -import io.customer.sdk.util.JsonAdapter -import io.customer.sdk.util.Logger -import io.customer.sdk.util.toSeconds -import java.util.* -import java.util.concurrent.TimeUnit - -interface QueueStorage { - fun getInventory(): QueueInventory - fun saveInventory(inventory: QueueInventory): Boolean - fun create( - type: String, - data: String, - groupStart: QueueTaskGroup?, - blockingGroups: List? - ): QueueModifyResult - - fun update(taskStorageId: String, runResults: QueueTaskRunResults): Boolean - fun get(taskStorageId: String): QueueTask? - fun delete(taskStorageId: String): QueueModifyResult - fun deleteExpired(): List -} - -internal class QueueStorageImpl internal constructor( - private val sdkConfig: CustomerIOConfig, - private val fileStorage: FileStorage, - private val jsonAdapter: JsonAdapter, - private val dateUtil: DateUtil, - private val logger: Logger -) : QueueStorage { - - @Synchronized - override fun getInventory(): QueueInventory { - val dataFromFile = fileStorage.get(FileType.QueueInventory()) ?: return emptyList() - return jsonAdapter.fromJsonListOrNull(dataFromFile) ?: emptyList() - } - - @Synchronized - override fun saveInventory(inventory: QueueInventory): Boolean { - val fileContents = jsonAdapter.toJson(inventory) - return fileStorage.save(FileType.QueueInventory(), fileContents) - } - - @Synchronized - override fun create( - type: String, - data: String, - groupStart: QueueTaskGroup?, - blockingGroups: List? - ): QueueModifyResult { - val existingInventory = getInventory().toMutableList() - val beforeCreateQueueStatus = QueueStatus(sdkConfig.siteId, existingInventory.count()) - - val newTaskStorageId = UUID.randomUUID().toString() - val newQueueTask = QueueTask(newTaskStorageId, type, data, QueueTaskRunResults(0)) - - if (!update(newQueueTask)) { - logger.error("error trying to add new queue task to queue. $newQueueTask") - return QueueModifyResult(false, beforeCreateQueueStatus) - } - - // Update the inventory *after* a successful insert of the new task into storage. When a task is added to the inventory, it's assumed the task is available in device storage for use. - val newInventoryItem = QueueTaskMetadata( - newTaskStorageId, - type, - // Usually, we do not convert data types to a string before converting to a JSON string. We left the JSON parser to take care of the conversion for us. For groups, a String data type works good for use in the background queue. - groupStart?.toString(), - blockingGroups?.map { it.toString() }, - dateUtil.now - ) - existingInventory.add(newInventoryItem) - - val updatedInventoryCount = existingInventory.count() - val afterCreateQueueStatus = QueueStatus(sdkConfig.siteId, updatedInventoryCount) - - if (!saveInventory(existingInventory)) { - logger.error("error trying to add new queue task to inventory. task: $newQueueTask, inventory item: $newInventoryItem") - return QueueModifyResult(false, beforeCreateQueueStatus) - } - - return QueueModifyResult(true, afterCreateQueueStatus) - } - - @Synchronized - override fun update(taskStorageId: String, runResults: QueueTaskRunResults): Boolean { - var existingQueueTask = get(taskStorageId) ?: return false - - existingQueueTask = existingQueueTask.copy(runResults = runResults) - - return update(existingQueueTask) - } - - @Synchronized - override fun get(taskStorageId: String): QueueTask? { - val fileContents = fileStorage.get(FileType.QueueTask(taskStorageId)) ?: return null - return jsonAdapter.fromJsonOrNull(fileContents) - } - - @Synchronized - override fun delete(taskStorageId: String): QueueModifyResult { - // update inventory first so if any deletion operation is unsuccessful, at least the inventory will not contain the task so queue doesn't try running it. - val existingInventory = getInventory().toMutableList() - val queueStatusBeforeModifyInventory = - QueueStatus(sdkConfig.siteId, existingInventory.count()) - - existingInventory.removeAll { it.taskPersistedId == taskStorageId } - - if (!saveInventory(existingInventory) || !fileStorage.delete( - FileType.QueueTask( - taskStorageId - ) - ) - ) { - logger.error("error trying to delete task with storage id: $taskStorageId from queue") - return QueueModifyResult(false, queueStatusBeforeModifyInventory) - } - - return QueueModifyResult(true, QueueStatus(sdkConfig.siteId, existingInventory.count())) - } - - @Synchronized - override fun deleteExpired(): List { - logger.debug("deleting expired tasks from the queue") - - val tasksToDelete: MutableSet = mutableSetOf() - val queueTaskExpiredThreshold = Date().subtract( - sdkConfig.backgroundQueueTaskExpiredSeconds.toSeconds().value, - TimeUnit.SECONDS - ) - logger.debug("deleting tasks older then $queueTaskExpiredThreshold, current time is: ${Date()}") - - getInventory().filter { - // Do not delete tasks that are at the start of a group of tasks. - // Why? Take for example Identifying a profile. If we identify profile X in an app today, we expire the Identify queue task and delete the - // queue task, and then profile X stays logged into an app for 6 months, that means we run the risk of 6 months of data never - // successfully being sent to the API. - // Also, queue tasks such as Identifying a profile are more rare queue tasks compared to tracking of events (that are not the start of a group). So, it should rarely - // be a scenario when there are thousands of "expired" Identifying a profile tasks sitting in a queue. It's the queue tasks such as - // tracking that are taking up a large majority of the queue inventory. Those we should be deleting more of. - it.groupStart == null - }.forEach { taskInventoryItem -> - val isItemTooOld = taskInventoryItem.createdAt.isOlderThan(queueTaskExpiredThreshold) - - if (isItemTooOld) { - tasksToDelete.add(taskInventoryItem) - } - } - - logger.debug("deleting ${tasksToDelete.count()} tasks. \n Tasks: $tasksToDelete") - - tasksToDelete.forEach { - // Because the queue tasks we are deleting are not the start of a group, if deleting a task is not successful, we can ignore that - // because it doesn't negatively effect the state of the SDK or the queue. - this.delete(it.taskPersistedId) - } - - return tasksToDelete.toList() - } - - private fun update(queueTask: QueueTask): Boolean { - val fileContents = jsonAdapter.toJson(queueTask) - return fileStorage.save(FileType.QueueTask(queueTask.storageId), fileContents) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/queue/taskdata/DeletePushNotificationQueueTaskData.kt b/sdk/src/main/java/io/customer/sdk/queue/taskdata/DeletePushNotificationQueueTaskData.kt deleted file mode 100644 index 26dadf84d..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/taskdata/DeletePushNotificationQueueTaskData.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.customer.sdk.queue.taskdata - -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -internal data class DeletePushNotificationQueueTaskData( - val profileIdentified: String, - val deviceToken: String -) diff --git a/sdk/src/main/java/io/customer/sdk/queue/taskdata/IdentifyProfileQueueTaskData.kt b/sdk/src/main/java/io/customer/sdk/queue/taskdata/IdentifyProfileQueueTaskData.kt deleted file mode 100644 index f071fe0fd..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/taskdata/IdentifyProfileQueueTaskData.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.customer.sdk.queue.taskdata - -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class IdentifyProfileQueueTaskData( - val identifier: String, - val attributes: Map -) diff --git a/sdk/src/main/java/io/customer/sdk/queue/taskdata/RegisterPushNotificationQueueTaskData.kt b/sdk/src/main/java/io/customer/sdk/queue/taskdata/RegisterPushNotificationQueueTaskData.kt deleted file mode 100644 index ec240fc31..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/taskdata/RegisterPushNotificationQueueTaskData.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.customer.sdk.queue.taskdata - -import com.squareup.moshi.JsonClass -import io.customer.sdk.data.request.Device -import java.util.* - -@JsonClass(generateAdapter = true) -internal data class RegisterPushNotificationQueueTaskData( - val profileIdentified: String, - val device: Device -) diff --git a/sdk/src/main/java/io/customer/sdk/queue/taskdata/TrackEventQueueTaskData.kt b/sdk/src/main/java/io/customer/sdk/queue/taskdata/TrackEventQueueTaskData.kt deleted file mode 100644 index 1c3d73aaa..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/taskdata/TrackEventQueueTaskData.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.customer.sdk.queue.taskdata - -import com.squareup.moshi.JsonClass -import io.customer.sdk.data.request.Event - -@JsonClass(generateAdapter = true) -internal data class TrackEventQueueTaskData( - val identifier: String, - val event: Event -) diff --git a/sdk/src/main/java/io/customer/sdk/queue/type/QueueInventory.kt b/sdk/src/main/java/io/customer/sdk/queue/type/QueueInventory.kt deleted file mode 100644 index 5a9dafda1..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/type/QueueInventory.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.customer.sdk.queue.type - -typealias QueueInventory = List diff --git a/sdk/src/main/java/io/customer/sdk/queue/type/QueueModifyResult.kt b/sdk/src/main/java/io/customer/sdk/queue/type/QueueModifyResult.kt deleted file mode 100644 index fa811d9a9..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/type/QueueModifyResult.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.customer.sdk.queue.type - -// / After performing a modification task (create, update, delete) on the queue, result of the call. -data class QueueModifyResult( - val success: Boolean, // was the modification operation successful? - val queueStatus: QueueStatus -) diff --git a/sdk/src/main/java/io/customer/sdk/queue/type/QueueRunTaskResult.kt b/sdk/src/main/java/io/customer/sdk/queue/type/QueueRunTaskResult.kt deleted file mode 100644 index b8d1c97c8..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/type/QueueRunTaskResult.kt +++ /dev/null @@ -1,4 +0,0 @@ -package io.customer.sdk.queue.type - -// We only care about if the task ran successfully or not so Unit was chosen. -typealias QueueRunTaskResult = Result diff --git a/sdk/src/main/java/io/customer/sdk/queue/type/QueueStatus.kt b/sdk/src/main/java/io/customer/sdk/queue/type/QueueStatus.kt deleted file mode 100644 index ea4969848..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/type/QueueStatus.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.customer.sdk.queue.type - -data class QueueStatus( - val siteId: String, - val numTasksInQueue: Int -) diff --git a/sdk/src/main/java/io/customer/sdk/queue/type/QueueTask.kt b/sdk/src/main/java/io/customer/sdk/queue/type/QueueTask.kt deleted file mode 100644 index 8d99aa4b9..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/type/QueueTask.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.customer.sdk.queue.type - -import com.squareup.moshi.JsonClass -import io.customer.sdk.extensions.random - -@JsonClass(generateAdapter = true) -data class QueueTask( - val storageId: String, - val type: String, - val data: String, - val runResults: QueueTaskRunResults -) { - companion object { - val random: QueueTask - get() = QueueTask(String.random, String.random, String.random, QueueTaskRunResults(0)) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskGroup.kt b/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskGroup.kt deleted file mode 100644 index 3b3bbfa1c..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskGroup.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.customer.sdk.queue.type - -/** - * All of the different groups that are possible in the queue. - * - * Not sure what queue groups are? See the `BACKGROUND_QUEUE.md` doc. - */ -sealed class QueueTaskGroup { - /** - * Be sure that the string is unique to each subclass of [QueueTaskGroup]. - * The returned string will identify each group in the background queue. Therefore, it's important to use properties from the subclass constructor in the returned string to differentiate groups of the same type. - */ - data class IdentifyProfile(val identifier: String) : QueueTaskGroup() { - override fun toString(): String = "identified_profile_$identifier" - } - data class RegisterPushToken(val token: String) : QueueTaskGroup() { - override fun toString(): String = "registered_push_token_$token" - } -} diff --git a/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskMetadata.kt b/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskMetadata.kt deleted file mode 100644 index 545820332..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskMetadata.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.customer.sdk.queue.type - -import com.squareup.moshi.JsonClass -import io.customer.base.extenstions.unixTimeToDate -import io.customer.sdk.extensions.random -import java.util.* - -// / Pointer to full queue task in persistent storage. -// / This data structure is meant to be as small as possible with the -// / ability to hold all queue task metadata in memory at runtime. -@JsonClass(generateAdapter = true) -data class QueueTaskMetadata( - val taskPersistedId: String, - val taskType: String, - // The start of a new group of tasks. - // Tasks can be the start of of 0 or 1 groups - val groupStart: String?, - // Groups that this task belongs to. - // Tasks can belong to 0+ groups - val groupMember: List?, - // Populated when the task is added to the queue. - val createdAt: Date -) { - companion object { - val random: QueueTaskMetadata - get() = QueueTaskMetadata(String.random, String.random, null, null, 1644600699L.unixTimeToDate()) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskRunResults.kt b/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskRunResults.kt deleted file mode 100644 index ff53f20aa..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskRunResults.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.customer.sdk.queue.type - -import com.squareup.moshi.JsonClass - -/** - * Metadata about a task in the background queue and it's execution history in the queue. Used, for example, to see how many times a task has failed running in the queue. - */ -@JsonClass(generateAdapter = true) -data class QueueTaskRunResults( - val totalRuns: Int -) diff --git a/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskType.kt b/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskType.kt deleted file mode 100644 index 6a73537b9..000000000 --- a/sdk/src/main/java/io/customer/sdk/queue/type/QueueTaskType.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.customer.sdk.queue.type - -// All the types of tasks the Tracking module runs in the background queue -internal enum class QueueTaskType { - IdentifyProfile, - TrackEvent, - RegisterDeviceToken, - DeletePushToken, - TrackPushMetric, - TrackDeliveryEvent -} diff --git a/sdk/src/main/java/io/customer/sdk/repository/CleanupRepository.kt b/sdk/src/main/java/io/customer/sdk/repository/CleanupRepository.kt deleted file mode 100644 index cefb831a5..000000000 --- a/sdk/src/main/java/io/customer/sdk/repository/CleanupRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.customer.sdk.repository - -import io.customer.sdk.queue.Queue - -/** - * In charge of cleaning up the SDK. Deleting old data or caches. Keeping the SDK fast and performant. - */ -internal interface CleanupRepository { - suspend fun cleanup() -} - -internal class CleanupRepositoryImpl( - private val queue: Queue -) : CleanupRepository { - - override suspend fun cleanup() { - queue.deleteExpiredTasks() - } -} diff --git a/sdk/src/main/java/io/customer/sdk/repository/DeviceRepository.kt b/sdk/src/main/java/io/customer/sdk/repository/DeviceRepository.kt deleted file mode 100644 index fc5e26818..000000000 --- a/sdk/src/main/java/io/customer/sdk/repository/DeviceRepository.kt +++ /dev/null @@ -1,105 +0,0 @@ -package io.customer.sdk.repository - -import io.customer.sdk.CustomerIOConfig -import io.customer.sdk.data.model.CustomAttributes -import io.customer.sdk.data.request.Device -import io.customer.sdk.data.store.DeviceStore -import io.customer.sdk.queue.Queue -import io.customer.sdk.repository.preference.SitePreferenceRepository -import io.customer.sdk.util.DateUtil -import io.customer.sdk.util.Logger - -interface DeviceRepository { - fun registerDeviceToken(deviceToken: String, attributes: CustomAttributes) - fun deleteDeviceToken() - fun addCustomDeviceAttributes(attributes: CustomAttributes) - fun getDeviceToken(): String? -} - -internal class DeviceRepositoryImpl( - private val config: CustomerIOConfig, - private val deviceStore: DeviceStore, - private val sitePreferenceRepository: SitePreferenceRepository, - private val backgroundQueue: Queue, - private val dateUtil: DateUtil, - private val logger: Logger -) : DeviceRepository { - - override fun registerDeviceToken(deviceToken: String, attributes: CustomAttributes) { - if (deviceToken.isBlank()) { - logger.debug("device token cannot be blank. ignoring request to register device token") - return - } - - val attributes = createDeviceAttributes(attributes) - - logger.info("registering device token $deviceToken, attributes: $attributes") - - // persist the device token for use later on such as automatically registering device token with a profile - // that gets identified later on. - logger.debug("storing device token to device storage $deviceToken") - sitePreferenceRepository.saveDeviceToken(deviceToken) - - val identifiedProfileId = sitePreferenceRepository.getIdentifier() - if (identifiedProfileId.isNullOrBlank()) { - logger.info("no profile identified, so not registering device token to a profile") - return - } - - val device = Device( - token = deviceToken, - lastUsed = dateUtil.now, - attributes = attributes - ) - - // if task doesn't successfully get added to the queue, it does not break the SDK's state. So, we can ignore the result of adding task to queue. - backgroundQueue.queueRegisterDevice(identifiedProfileId, device) - } - - override fun addCustomDeviceAttributes(attributes: CustomAttributes) { - logger.debug("adding custom device attributes request made") - - val existingDeviceToken = sitePreferenceRepository.getDeviceToken() - - if (existingDeviceToken == null) { - logger.debug("no device token yet registered. ignoring request to add custom device attributes") - return - } - - registerDeviceToken(existingDeviceToken, attributes) - } - - override fun getDeviceToken(): String? { - return sitePreferenceRepository.getDeviceToken() - } - - private fun createDeviceAttributes(customAddedAttributes: CustomAttributes): Map { - if (!config.autoTrackDeviceAttributes) return customAddedAttributes - - val defaultAttributes = deviceStore.buildDeviceAttributes() - - return defaultAttributes + customAddedAttributes // order matters! allow customer to override default values if they wish. - } - - override fun deleteDeviceToken() { - logger.info("deleting device token request made") - - val existingDeviceToken = sitePreferenceRepository.getDeviceToken() - if (existingDeviceToken == null) { - logger.info("no device token exists so ignoring request to delete") - return - } - - // Do not delete push token from device storage. The token is valid - // once given to SDK. We need it for future profile identifications. - - val identifiedProfileId = sitePreferenceRepository.getIdentifier() - if (identifiedProfileId == null) { - logger.info("no profile identified so not removing device token from profile") - return - } - - // if task doesn't successfully get added to the queue, it does not break the SDK's state. So, we can ignore the result of adding task to queue. - backgroundQueue.queueDeletePushToken(identifiedProfileId, existingDeviceToken) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/repository/ProfileRepository.kt b/sdk/src/main/java/io/customer/sdk/repository/ProfileRepository.kt deleted file mode 100644 index 50d1f8ae9..000000000 --- a/sdk/src/main/java/io/customer/sdk/repository/ProfileRepository.kt +++ /dev/null @@ -1,119 +0,0 @@ -package io.customer.sdk.repository - -import io.customer.sdk.data.model.CustomAttributes -import io.customer.sdk.hooks.HooksManager -import io.customer.sdk.hooks.ModuleHook -import io.customer.sdk.queue.Queue -import io.customer.sdk.repository.preference.SitePreferenceRepository -import io.customer.sdk.util.Logger - -interface ProfileRepository { - fun identify(identifier: String, attributes: CustomAttributes) - fun clearIdentify() - fun addCustomProfileAttributes(attributes: CustomAttributes) -} - -internal class ProfileRepositoryImpl( - private val deviceRepository: DeviceRepository, - private val sitePreferenceRepository: SitePreferenceRepository, - private val backgroundQueue: Queue, - private val logger: Logger, - private val hooksManager: HooksManager -) : ProfileRepository { - - override fun identify(identifier: String, attributes: CustomAttributes) { - logger.info("identify profile $identifier") - logger.debug("identify profile $identifier, $attributes") - - if (identifier.isBlank()) { - logger.debug("Profile cannot be identified: Identifier is blank. Please retry with a valid, non-empty identifier.") - return - } - - val currentlyIdentifiedProfileIdentifier = sitePreferenceRepository.getIdentifier() - // The SDK calls identify() with the already identified profile for changing profile attributes. - val isChangingIdentifiedProfile = - currentlyIdentifiedProfileIdentifier != null && currentlyIdentifiedProfileIdentifier != identifier - val isFirstTimeIdentifying = currentlyIdentifiedProfileIdentifier == null - - currentlyIdentifiedProfileIdentifier?.let { currentlyIdentifiedProfileIdentifier -> - if (isChangingIdentifiedProfile) { - logger.info("changing profile from id $currentlyIdentifiedProfileIdentifier to $identifier") - - logger.debug("deleting device token before identifying new profile") - deviceRepository.deleteDeviceToken() - } - } - - val queueStatus = backgroundQueue.queueIdentifyProfile( - identifier, - currentlyIdentifiedProfileIdentifier, - attributes - ) - - // Don't modify the state of the SDK's data until we confirm we added a queue task successfully. This could put the Customer.io API - // out-of-sync with the SDK's state and cause many future HTTP errors. - // Therefore, if adding the task to the queue failed, ignore the request and fail early. - if (!queueStatus.success) { - logger.debug("failed to add identify task to queue") - return - } - - logger.debug("storing identifier on device storage $identifier") - sitePreferenceRepository.saveIdentifier(identifier) - - hooksManager.onHookUpdate( - hook = ModuleHook.ProfileIdentifiedHook(identifier) - ) - - if (isFirstTimeIdentifying || isChangingIdentifiedProfile) { - logger.debug("first time identified or changing identified profile") - - sitePreferenceRepository.getDeviceToken()?.let { - logger.debug("automatically registering device token to newly identified profile") - deviceRepository.registerDeviceToken( - it, - emptyMap() - ) // no new attributes but default ones to pass so pass empty. - } - } - } - - override fun addCustomProfileAttributes(attributes: CustomAttributes) { - logger.debug("adding profile attributes request made") - - val currentlyIdentifiedProfileId = sitePreferenceRepository.getIdentifier() - - if (currentlyIdentifiedProfileId == null) { - logger.debug("no profile is currently identified. ignoring request to add attributes to a profile") - return - } - - identify(currentlyIdentifiedProfileId, attributes) - } - - override fun clearIdentify() { - logger.debug("clearing identified profile request made") - - val currentlyIdentifiedProfileId = sitePreferenceRepository.getIdentifier() - - if (currentlyIdentifiedProfileId == null) { - logger.info("no profile is currently identified. ignoring request to clear identified profile") - return - } - - // notify hooks about identifier being cleared - hooksManager.onHookUpdate( - ModuleHook.BeforeProfileStoppedBeingIdentified( - currentlyIdentifiedProfileId - ) - ) - - // delete token from profile to prevent sending the profile pushes when they are not identified in the SDK. - deviceRepository.deleteDeviceToken() - - // delete identified from device storage to not associate future SDK calls to this profile - logger.debug("clearing profile from device storage") - sitePreferenceRepository.removeIdentifier(currentlyIdentifiedProfileId) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/repository/TrackRepository.kt b/sdk/src/main/java/io/customer/sdk/repository/TrackRepository.kt deleted file mode 100644 index 09f91f9b8..000000000 --- a/sdk/src/main/java/io/customer/sdk/repository/TrackRepository.kt +++ /dev/null @@ -1,89 +0,0 @@ -package io.customer.sdk.repository - -import io.customer.sdk.data.model.CustomAttributes -import io.customer.sdk.data.model.EventType -import io.customer.sdk.data.request.MetricEvent -import io.customer.sdk.hooks.HooksManager -import io.customer.sdk.hooks.ModuleHook -import io.customer.sdk.queue.Queue -import io.customer.sdk.repository.preference.SitePreferenceRepository -import io.customer.sdk.util.Logger - -interface TrackRepository { - fun track(name: String, attributes: CustomAttributes) - fun trackMetric(deliveryID: String, event: MetricEvent, deviceToken: String) - fun trackInAppMetric( - deliveryID: String, - event: MetricEvent, - metadata: Map = emptyMap() - ) - fun screen(name: String, attributes: CustomAttributes) -} - -internal class TrackRepositoryImpl( - private val sitePreferenceRepository: SitePreferenceRepository, - private val backgroundQueue: Queue, - private val logger: Logger, - private val hooksManager: HooksManager -) : TrackRepository { - - override fun track(name: String, attributes: CustomAttributes) { - return track(EventType.event, name, attributes) - } - - override fun screen(name: String, attributes: CustomAttributes) { - return track(EventType.screen, name, attributes) - } - - private fun track(eventType: EventType, name: String, attributes: CustomAttributes) { - val eventTypeDescription = - if (eventType == EventType.screen) "track screen view event" else "track event" - - logger.info("$eventTypeDescription $name") - logger.debug("$eventTypeDescription $name attributes: $attributes") - - val identifier = sitePreferenceRepository.getIdentifier() - if (identifier == null) { - // when we have anonymous profiles implemented in the SDK, we can decide to not - // ignore events when a profile is not logged in yet. - logger.info("ignoring $eventTypeDescription $name because no profile currently identified") - return - } - - val queueStatus = backgroundQueue.queueTrack(identifier, name, eventType, attributes) - - if (queueStatus.success && eventType == EventType.screen) { - hooksManager.onHookUpdate( - hook = ModuleHook.ScreenTrackedHook(name) - ) - } - } - - override fun trackMetric( - deliveryID: String, - event: MetricEvent, - deviceToken: String - ) { - logger.info("push metric ${event.name}") - logger.debug("delivery id $deliveryID device token $deviceToken") - - // if task doesn't successfully get added to the queue, it does not break the SDK's state. So, we can ignore the result of adding task to queue. - backgroundQueue.queueTrackMetric(deliveryID, deviceToken, event) - } - - override fun trackInAppMetric( - deliveryID: String, - event: MetricEvent, - metadata: Map - ) { - logger.info("in-app metric ${event.name}") - logger.debug("delivery id $deliveryID") - - // if task doesn't successfully get added to the queue, it does not break the SDK's state. So, we can ignore the result of adding task to queue. - backgroundQueue.queueTrackInAppMetric( - deliveryId = deliveryID, - event = event, - metadata = metadata - ) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/util/JsonAdapter.kt b/sdk/src/main/java/io/customer/sdk/util/JsonAdapter.kt deleted file mode 100644 index 980ea15f7..000000000 --- a/sdk/src/main/java/io/customer/sdk/util/JsonAdapter.kt +++ /dev/null @@ -1,109 +0,0 @@ -package io.customer.sdk.util - -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types - -/** - * Abstract way to deserialize JSON strings without thinking about the library used. - * - * This is an object instead of a class through dependency injection because when we run tests, we like to use the real instance of this class in our tests. That is usually a sign it doesn't need to be injected. - */ -class JsonAdapter internal constructor(val moshi: Moshi) { - - /** - * Note: This class must treat arrays and not arrays differently (below we call them Lists but we mean arrays because that's what we call them in Json). - * - * We are not talking about embedded arrays: - * ``` - * { - * "nested": [] - * } - * ``` - * Nesting works fine with the regular functions. - * - * We mean: - * ``` - * { - * "not_array_json": "" - * } - * ``` - * vs - * ``` - * [ - * { - * "not_array_json": "" - * } - * ] - * ``` - * - * Moshi handles arrays differently: https://github.com/square/moshi#parse-json-arrays - * - * Parses data string to single objects. Using this method directly is - * discouraged as parsing json incorrectly can lead to crashes on client - * apps. Prefer safe parsing instead by using [fromJsonOrNull]. - * - * @see [fromJsonOrNull] for safe parsing - */ - @Throws(Exception::class) - inline fun fromJson(data: String): T { - val json = data.trim() - - if (json.isNotEmpty() && json[0] == '[') throw IllegalArgumentException("String is a list. Use `fromJsonList` instead.") - - val jsonAdapter = moshi.adapter(T::class.java) - - return jsonAdapter.fromJson(json) as T - } - - /** - * Parses data string to a single objects or null if there is any exception - * e.g. malformed json, missing adapter, etc. - */ - inline fun fromJsonOrNull(json: String): T? = try { - fromJson(json) - } catch (ex: Exception) { - null - } - - /** - * Parses data string to list of objects. Using this method directly is - * discouraged as parsing json incorrectly can lead to crashes on client - * apps. Prefer safe parsing instead by using [fromJsonListOrNull]. - * - * @see [fromJsonListOrNull] for safe parsing - */ - @Throws(Exception::class) - inline fun fromJsonList(data: String): List { - val json = data.trim() - - if (json.isNotEmpty() && json[0] != '[') throw IllegalArgumentException("String is not a list. Use `fromJson` instead.") - - val type = Types.newParameterizedType(List::class.java, T::class.java) - val adapter = moshi.adapter>(type) - - return adapter.fromJson(json) as List - } - - /** - * Parses data string to list of objects or null if there is any exception - * e.g. malformed json, missing adapter, etc. - */ - inline fun fromJsonListOrNull(json: String): List? = try { - fromJsonList(json) - } catch (ex: Exception) { - null - } - - fun toJson(data: T): String { - val jsonAdapter = moshi.adapter(data::class.java) - - return jsonAdapter.toJson(data) - } - - inline fun toJson(data: List): String { - val type = Types.newParameterizedType(List::class.java, T::class.java) - val adapter = moshi.adapter>(type) - - return adapter.toJson(data) - } -} diff --git a/sdk/src/main/java/io/customer/sdk/util/Seconds.kt b/sdk/src/main/java/io/customer/sdk/util/Seconds.kt deleted file mode 100644 index 64f8a5d47..000000000 --- a/sdk/src/main/java/io/customer/sdk/util/Seconds.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.customer.sdk.util - -/** - * It's error-prone when time units are the same data type. Seconds and milliseconds are expressed with [Long]. You need to be cautious that when using [Long] to express both data types. Your code needs to keep track of what unit you're using at a given time and converting between the two when needed. A good integration test suite would also be needed to make sure conversion each step of the way is done correctly. - * - * By creating separate data types for each time unit, this issue is no longer a problem. - */ -data class Seconds( - val value: Double -) { - companion object { - fun fromDays(numDays: Int): Seconds { - val secondsIn24Hours = 86400.0 - - return Seconds(numDays * secondsIn24Hours) - } - } - - val toMilliseconds: Milliseconds - get() = Milliseconds((value * 1000).toLong()) - - override fun toString(): String = "$value seconds" -} -fun Double.toSeconds(): Seconds = Seconds(this) - -data class Milliseconds( - val value: Long -) { - val toSeconds: Seconds - get() = Seconds(value.toDouble() / 1000) - - override fun toString(): String = "$value millis" -} diff --git a/sdk/src/main/java/io/customer/sdk/util/SimpleTimer.kt b/sdk/src/main/java/io/customer/sdk/util/SimpleTimer.kt deleted file mode 100644 index ffdca463b..000000000 --- a/sdk/src/main/java/io/customer/sdk/util/SimpleTimer.kt +++ /dev/null @@ -1,104 +0,0 @@ -package io.customer.sdk.util - -import android.os.CountDownTimer -import io.customer.sdk.extensions.random -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch - -/** - * Wrapper around an OS timer that gives us the ability to mock timers in tests to make them run faster. - */ -interface SimpleTimer { - // after block is called, timer is reset to be ready to use again - fun scheduleAndCancelPrevious(seconds: Seconds, block: () -> Unit) - - // after block is called, timer is reset to be ready to use again - fun scheduleIfNotAlready(seconds: Seconds, block: () -> Unit): Boolean - fun cancel() -} - -internal class AndroidSimpleTimer( - private val logger: Logger, - private val dispatchersProvider: DispatchersProvider -) : SimpleTimer { - - @Volatile private var countdownTimer: CountDownTimer? = null - - @Volatile private var startTimerMainThreadJob: Job? = null - - @Volatile private var timerAlreadyScheduled = false - private val instanceIdentifier = String.random - - override fun scheduleAndCancelPrevious(seconds: Seconds, block: () -> Unit) { - // Must create and start timer on the main UI thread or Android will throw an exception saying the current thread doesn't have a Looper. - // Because we are starting a new coroutine, there is a chance that there could be a delay in starting the timer. This is OK because - // this function is designed to be async anyway so the logic from the caller has not changed. - startTimerMainThreadJob = CoroutineScope(dispatchersProvider.main).launch { - val newTimer: CountDownTimer = synchronized(this) { - timerAlreadyScheduled = true - unsafeCancel() - - log("making a timer for $seconds") - - object : CountDownTimer(seconds.toMilliseconds.value, 1) { - override fun onTick(millisUntilFinished: Long) {} - override fun onFinish() { - timerDone() // reset timer before calling block as block might be synchronous and if it tries to start a new timer, it will not succeed because we need to reset the timer. - block() - } - } - } - - this@AndroidSimpleTimer.countdownTimer = newTimer - - newTimer.start() - } - } - - override fun scheduleIfNotAlready(seconds: Seconds, block: () -> Unit): Boolean { - synchronized(this) { - if (timerAlreadyScheduled) { - log("already scheduled to run. Skipping request.") - return false - } - - scheduleAndCancelPrevious(seconds, block) - - return true - } - } - - private fun timerDone() { - synchronized(this) { - timerAlreadyScheduled = false - - log("timer is done! It's been reset") - } - } - - override fun cancel() { - synchronized(this) { - log("timer is being cancelled") - unsafeCancel() - - timerAlreadyScheduled = false - } - } - - // cancel without having a mutex lock. Call within a synchronized{} block - private fun unsafeCancel() { - try { - startTimerMainThreadJob?.cancel() - } catch (e: Throwable) { - // cancel() throws an exception. We want to cancel so ignore the error thrown. - } - - countdownTimer?.cancel() - countdownTimer = null - } - - private fun log(message: String) { - logger.debug("Timer $instanceIdentifier $message") - } -}