Skip to content

Commit

Permalink
feat: added shared instance for independent components
Browse files Browse the repository at this point in the history
  • Loading branch information
mrehan27 committed Oct 3, 2022
1 parent b93c2dc commit 70fa8cd
Show file tree
Hide file tree
Showing 13 changed files with 267 additions and 24 deletions.
6 changes: 5 additions & 1 deletion common-test/src/main/java/io/customer/commontest/BaseTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.customer.sdk.data.model.Region
import io.customer.sdk.data.store.Client
import io.customer.sdk.data.store.DeviceStore
import io.customer.sdk.di.CustomerIOComponent
import io.customer.sdk.di.CustomerIOSharedComponent
import io.customer.sdk.module.CustomerIOModuleConfig
import io.customer.sdk.util.*
import okhttp3.ResponseBody.Companion.toResponseBody
Expand Down Expand Up @@ -38,6 +39,7 @@ abstract class BaseTest {
protected lateinit var deviceStore: DeviceStore
protected lateinit var dispatchersProviderStub: DispatchersProviderStub

protected lateinit var sharedDI: CustomerIOSharedComponent
protected lateinit var di: CustomerIOComponent
protected val jsonAdapter: JsonAdapter
get() = di.jsonAdapter
Expand Down Expand Up @@ -96,7 +98,9 @@ abstract class BaseTest {
throw RuntimeException("server didnt' start ${cioConfig.trackingApiUrl}")
}

sharedDI = CustomerIOSharedComponent()
di = CustomerIOComponent(
sharedComponent = sharedDI,
sdkConfig = cioConfig,
context = application
)
Expand All @@ -108,7 +112,7 @@ abstract class BaseTest {
}
deviceStore = DeviceStoreStub().getDeviceStore(cioConfig)
dispatchersProviderStub = DispatchersProviderStub().also {
di.overrideDependency(DispatchersProvider::class.java, it)
sharedDI.overrideDependency(DispatchersProvider::class.java, it)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOShared

@DrawableRes
internal fun Context.getDrawableByName(name: String?): Int? = if (name.isNullOrBlank()) null
Expand All @@ -20,6 +20,6 @@ else resources?.getIdentifier(name, "drawable", packageName)?.takeUnless { id ->
internal fun Context.getColorOrNull(@ColorRes id: Int): Int? = try {
ContextCompat.getColor(this, id)
} catch (ex: Resources.NotFoundException) {
CustomerIO.instance().diGraph.logger.error("Invalid resource $id, ${ex.message}")
CustomerIOShared.instance().diGraph.logger.error("Invalid resource $id, ${ex.message}")
null
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package io.customer.messagingpush.extensions

import android.graphics.Color
import androidx.annotation.DrawableRes
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOShared

@DrawableRes
internal fun String.toColorOrNull(): Int? = try {
Color.parseColor(this)
} catch (ex: IllegalArgumentException) {
CustomerIO.instance().diGraph.logger.error("Invalid color string $this, ${ex.message}")
CustomerIOShared.instance().diGraph.logger.error("Invalid color string $this, ${ex.message}")
null
}
21 changes: 20 additions & 1 deletion sdk/api/sdk.api
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ public abstract interface class io/customer/sdk/CustomerIOInstance {
public abstract fun trackMetric (Ljava/lang/String;Lio/customer/sdk/data/request/MetricEvent;Ljava/lang/String;)V
}

public final class io/customer/sdk/CustomerIOShared {
public static final field Companion Lio/customer/sdk/CustomerIOShared$Companion;
public synthetic fun <init> (Lio/customer/sdk/di/CustomerIOSharedComponent;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun attachSDKConfig (Lio/customer/sdk/CustomerIOConfig;)V
public final fun getDiGraph ()Lio/customer/sdk/di/CustomerIOSharedComponent;
public static final fun instance ()Lio/customer/sdk/CustomerIOShared;
}

public final class io/customer/sdk/CustomerIOShared$Companion {
public final fun instance ()Lio/customer/sdk/CustomerIOShared;
}

public final class io/customer/sdk/data/model/EventType : java/lang/Enum {
public static final field event Lio/customer/sdk/data/model/EventType;
public static final field screen Lio/customer/sdk/data/model/EventType;
Expand Down Expand Up @@ -175,7 +187,7 @@ public abstract interface class io/customer/sdk/device/DeviceTokenProvider {
}

public final class io/customer/sdk/di/CustomerIOComponent : io/customer/sdk/di/DiGraph {
public fun <init> (Landroid/content/Context;Lio/customer/sdk/CustomerIOConfig;)V
public fun <init> (Lio/customer/sdk/di/CustomerIOSharedComponent;Landroid/content/Context;Lio/customer/sdk/CustomerIOConfig;)V
public final fun buildRetrofit (Ljava/lang/String;J)Lretrofit2/Retrofit;
public final fun buildStore ()Lio/customer/sdk/data/store/CustomerIOStore;
public final fun getActivityLifecycleCallbacks ()Lio/customer/sdk/CustomerIOActivityLifecycleCallbacks;
Expand All @@ -201,6 +213,13 @@ public final class io/customer/sdk/di/CustomerIOComponent : io/customer/sdk/di/D
public final fun getTrackRepository ()Lio/customer/sdk/repository/TrackRepository;
}

public final class io/customer/sdk/di/CustomerIOSharedComponent : io/customer/sdk/di/DiGraph {
public fun <init> ()V
public final fun getDispatchersProvider ()Lio/customer/sdk/util/DispatchersProvider;
public final fun getLogger ()Lio/customer/sdk/util/Logger;
public final fun getStaticSettingsProvider ()Lio/customer/sdk/util/StaticSettingsProvider;
}

public abstract class io/customer/sdk/di/DiGraph {
public fun <init> ()V
public final fun getOverrides ()Ljava/util/Map;
Expand Down
9 changes: 9 additions & 0 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ android {
}
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
freeCompilerArgs += [
'-Xopt-in=kotlin.RequiresOptIn',
'-Xopt-in=io.customer.base.internal.InternalCustomerIOApi',
]
}
}

dependencies {
api project(":base")

Expand Down
11 changes: 10 additions & 1 deletion sdk/src/main/java/io/customer/sdk/CustomerIO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ class CustomerIO internal constructor(
fun instanceOrNull(): CustomerIO? = try {
instance()
} catch (ex: Exception) {
CustomerIOShared.instance().diGraph.logger.error(
"Customer.io instance not initialized: ${ex.message}"
)
null
}

Expand All @@ -123,6 +126,7 @@ class CustomerIO internal constructor(
private var region: Region = Region.US,
private val appContext: Application
) {
private val sharedInstance = CustomerIOShared.instance()
private var client: Client = Client.Android(Version.version)
private var timeout = 6000L
private var shouldAutoRecordScreenViews: Boolean = false
Expand Down Expand Up @@ -226,7 +230,12 @@ class CustomerIO internal constructor(
configurations = modules.entries.associate { entry -> entry.key to entry.value.moduleConfig }
)

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

Expand Down
54 changes: 54 additions & 0 deletions sdk/src/main/java/io/customer/sdk/CustomerIOShared.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.customer.sdk

import androidx.annotation.VisibleForTesting
import io.customer.base.internal.InternalCustomerIOApi
import io.customer.sdk.CustomerIOShared.Companion.instance
import io.customer.sdk.di.CustomerIOSharedComponent
import io.customer.sdk.util.LogcatLogger

/**
* Singleton static instance of Customer.io SDK that is initialized exactly when
* [instance] is called the first time. The class should be lightweight and only
* be used to hold code that might be required before initializing the SDK.
* <p/>
* Some use cases of the class may include:
* - access selected SDK methods even when SDK is not initialized
* - contains code that cannot guarantee SDK initialization
* - notify user and prevent unwanted SDK crashes in case of late initialization
* - hold callbacks/values that might be needed post-initialization of the SDK
* - reduce challenges of communication when wrapping the SDK for non native
* platforms
*
* @property diGraph instance of DI graph to satisfy dependencies
*/
class CustomerIOShared private constructor(
val diGraph: CustomerIOSharedComponent
) {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
fun attachSDKConfig(sdkConfig: CustomerIOConfig) {
(diGraph.logger as? LogcatLogger)?.setPreferredLogLevel(logLevel = sdkConfig.logLevel)
}

companion object {
private var INSTANCE: CustomerIOShared? = null

@JvmStatic
@OptIn(InternalCustomerIOApi::class)
fun instance(): CustomerIOShared = createInstance(diGraph = null)

@Synchronized
@InternalCustomerIOApi
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
fun createInstance(
diGraph: CustomerIOSharedComponent? = null
): CustomerIOShared = INSTANCE ?: CustomerIOShared(
diGraph = diGraph ?: CustomerIOSharedComponent()
).apply { INSTANCE = this }

@InternalCustomerIOApi
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
fun clearInstance() {
INSTANCE = null
}
}
}
15 changes: 5 additions & 10 deletions sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@ package io.customer.sdk.di

import android.content.Context
import com.squareup.moshi.Moshi
import io.customer.sdk.BuildConfig
import io.customer.sdk.CustomerIOActivityLifecycleCallbacks
import io.customer.sdk.CustomerIOConfig
import io.customer.sdk.Version
import io.customer.sdk.api.CustomerIOApiRetryPolicy
import io.customer.sdk.api.HttpRequestRunner
import io.customer.sdk.api.HttpRequestRunnerImpl
import io.customer.sdk.api.HttpRetryPolicy
import io.customer.sdk.api.RetrofitTrackingHttpClient
import io.customer.sdk.api.TrackingHttpClient
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
Expand All @@ -32,6 +26,7 @@ import java.util.concurrent.TimeUnit
* Configuration class to configure/initialize low-level operations and objects.
*/
class CustomerIOComponent(
private val sharedComponent: CustomerIOSharedComponent,
val context: Context,
val sdkConfig: CustomerIOConfig
) : DiGraph() {
Expand All @@ -49,7 +44,7 @@ class CustomerIOComponent(
get() = override() ?: QueueRunnerImpl(jsonAdapter, cioHttpClient, logger)

val dispatchersProvider: DispatchersProvider
get() = override() ?: SdkDispatchers()
get() = override() ?: sharedComponent.dispatchersProvider

val queue: Queue
get() = override() ?: getSingletonInstanceCreate {
Expand All @@ -71,7 +66,7 @@ class CustomerIOComponent(
)

val logger: Logger
get() = override() ?: LogcatLogger(sdkConfig)
get() = override() ?: sharedComponent.logger

val hooksManager: HooksManager
get() = override() ?: getSingletonInstanceCreate { CioHooksManager() }
Expand Down Expand Up @@ -158,7 +153,7 @@ class CustomerIOComponent(

private val httpLoggingInterceptor by lazy {
override() ?: HttpLoggingInterceptor().apply {
if (BuildConfig.DEBUG) {
if (sharedComponent.staticSettingsProvider.isDebuggable) {
level = HttpLoggingInterceptor.Level.BODY
}
}
Expand Down
25 changes: 25 additions & 0 deletions sdk/src/main/java/io/customer/sdk/di/CustomerIOSharedComponent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.customer.sdk.di

import io.customer.sdk.util.*

/**
* Static/shared component dependency graph to satisfy independent dependencies
* from single place. All other graphs should never redefine dependencies defined
* here unless extremely necessary.
* <p/>
* The class should only contain dependencies matching the following criteria:
* - dependencies that may be required without SDK initialization
* - dependencies that are lightweight and are not dependent on SDK initialization
*/
@Suppress("MemberVisibilityCanBePrivate")
class CustomerIOSharedComponent : DiGraph() {
val staticSettingsProvider: StaticSettingsProvider by lazy {
override() ?: StaticSettingsProviderImpl()
}

val logger: Logger by lazy {
override() ?: LogcatLogger(staticSettingsProvider = staticSettingsProvider)
}

val dispatchersProvider: DispatchersProvider by lazy { override() ?: SdkDispatchers() }
}
32 changes: 25 additions & 7 deletions sdk/src/main/java/io/customer/sdk/util/Logger.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.customer.sdk.util

import android.util.Log
import io.customer.sdk.CustomerIOConfig
import androidx.annotation.VisibleForTesting

interface Logger {
fun info(message: String)
Expand All @@ -26,32 +26,50 @@ enum class CioLogLevel {
}

internal class LogcatLogger(
private val sdkConfig: CustomerIOConfig
private val staticSettingsProvider: StaticSettingsProvider
) : Logger {
// Log level defined by user in configurations
private var preferredLogLevel: CioLogLevel? = null

private val tag = "[CIO]"
// Fallback log level to be used only if log level is not yet defined by the user
private val fallbackLogLevel
get() = if (staticSettingsProvider.isDebuggable) CioLogLevel.DEBUG
else CioLogLevel.ERROR

// Prefer user log level; fallback to default only till the user defined value is not received
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val logLevel: CioLogLevel
get() = preferredLogLevel ?: fallbackLogLevel

fun setPreferredLogLevel(logLevel: CioLogLevel) {
preferredLogLevel = logLevel
}

override fun info(message: String) {
runIfMeetsLogLevelCriteria(CioLogLevel.INFO) {
Log.i(tag, message)
Log.i(TAG, message)
}
}

override fun debug(message: String) {
runIfMeetsLogLevelCriteria(CioLogLevel.DEBUG) {
Log.d(tag, message)
Log.d(TAG, message)
}
}

override fun error(message: String) {
runIfMeetsLogLevelCriteria(CioLogLevel.ERROR) {
Log.e(tag, message)
Log.e(TAG, message)
}
}

private fun runIfMeetsLogLevelCriteria(levelForMessage: CioLogLevel, block: () -> Unit) {
val shouldLog = sdkConfig.logLevel.shouldLog(levelForMessage)
val shouldLog = logLevel.shouldLog(levelForMessage)

if (shouldLog) block()
}

companion object {
const val TAG = "[CIO]"
}
}
18 changes: 18 additions & 0 deletions sdk/src/main/java/io/customer/sdk/util/StaticSettingsProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.customer.sdk.util

import io.customer.sdk.BuildConfig

/**
* Wrapper class to hold static/only one time defined properties from
* [BuildConfig] and other Android classes to achieve the following:
* - making it easier to test classes relying on these properties
* - create abstraction and reduce dependency from native Android properties;
* can be helpful in SDK wrappers
*/
interface StaticSettingsProvider {
val isDebuggable: Boolean
}

class StaticSettingsProviderImpl : StaticSettingsProvider {
override val isDebuggable: Boolean = BuildConfig.DEBUG
}
Loading

0 comments on commit 70fa8cd

Please sign in to comment.