Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added SDK config #1

Merged
merged 5 commits into from
Nov 24, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ rootProject.allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://maven.gist.build' }
}
}

Expand All @@ -41,10 +42,15 @@ android {
}

defaultConfig {
minSdkVersion 16
minSdkVersion 21
}
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Customer.io SDK
def cioVersion = "3.1.0"
implementation "io.customer.android:tracking:$cioVersion"
implementation "io.customer.android:messaging-push-fcm:$cioVersion"
implementation "io.customer.android:messaging-in-app:$cioVersion"
}
4 changes: 1 addition & 3 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.customer.customer_io">
</manifest>
<manifest package="io.customer.customer_io"></manifest>
120 changes: 98 additions & 22 deletions android/src/main/kotlin/io/customer/customer_io/CustomerIoPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,35 +1,111 @@
package io.customer.customer_io

import android.app.Application
import android.content.Context
import androidx.annotation.NonNull

import io.customer.customer_io.constant.Keys
import io.customer.customer_io.extension.*
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOShared
import io.customer.sdk.data.store.Client
import io.customer.sdk.util.Logger
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result

/** CustomerIoPlugin */
class CustomerIoPlugin: FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "customer_io")
channel.setMethodCallHandler(this)
}

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
class CustomerIoPlugin : FlutterPlugin, MethodCallHandler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class CustomerIoPlugin : FlutterPlugin, MethodCallHandler {
class CustomerIOPlugin : FlutterPlugin, MethodCallHandler {

Usually when referring to CIO, we use CustomerIO instead of CustomerIo.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since our team is not knowledgable with flutter, classes like this could benefit from more comment explaining what a Flutter plugin is?

/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
Comment on lines +23 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comment! I appreciate that since our team doesn't know Flutter.

private lateinit var channel: MethodChannel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private lateinit var channel: MethodChannel
private lateinit var flutterCommunicationChannel: MethodChannel

I like the idea of having a more descriptive variable name so it's easy to understand that MethodChannel is specific to flutter and it's purpose.

private lateinit var context: Context

private val logger: Logger
get() = CustomerIOShared.instance().diGraph.logger

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "customer_io")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of "customer_io"? Is this string meant to be unique for the host Flutter app?

If so, should we use package name? io.customer.sdk.flutter?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its the channel name, we can have multiple channels to communicate with the native platform and flutter, also the convention in dart is to use "_" rather than "."

channel.setMethodCallHandler(this)
}

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, wow. If it's true that Flutter plugins use strings for calling methods of our native SDK, I can see us eventually wanting automated tests written.

I don't know Flutter well enough to see if we create integration tests that:

  • mock the MethodChannel in this file
  • call methods on our Flutter plugin: CustomerIO.initialize(...) and expect the mock to be called.

when (call.method) {
"getPlatformVersion" -> {
result.success("Android-${android.os.Build.VERSION.RELEASE}")
}
Comment on lines +41 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function just for your own testing or is this something we need to implement for a flutter plugin?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for internal testing, will remove it afterwards.

"initialize" -> {
initialize(call, result)
}
else -> {
result.notImplemented()
}
}
}


private fun initialize(call: MethodCall, result: Result) {
try {
val application: Application = context.applicationContext as Application
val configData = call.arguments as? Map<String, Any> ?: emptyMap()
val siteId = configData.getString(Keys.Environment.SITE_ID)
val apiKey = configData.getString(Keys.Environment.API_KEY)
val region = configData.getProperty<String>(
Keys.Environment.REGION
)?.takeIfNotBlank().toRegion()

CustomerIO.Builder(
siteId = siteId,
apiKey = apiKey,
region = region,
appContext = application,
).apply {
setClient(client = getUserAgentClient(packageConfig = configData))
setupConfig(configData)
}.build()
logger.info("Customer.io instance initialized successfully")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this log statement need to be here since the native SDK makes logs similar to this?

If there is value in adding logs here, should we make the logs Flutter specific?

Suggested change
logger.info("Customer.io instance initialized successfully")
logger.info("[Flutter] Customer.io instance initialized successfully")

result.success(true)
} catch (e: Exception) {
logger.error("Failed to initialize Customer.io instance from app, ${e.message}")
result.error("FlutterSegmentException", e.localizedMessage, null);
}
}

