Skip to content

Commit

Permalink
chore: use meta-data for user agent client (#439)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrehan27 authored Sep 25, 2024
1 parent 0f20226 commit 5f55551
Show file tree
Hide file tree
Showing 19 changed files with 209 additions and 109 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,7 +1,6 @@
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
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.registerAndroidSDKComponent(application))
}
}
8 changes: 6 additions & 2 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 Down Expand Up @@ -155,7 +155,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 registerAndroidSDKComponent (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 +167,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 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

Check warning on line 32 in core/src/main/kotlin/io/customer/sdk/core/di/AndroidSDKComponent.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/core/di/AndroidSDKComponent.kt#L32

Added line #L32 was not covered by tests
) : AndroidSDKComponent() {
override val application: Application
get() = newInstance<Application> { context.applicationContext as Application }

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

Check warning on line 38 in core/src/main/kotlin/io/customer/sdk/core/di/AndroidSDKComponent.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/core/di/AndroidSDKComponent.kt#L38

Added line #L38 was not covered by tests

init {
SDKComponent.activityLifecycleCallbacks.register(application)
}
Expand Down
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 @@ -12,8 +11,7 @@ import io.customer.sdk.data.store.Client
* only if it is not already initialized.
*/
fun SDKComponent.registerAndroidSDKComponent(
context: Context,
client: Client
context: Context
) = registerDependency<AndroidSDKComponent> {
AndroidSDKComponentImpl(context, client)
AndroidSDKComponentImpl(context)

Check warning on line 16 in core/src/main/kotlin/io/customer/sdk/core/di/SDKComponentExt.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/core/di/SDKComponentExt.kt#L16

Added line #L16 was not covered by tests
}
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 {

Check warning on line 14 in core/src/main/kotlin/io/customer/sdk/core/extensions/ContextExtensions.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/core/extensions/ContextExtensions.kt#L14

Added line #L14 was not covered by tests
val applicationInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getApplicationInfo(
packageName,
PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong())

Check warning on line 18 in core/src/main/kotlin/io/customer/sdk/core/extensions/ContextExtensions.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/core/extensions/ContextExtensions.kt#L16-L18

Added lines #L16 - L18 were not covered by tests
)
} else {
@Suppress("DEPRECATION")
packageManager.getApplicationInfo(
packageName,
PackageManager.GET_META_DATA

Check warning on line 24 in core/src/main/kotlin/io/customer/sdk/core/extensions/ContextExtensions.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/core/extensions/ContextExtensions.kt#L22-L24

Added lines #L22 - L24 were not covered by tests
)
}
applicationInfo.metaData
} catch (ex: Exception) {
SDKComponent.logger.error("Failed to get ApplicationInfo with error: ${ex.message}")
null

Check warning on line 30 in core/src/main/kotlin/io/customer/sdk/core/extensions/ContextExtensions.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/core/extensions/ContextExtensions.kt#L27-L30

Added lines #L27 - L30 were not covered by tests
}
88 changes: 25 additions & 63 deletions core/src/main/kotlin/io/customer/sdk/data/store/Client.kt
Original file line number Diff line number Diff line change
@@ -1,83 +1,45 @@
package io.customer.sdk.data.store

import android.os.Bundle
import io.customer.sdk.Version

