Skip to content

Commit

Permalink
feat: support for wrapper SDKs (#442)
Browse files Browse the repository at this point in the history
Co-authored-by: Shahroz Khan <[email protected]>
  • Loading branch information
mrehan27 and Shahroz16 authored Oct 16, 2024
1 parent ce6edf6 commit 5c70469
Show file tree
Hide file tree
Showing 34 changed files with 485 additions and 181 deletions.
15 changes: 14 additions & 1 deletion common-test/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
<application>
<!--
Meta-data tags added here to test challenges that may arise when merging multiple
AndroidManifest.xml files with same meta-data tags.
See AndroidManifest.xml in androidTest directory of core module for more details.
-->
<meta-data
android:name="io.customer.sdk.android.core.SDK_SOURCE"
android:value="CommonTestAndroidSDK" />
<meta-data
android:name="io.customer.sdk.android.core.SDK_VERSION"
android:value="0.1.0" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.customer.commontest.config

import android.app.Application
import io.customer.sdk.data.store.Client

/**
* Base interface for all test arguments.
Expand All @@ -17,10 +16,3 @@ interface TestArgument
data class ApplicationArgument(
val value: Application
) : TestArgument

/**
* Argument for passing client instance to test configuration.
*/
data class ClientArgument(
val value: Client = Client.Android(sdkVersion = "3.0.0")
) : TestArgument
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package io.customer.commontest.core

import io.customer.commontest.config.ApplicationArgument
import io.customer.commontest.config.ClientArgument
import io.customer.commontest.config.TestConfig
import io.customer.commontest.config.argumentOrNull
import io.customer.commontest.config.configureAndroidSDKComponent
import io.customer.commontest.config.configureSDKComponent
import io.customer.commontest.config.testConfigurationDefault
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.core.di.registerAndroidSDKComponent
import io.customer.sdk.core.di.setupAndroidComponent
import io.mockk.clearAllMocks

/**
Expand Down Expand Up @@ -43,8 +42,7 @@ abstract class BaseTest {
*/
private fun registerAndroidSDKComponent(testConfig: TestConfig) {
val application = testConfig.argumentOrNull<ApplicationArgument>()?.value ?: return
val client = testConfig.argumentOrNull<ClientArgument>()?.value ?: return

testConfig.configureAndroidSDKComponent(SDKComponent.registerAndroidSDKComponent(application, client))
testConfig.configureAndroidSDKComponent(SDKComponent.setupAndroidComponent(application))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import io.customer.sdk.core.util.Logger
class UnitTestLogger : Logger {
override var logLevel: CioLogLevel = CioLogLevel.DEBUG

override fun setLogDispatcher(dispatcher: ((CioLogLevel, String) -> Unit)?) {
}

override fun info(message: String) {
log(CioLogLevel.INFO, message)
}
Expand Down
12 changes: 9 additions & 3 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public abstract class io/customer/sdk/core/di/AndroidSDKComponent : io/customer/
}

public final class io/customer/sdk/core/di/AndroidSDKComponentImpl : io/customer/sdk/core/di/AndroidSDKComponent {
public fun <init> (Landroid/content/Context;Lio/customer/sdk/data/store/Client;)V
public fun <init> (Landroid/content/Context;)V
public fun getApplication ()Landroid/app/Application;
public fun getApplicationContext ()Landroid/content/Context;
public fun getApplicationStore ()Lio/customer/sdk/data/store/ApplicationStore;
Expand All @@ -137,7 +137,6 @@ public abstract class io/customer/sdk/core/di/DiGraph {
public fun <init> ()V
public final fun getOverrides ()Ljava/util/concurrent/ConcurrentHashMap;
public final fun getSingletons ()Ljava/util/concurrent/ConcurrentHashMap;
public final fun overrideDependency (Ljava/lang/Class;Ljava/lang/Object;)V
public fun reset ()V
}

Expand All @@ -155,7 +154,7 @@ public final class io/customer/sdk/core/di/SDKComponent : io/customer/sdk/core/d
}

public final class io/customer/sdk/core/di/SDKComponentExtKt {
public static final fun registerAndroidSDKComponent (Lio/customer/sdk/core/di/SDKComponent;Landroid/content/Context;Lio/customer/sdk/data/store/Client;)Lio/customer/sdk/core/di/AndroidSDKComponent;
public static final fun setupAndroidComponent (Lio/customer/sdk/core/di/SDKComponent;Landroid/content/Context;)Lio/customer/sdk/core/di/AndroidSDKComponent;
}

public abstract interface class io/customer/sdk/core/environment/BuildEnvironment {
Expand All @@ -167,6 +166,10 @@ public final class io/customer/sdk/core/environment/DefaultBuildEnvironment : io
public fun getDebugModeEnabled ()Z
}

public final class io/customer/sdk/core/extensions/ContextExtensionsKt {
public static final fun applicationMetaData (Landroid/content/Context;)Landroid/os/Bundle;
}

public abstract interface class io/customer/sdk/core/module/CustomerIOModule {
public abstract fun getModuleConfig ()Lio/customer/sdk/core/module/CustomerIOModuleConfig;
public abstract fun getModuleName ()Ljava/lang/String;
Expand All @@ -186,6 +189,7 @@ public final class io/customer/sdk/core/util/CioLogLevel : java/lang/Enum {
public static final field ERROR Lio/customer/sdk/core/util/CioLogLevel;
public static final field INFO Lio/customer/sdk/core/util/CioLogLevel;
public static final field NONE Lio/customer/sdk/core/util/CioLogLevel;
public final fun getPriority ()I
public static fun valueOf (Ljava/lang/String;)Lio/customer/sdk/core/util/CioLogLevel;
public static fun values ()[Lio/customer/sdk/core/util/CioLogLevel;
}
Expand All @@ -210,6 +214,7 @@ public final class io/customer/sdk/core/util/LogcatLogger : io/customer/sdk/core
public fun error (Ljava/lang/String;)V
public fun getLogLevel ()Lio/customer/sdk/core/util/CioLogLevel;
public fun info (Ljava/lang/String;)V
public fun setLogDispatcher (Lkotlin/jvm/functions/Function2;)V
public fun setLogLevel (Lio/customer/sdk/core/util/CioLogLevel;)V
}

Expand All @@ -221,6 +226,7 @@ public abstract interface class io/customer/sdk/core/util/Logger {
public abstract fun error (Ljava/lang/String;)V
public abstract fun getLogLevel ()Lio/customer/sdk/core/util/CioLogLevel;
public abstract fun info (Ljava/lang/String;)V
public abstract fun setLogDispatcher (Lkotlin/jvm/functions/Function2;)V
public abstract fun setLogLevel (Lio/customer/sdk/core/util/CioLogLevel;)V
}

Expand Down
22 changes: 22 additions & 0 deletions core/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application>
<!--
This is how wrapper SDKs can provide their own values for the SDK_SOURCE and SDK_VERSION meta-data.
tools:replace="android:value" is used to replace the value of the meta-data.
tools:node="replace" is used to replace the entire meta-data tag.
Either of these attributes can be used to resolve any conflicts that may arise when merging
multiple AndroidManifest.xml files with the same meta-data tags.
-->
<meta-data
android:name="io.customer.sdk.android.core.SDK_SOURCE"
android:value="TestUserAgent"
tools:replace="android:value" />
<meta-data
android:name="io.customer.sdk.android.core.SDK_VERSION"
android:value="1.3.5"
tools:node="replace" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.customer.sdk.data.store

import io.customer.commontest.core.AndroidTest
import io.customer.sdk.core.extensions.applicationMetaData
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test

class AndroidManifestClientTest : AndroidTest() {
@Test
fun fromManifest_givenTestMetaData_expectClientWithTestMetaData() {
val client = Client.fromMetadata(application.applicationMetaData())

client.toString() shouldBeEqualTo "TestUserAgent Client/1.3.5"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.customer.sdk.core.di

import android.app.Application
import android.content.Context
import io.customer.sdk.core.extensions.applicationMetaData
import io.customer.sdk.data.store.ApplicationStore
import io.customer.sdk.data.store.ApplicationStoreImpl
import io.customer.sdk.data.store.BuildStore
Expand All @@ -28,12 +29,14 @@ abstract class AndroidSDKComponent : DiGraph() {
* Integrate this graph at SDK startup using from Android entry point.
*/
class AndroidSDKComponentImpl(
private val context: Context,
override val client: Client
private val context: Context
) : AndroidSDKComponent() {
override val application: Application
get() = newInstance<Application> { context.applicationContext as Application }

override val client: Client
get() = singleton<Client> { Client.fromMetadata(context.applicationMetaData()) }

init {
SDKComponent.activityLifecycleCallbacks.register(application)
}
Expand Down
14 changes: 0 additions & 14 deletions core/src/main/kotlin/io/customer/sdk/core/di/DiGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,4 @@ abstract class DiGraph {
overrides.clear()
singletons.clear()
}

// TODO: Remove deprecated functions after all usages are removed.
@Deprecated("Use overrideDependency<Dependency> instead", ReplaceWith("overrideDependency<Dependency>(value)"))
fun <Dependency : Any> overrideDependency(dependency: Class<Dependency>, value: Dependency) {
overrides[dependency.name] = value as Any
}

@Deprecated("Use newInstance or singleton instead", ReplaceWith("newInstance()"))
inline fun <reified DEP : Any> override(): DEP? = overrides[dependencyKey<DEP>(identifier = null)] as? DEP

@Deprecated("Use singleton instead", ReplaceWith("singleton(newInstanceCreator)"))
inline fun <reified INST : Any> getSingletonInstanceCreate(newInstanceCreator: () -> INST): INST {
return getOrCreateSingletonInstance(identifier = null, newInstanceCreator = newInstanceCreator)
}
}
10 changes: 5 additions & 5 deletions core/src/main/kotlin/io/customer/sdk/core/di/SDKComponentExt.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.customer.sdk.core.di

import android.content.Context
import io.customer.sdk.data.store.Client

/**
* The file contains extension functions for the SDKComponent object and its dependencies.
Expand All @@ -10,10 +9,11 @@ import io.customer.sdk.data.store.Client
/**
* Create and register an instance of AndroidSDKComponent with the provided context,
* only if it is not already initialized.
* This function should be called from all entry points of the SDK to ensure that
* AndroidSDKComponent is initialized before accessing any of its dependencies.
*/
fun SDKComponent.registerAndroidSDKComponent(
context: Context,
client: Client
fun SDKComponent.setupAndroidComponent(
context: Context
) = registerDependency<AndroidSDKComponent> {
AndroidSDKComponentImpl(context, client)
AndroidSDKComponentImpl(context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.customer.sdk.core.extensions

import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import io.customer.sdk.core.di.SDKComponent

/**
* Retrieves application meta-data from AndroidManifest.xml file.
*
* @return The meta-data bundle from application info.
*/
fun Context.applicationMetaData(): Bundle? = try {
val applicationInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getApplicationInfo(
packageName,
PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong())
)
} else {
@Suppress("DEPRECATION")
packageManager.getApplicationInfo(
packageName,
PackageManager.GET_META_DATA
)
}
applicationInfo.metaData
} catch (ex: Exception) {
SDKComponent.logger.error("Failed to get ApplicationInfo with error: ${ex.message}")
null
}
80 changes: 57 additions & 23 deletions core/src/main/kotlin/io/customer/sdk/core/util/Logger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,59 @@ import android.util.Log
import io.customer.sdk.core.environment.BuildEnvironment

interface Logger {
// Log level to determine which logs to print
// This is the log level set by the user in configurations or the default log level
var logLevel: CioLogLevel

/**
* Sets the dispatcher to handle log events based on the log level
* Default implementation is to print logs to Logcat
* In wrapper SDKs, this will be overridden to emit logs to more user-friendly channels
* like console, etc.
* If the dispatcher holds any reference to application context, the caller should ensure
* to clear references when the context is destroyed.
*
* @param dispatcher Dispatcher to handle log events based on the log level, pass null
* to reset to default
*/
fun setLogDispatcher(dispatcher: ((CioLogLevel, String) -> Unit)?)

fun info(message: String)
fun debug(message: String)
fun error(message: String)
}

enum class CioLogLevel {
NONE,
ERROR,
INFO,
DEBUG;
/**
* Log levels for Customer.io SDK logs
*
* @property priority Priority of the log level. The higher the value, the more verbose
* the log level.
* @see shouldLog to determine if a log should be printed based on specified log level
*/
enum class CioLogLevel(val priority: Int) {
NONE(priority = 0),
ERROR(priority = 1),
INFO(priority = 2),
DEBUG(priority = 3);

companion object {
val DEFAULT = ERROR
fun getLogLevel(level: String?, fallback: CioLogLevel = NONE): CioLogLevel {

fun getLogLevel(level: String?, fallback: CioLogLevel = DEFAULT): CioLogLevel {
return values().find { value -> value.name.equals(level, ignoreCase = true) }
?: fallback
}
}
}

/**
* Determines if a log should be printed based on the specified log level
*
* @param levelForMessage Log level of the message
* @return true if the log should be printed, false otherwise
*/
internal fun CioLogLevel.shouldLog(levelForMessage: CioLogLevel): Boolean {
return when (this) {
CioLogLevel.NONE -> false
CioLogLevel.ERROR -> levelForMessage == CioLogLevel.ERROR
CioLogLevel.INFO -> levelForMessage == CioLogLevel.ERROR || levelForMessage == CioLogLevel.INFO
CioLogLevel.DEBUG -> true
}
return this.priority >= levelForMessage.priority
}

class LogcatLogger(
Expand All @@ -54,28 +79,37 @@ class LogcatLogger(
preferredLogLevel = value
}

private var logDispatcher: ((CioLogLevel, String) -> Unit)? = null

override fun setLogDispatcher(dispatcher: ((CioLogLevel, String) -> Unit)?) {
logDispatcher = dispatcher
}

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

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

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

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

if (shouldLog) block()
if (shouldLog) {
// Dispatch log event to log dispatcher only if the log level is met and the dispatcher is set
// Otherwise, log to Logcat
logDispatcher?.invoke(levelForMessage, message) ?: when (levelForMessage) {
CioLogLevel.NONE -> {}
CioLogLevel.ERROR -> Log.e(TAG, message)
CioLogLevel.INFO -> Log.i(TAG, message)
CioLogLevel.DEBUG -> Log.d(TAG, message)
}
}
}

companion object {
Expand Down
Loading

0 comments on commit 5c70469

Please sign in to comment.