private fun getUserAgentClient(packageConfig: Map<String, Any?>?): Client {
val sourceSDKVersion = packageConfig?.getProperty<String>(
Keys.PackageConfig.SOURCE_SDK_VERSION
)?.takeIfNotBlank() ?: packageConfig?.getProperty<String>(
Keys.PackageConfig.SOURCE_SDK_VERSION_COMPAT
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what Keys is.

Is this code saying that Flutter customers will set in their apps the value for SOURCE_SDK_VERSION? Or are we able to set that value ourselves?

mrehan27 marked this conversation as resolved.
Show resolved Hide resolved
)?.takeIfNotBlank() ?: "n/a"
// TODO: change it to flutter
return Client.ReactNative(sdkVersion = sourceSDKVersion)
}
}

override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
private fun CustomerIO.Builder.setupConfig(config: Map<String, Any?>?): CustomerIO.Builder {
if (config == null) return this

val logLevel = config.getProperty<String>(Keys.Config.LOG_LEVEL).toCIOLogLevel()
setLogLevel(level = logLevel)
config.getProperty<String>(Keys.Config.TRACKING_API_URL)?.takeIfNotBlank()?.let { value ->
setTrackingApiURL(value)
}
config.getProperty<Boolean>(Keys.Config.AUTO_TRACK_DEVICE_ATTRIBUTES)?.let { value ->
autoTrackDeviceAttributes(shouldTrackDeviceAttributes = value)
}
config.getProperty<Double>(Keys.Config.BACKGROUND_QUEUE_MIN_NUMBER_OF_TASKS)?.let { value ->
setBackgroundQueueMinNumberOfTasks(backgroundQueueMinNumberOfTasks = value.toInt())
}
config.getProperty<Double>(Keys.Config.BACKGROUND_QUEUE_SECONDS_DELAY)?.let { value ->
setBackgroundQueueSecondsDelay(backgroundQueueSecondsDelay = value)
}
Comment on lines +91 to +104
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a function that we can add to the native SDK or something to do this work for us?

I feel this code is error-prone as we would need to remember to update this code each time that we update the SDK configuration object in the native SDKs. By housing the code in the native SDK, we have a higher chance of remembering.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can support initialization through maps in Native SDKs to help in this for both React Native and Flutter. But we need to be careful and not rename any key used by these wrappers or change its type to avoid breaking this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Agreed, something that we should definitely focus later on.

return this
}

override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
24 changes: 24 additions & 0 deletions android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.customer.customer_io.constant