/**
* Sealed class to hold information about the SDK wrapper and package that the
* client app is using.
* Represents the client information to append with user-agent.
*
* @property source name of the client to append with user-agent.
* @property sdkVersion version of the SDK used.
*/
sealed class Client(
@Suppress("MemberVisibilityCanBePrivate")
class Client(
val source: String,
val sdkVersion: String
) {
override fun toString(): String = "$source Client/$sdkVersion"

/**
* Simpler class for Android clients.
*/
class Android(sdkVersion: String) : Client(source = SOURCE_ANDROID, sdkVersion = sdkVersion)

/**
* Simpler class for ReactNative clients.
*/
class ReactNative(sdkVersion: String) : Client(
source = SOURCE_REACT_NATIVE,
sdkVersion = sdkVersion
)

/**
* Simpler class for Expo clients.
*/
class Expo(sdkVersion: String) : Client(source = SOURCE_EXPO, sdkVersion = sdkVersion)

/**
* Simpler class for Flutter clients.
*/
class Flutter(sdkVersion: String) : Client(source = SOURCE_FLUTTER, sdkVersion = sdkVersion)

/**
* Other class to allow adding custom sources for clients that are not
* supported above.
* <p/>
* Use this only if the client platform is not available in the above list.
*/
class Other internal constructor(
source: String,
sdkVersion: String
) : Client(source = source, sdkVersion = sdkVersion)

companion object {
internal const val SOURCE_ANDROID = "Android"
internal const val SOURCE_REACT_NATIVE = "ReactNative"
internal const val SOURCE_EXPO = "Expo"
internal const val SOURCE_FLUTTER = "Flutter"
private const val SOURCE_ANDROID = "Android"
internal const val META_DATA_SDK_SOURCE = "io.customer.sdk.android.core.SDK_SOURCE"
internal const val META_DATA_SDK_VERSION = "io.customer.sdk.android.core.SDK_VERSION"

/**
* Helper method to create client from raw values
* Creates a new [Client] instance from the manifest meta-data.
* If the user-agent or SDK version is not found, the default client is returned.
* Default client is created with [SOURCE_ANDROID] and SDK version mentioned in [Version] class.
*
* @param source raw string of client source (case insensitive)
* @param sdkVersion version of the SDK used
* @return [Client] created from provided values
* @param metadata Android application meta-data to retrieve the user-agent and SDK version from.
* @return The client instance created from the manifest meta-data.
* If not found, the default client is returned.
*/
fun fromRawValue(source: String, sdkVersion: String): Client = when {
source.equals(
other = SOURCE_ANDROID,
ignoreCase = true
) -> Android(sdkVersion = sdkVersion)
source.equals(
other = SOURCE_REACT_NATIVE,
ignoreCase = true
) -> ReactNative(sdkVersion = sdkVersion)
source.equals(
other = SOURCE_EXPO,
ignoreCase = true
) -> Expo(sdkVersion = sdkVersion)
source.equals(
other = SOURCE_FLUTTER,
ignoreCase = true
) -> Flutter(sdkVersion = sdkVersion)
else -> Other(source = source, sdkVersion = sdkVersion)
fun fromMetadata(metadata: Bundle?): Client {
val userAgent = metadata?.getString(META_DATA_SDK_SOURCE)
val sdkVersion = metadata?.getString(META_DATA_SDK_VERSION)

// If either value is null or blank, return the default client
return if (userAgent.isNullOrBlank() || sdkVersion.isNullOrBlank()) {
Client(source = SOURCE_ANDROID, sdkVersion = Version.version)
} else {
Client(source = userAgent, sdkVersion = sdkVersion)
}
}
}
}
85 changes: 85 additions & 0 deletions core/src/test/java/io/customer/sdk/data/store/ClientTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.customer.sdk.data.store

import android.os.Bundle
import io.customer.commontest.core.RobolectricTest
import io.customer.sdk.Version
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class ClientTest : RobolectricTest() {
private val defaultClientString: String = "Android Client/${Version.version}"

private fun createMetadata(
userAgent: String?,
sdkVersion: String?
) = Bundle().apply {
userAgent?.let { putString(Client.META_DATA_SDK_SOURCE, it) }
sdkVersion?.let { putString(Client.META_DATA_SDK_VERSION, it) }
}

@Test
fun fromManifest_givenValidMetaData_expectClientWithMetaData() {
val metadata = createMetadata("ReactNative", "1.2.3")

val client = Client.fromMetadata(metadata)

client.toString() shouldBeEqualTo "ReactNative Client/1.2.3"
}

@Test
fun fromManifest_givenNullUserAgent_expectDefaultSourceUsed() {
val metadata = createMetadata(null, "1.2.3")

val client = Client.fromMetadata(metadata)

client.toString() shouldBeEqualTo defaultClientString
}

@Test
fun fromManifest_givenEmptyUserAgent_expectDefaultSourceUsed() {
val metadata = createMetadata("", "1.2.3")

val client = Client.fromMetadata(metadata)

client.toString() shouldBeEqualTo defaultClientString
}

@Test
fun fromManifest_givenNullSdkVersion_expectDefaultSdkVersionUsed() {
val metadata = createMetadata("ReactNative", null)

val client = Client.fromMetadata(metadata)

client.toString() shouldBeEqualTo defaultClientString
}

@Test
fun fromManifest_givenEmptySdkVersion_expectDefaultSdkVersionUsed() {
val metadata = createMetadata("ReactNative", "")

val client = Client.fromMetadata(metadata)

client.toString() shouldBeEqualTo defaultClientString
}

@Test
fun fromManifest_givenNullMetaData_expectDefaultValuesUsed() {
val metadata = createMetadata(null, null)

val client = Client.fromMetadata(metadata)

client.toString() shouldBeEqualTo defaultClientString
}

@Test
fun fromManifest_givenEmptyMetaData_expectDefaultValuesUsed() {
val metadata = createMetadata("", "")

val client = Client.fromMetadata(metadata)

client.toString() shouldBeEqualTo defaultClientString
}
}
Loading

0 comments on commit 5f55551

Please sign in to comment.