internal object Keys {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See PR comment in regards to iOS Keys object

object Environment {
const val SITE_ID = "siteId"
const val API_KEY = "apiKey"
const val REGION = "region"
const val ORGANIZATION_ID = "organizationId"
}

object Config {
const val TRACKING_API_URL = "trackingApiUrl"
const val AUTO_TRACK_DEVICE_ATTRIBUTES = "autoTrackDeviceAttributes"
const val LOG_LEVEL = "logLevel"
const val AUTO_TRACK_PUSH_EVENTS = "autoTrackPushEvents"
const val BACKGROUND_QUEUE_MIN_NUMBER_OF_TASKS = "backgroundQueueMinNumberOfTasks"
const val BACKGROUND_QUEUE_SECONDS_DELAY = "backgroundQueueSecondsDelay"
}

object PackageConfig {
const val SOURCE_SDK_VERSION = "version"
const val SOURCE_SDK_VERSION_COMPAT = "sdkVersion"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.customer.customer_io.extension

import io.customer.sdk.CustomerIOShared

@Throws(IllegalArgumentException::class)
internal inline fun <reified T> Map<String, Any?>.getPropertyUnsafe(key: String): T {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
internal inline fun <reified T> Map<String, Any?>.getPropertyUnsafe(key: String): T {
internal inline fun <reified T> Map<String, Any?>.getValueUnsafe(key: String): T {

IMO, value reads easier since maps contain key value pairs.

val property = get(key)

if (property !is T) {
throw IllegalArgumentException(
"Invalid value provided for key: $key, value $property must be of type ${T::class.java.simpleName}"
)
}
return property
}

internal inline fun <reified T> Map<String, Any?>.getProperty(key: String): T? = try {
getPropertyUnsafe(key)
} catch (ex: IllegalArgumentException) {
CustomerIOShared.instance().diGraph.logger.error(
ex.message ?: "getProperty($key) -> IllegalArgumentException"
)
null
}

@Throws(IllegalArgumentException::class)
internal fun Map<String, Any?>.getString(key: String): String = try {
getPropertyUnsafe<String>(key).takeIfNotBlank() ?: throw IllegalArgumentException(
"Invalid value provided for $key, must not be blank"
)
} catch (ex: IllegalArgumentException) {
CustomerIOShared.instance().diGraph.logger.error(
ex.message ?: "getString($key) -> IllegalArgumentException"
)
throw ex
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.customer.customer_io.extension

internal fun String.takeIfNotBlank(): String? = takeIf { it.isNotBlank() }
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.customer.customer_io.extension

import io.customer.sdk.data.model.Region
import io.customer.sdk.util.CioLogLevel

internal fun String?.toRegion(fallback: Region = Region.US): Region {
return if (this.isNullOrBlank()) fallback
else listOf(
Region.US,
Region.EU,
).find { value -> value.code.equals(this, ignoreCase = true) } ?: fallback
}

internal fun String?.toCIOLogLevel(fallback: CioLogLevel = CioLogLevel.NONE): CioLogLevel {
return CioLogLevel.values().find { value -> value.name.equals(this, ignoreCase = true) }
?: fallback
}
Comment on lines +6 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See PR comment in regards to housing code like this in the native SDKs instead of in wrapper SDK (in this case, Flutter).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, thats the plan eventually, I think we even added that to shared because React native also relies on same methods for convertions.

4 changes: 2 additions & 2 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ android {
applicationId "io.customer.customer_io_example"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
minSdkVersion 21
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Expand Down
2 changes: 1 addition & 1 deletion example/android/app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
20 changes: 10 additions & 10 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.customer.customer_io_example">
<application
android:label="customer_io_example"

<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:label="customer_io_example">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package io.customer.customer_io_example

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
class MainActivity : FlutterActivity() {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<?xml version="1.0" encoding="utf-8"?><!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<?xml version="1.0" encoding="utf-8"?><!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />

Expand Down
2 changes: 1 addition & 1 deletion example/android/app/src/profile/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
37 changes: 37 additions & 0 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
PODS:
- customer_io (0.0.1):
- CustomerIO/Tracking
- Flutter
- CustomerIO/Tracking (1.2.2):
- CustomerIOTracking (= 1.2.2)
- CustomerIOCommon (1.2.2)
- CustomerIOTracking (1.2.2):
- CustomerIOCommon (= 1.2.2)
- Flutter (1.0.0)

DEPENDENCIES:
- customer_io (from `.symlinks/plugins/customer_io/ios`)
- Flutter (from `Flutter`)

SPEC REPOS:
trunk:
- CustomerIO
- CustomerIOCommon
- CustomerIOTracking

EXTERNAL SOURCES:
customer_io:
:path: ".symlinks/plugins/customer_io/ios"
Flutter:
:path: Flutter

SPEC CHECKSUMS:
customer_io: 99ae280180a2fe4c0514f28a8da5e7a66734a612
CustomerIO: 0a31ad1baed6cd952469b9098e2e73fa96cec70e
CustomerIOCommon: 3bf82c3574205819a69baa1f0bc5b47671dd2d19
CustomerIOTracking: 101dc8c25eff807eadc8fbcf83b76f98fb5832a5
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a

PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c

COCOAPODS: 1.11.3
Loading