diff --git a/android/src/main/java/com/amplitude/android/utilities/AndroidStorage.kt b/android/src/main/java/com/amplitude/android/utilities/AndroidStorage.kt
index 66df52bc..437e2201 100644
--- a/android/src/main/java/com/amplitude/android/utilities/AndroidStorage.kt
+++ b/android/src/main/java/com/amplitude/android/utilities/AndroidStorage.kt
@@ -92,7 +92,10 @@ class AndroidStorage(
}
override fun getEventCallback(insertId: String): EventCallBack? {
- return eventCallbacksMap.getOrDefault(insertId, null)
+ if (eventCallbacksMap.contains(insertId)) {
+ return eventCallbacksMap.get(insertId)
+ }
+ return null
}
override fun removeEventCallback(insertId: String) {
diff --git a/core/src/main/java/com/amplitude/core/Amplitude.kt b/core/src/main/java/com/amplitude/core/Amplitude.kt
index 3db6eaa3..9f9e30b5 100644
--- a/core/src/main/java/com/amplitude/core/Amplitude.kt
+++ b/core/src/main/java/com/amplitude/core/Amplitude.kt
@@ -29,6 +29,11 @@ import kotlinx.coroutines.launch
import org.json.JSONObject
import java.util.concurrent.Executors
+/**
+ *
Amplitude
+ * This is the SDK instance class that contains all of the SDK functionality.
+ * Many of the SDK functions return the SDK instance back, allowing you to chain multiple methods calls together.
+ */
open class Amplitude internal constructor(
val configuration: Configuration,
val store: State,
@@ -52,7 +57,7 @@ open class Amplitude internal constructor(
}
/**
- * Public Constructor
+ * Public Constructor.
*/
constructor(configuration: Configuration) : this(configuration, State())
@@ -68,18 +73,34 @@ open class Amplitude internal constructor(
}
@Deprecated("Please use 'track' instead.", ReplaceWith("track"))
- fun logEvent(event: BaseEvent) {
- track(event)
+ fun logEvent(event: BaseEvent): Amplitude {
+ return track(event)
}
- fun track(event: BaseEvent, callback: EventCallBack? = null) {
+ /**
+ * Track an event.
+ *
+ * @param event the event
+ * @param callback the optional event callback
+ * @return the Amplitude instance
+ */
+ fun track(event: BaseEvent, callback: EventCallBack? = null): Amplitude {
callback ?. let {
event.callback = it
}
process(event)
+ return this
}
- fun track(eventType: String, eventProperties: JSONObject? = null, options: EventOptions? = null) {
+ /**
+ * Log event with the specified event type, event properties, and optional event options.
+ *
+ * @param eventType the event type
+ * @param eventProperties the event properties
+ * @param options optional event options
+ * @return the Amplitude instance
+ */
+ fun track(eventType: String, eventProperties: JSONObject? = null, options: EventOptions? = null): Amplitude {
val event = BaseEvent()
event.eventType = eventType
event.eventProperties = eventProperties
@@ -87,35 +108,65 @@ open class Amplitude internal constructor(
event.mergeEventOptions(it)
}
process(event)
+ return this
}
/**
- * Identify. Use this to send an Identify object containing user property operations to Amplitude server.
+ * Identify lets you to send an Identify object containing user property operations to Amplitude server.
+ * You can modify user properties by calling this api.
+ *
+ * @param identify identify object
+ * @param options optional event options
+ * @return the Amplitude instance
*/
- fun identify(identify: Identify, options: EventOptions? = null) {
+ fun identify(identify: Identify, options: EventOptions? = null): Amplitude {
val event = IdentifyEvent()
event.userProperties = identify.properties
options ?. let {
event.mergeEventOptions(it)
}
process(event)
+ return this
}
- fun identify(userId: String) {
+ /**
+ * Set the user id (can be null).
+ *
+ * @param userId custom user id
+ * @return the Amplitude instance
+ */
+ fun setUserId(userId: String?): Amplitude {
this.idContainer.identityManager.editIdentity().setUserId(userId).commit()
+ return this
}
- fun setDeviceId(deviceId: String) {
+ /**
+ * Sets a custom device id. Note: only do this if you know what you are doing!
+ *
+ * @param deviceId custom device id
+ * @return the Amplitude instance
+ */
+ fun setDeviceId(deviceId: String): Amplitude {
this.idContainer.identityManager.editIdentity().setDeviceId(deviceId).commit()
+ return this
}
- fun groupIdentify(groupType: String, groupName: String, identify: Identify, options: EventOptions? = null) {
+ /**
+ * Identify a group. You can modify group properties by calling this api.
+ *
+ * @param groupType the group type
+ * @param groupName the group name
+ * @param identify identify object
+ * @param options optional event options
+ * @return the Amplitude instance
+ */
+ fun groupIdentify(groupType: String, groupName: String, identify: Identify, options: EventOptions? = null): Amplitude {
val event = GroupIdentifyEvent()
var group: JSONObject? = null
try {
group = JSONObject().put(groupType, groupName)
} catch (e: Exception) {
- logger.error(e.toString())
+ logger.error("Error in groupIdentify: $e")
}
event.groups = group
event.groupProperties = identify.properties
@@ -123,49 +174,73 @@ open class Amplitude internal constructor(
event.mergeEventOptions(it)
}
process(event)
+ return this
}
/**
- * Sets the user's group.
+ * Set the user's group.
+ *
+ * @param groupType the group type
+ * @param groupName the group name
+ * @param options optional event options
+ * @return the Amplitude instance
*/
- fun setGroup(groupType: String, groupName: String, options: EventOptions? = null) {
+ fun setGroup(groupType: String, groupName: String, options: EventOptions? = null): Amplitude {
val identify = Identify().set(groupType, groupName)
identify(identify, options)
+ return this
}
/**
- * ets the user's groups.
+ * Sets the user's groups.
+ *
+ * @param groupType the group type
+ * @param groupName the group name
+ * @param options optional event options
+ * @return the Amplitude instance
*/
- fun setGroup(groupType: String, groupName: Array, options: EventOptions? = null) {
+ fun setGroup(groupType: String, groupName: Array, options: EventOptions? = null): Amplitude {
val identify = Identify().set(groupType, groupName)
identify(identify, options)
+ return this
}
@Deprecated("Please use 'revenue' instead.", ReplaceWith("revenue"))
- fun logRevenue(revenue: Revenue) {
+ fun logRevenue(revenue: Revenue): Amplitude {
revenue(revenue)
+ return this
}
/**
* Create a Revenue object to hold your revenue data and properties,
* and log it as a revenue event using this method.
+ *
+ * @param revenue revenue object
+ * @param options optional event options
+ * @return the Amplitude instance
*/
- fun revenue(revenue: Revenue, options: EventOptions? = null) {
+ fun revenue(revenue: Revenue, options: EventOptions? = null): Amplitude {
if (!revenue.isValid()) {
- return
+ logger.warn("Invalid revenue object, missing required fields")
+ return this
}
val event = revenue.toRevenueEvent()
options ?. let {
event.mergeEventOptions(it)
}
revenue(event)
+ return this
}
/**
- * Log a Revenue Event
+ * Log a Revenue Event.
+ *
+ * @param event the revenue event
+ * @return the Amplitude instance
*/
- fun revenue(event: RevenueEvent) {
+ fun revenue(event: RevenueEvent): Amplitude {
process(event)
+ return this
}
private fun process(event: BaseEvent) {
@@ -177,6 +252,12 @@ open class Amplitude internal constructor(
}
}
+ /**
+ * Add a plugin.
+ *
+ * @param plugin the plugin
+ * @return the Amplitude instance
+ */
fun add(plugin: Plugin): Amplitude {
when (plugin) {
is ObservePlugin -> {
diff --git a/core/src/main/java/com/amplitude/core/events/Identify.kt b/core/src/main/java/com/amplitude/core/events/Identify.kt
index e92d9341..68e2295a 100644
--- a/core/src/main/java/com/amplitude/core/events/Identify.kt
+++ b/core/src/main/java/com/amplitude/core/events/Identify.kt
@@ -569,7 +569,7 @@ class Identify() {
properties.getJSONObject(operation.operationType).put(property, value)
propertySet.add(property)
} catch (e: Exception) {
- ConsoleLogger.logger.error(e.toString())
+ ConsoleLogger.logger.error("Error in set user property: $e")
}
}
diff --git a/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt b/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt
index 305250b2..092ef137 100644
--- a/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt
+++ b/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt
@@ -80,7 +80,7 @@ class EventPipeline(
storage.writeEvent(message.event)
} catch (e: Exception) {
e.message?.let {
- amplitude.logger.error(it)
+ amplitude.logger.error("Error when write event: $it")
}
}
@@ -117,7 +117,7 @@ class EventPipeline(
responseHandler?.handle(connection.response)
} catch (e: Exception) {
e.message?.let {
- amplitude.logger.error(it)
+ amplitude.logger.error("Error when upload event: $it")
}
}
}
diff --git a/core/src/main/java/com/amplitude/core/utilities/JSONUtil.kt b/core/src/main/java/com/amplitude/core/utilities/JSONUtil.kt
index 7029ceb5..e65cd2e7 100644
--- a/core/src/main/java/com/amplitude/core/utilities/JSONUtil.kt
+++ b/core/src/main/java/com/amplitude/core/utilities/JSONUtil.kt
@@ -48,6 +48,7 @@ object JSONUtil {
eventJSON.addValue("insert_id", event.insertId)
eventJSON.addValue("library", event.library)
eventJSON.addValue("partner_id", event.partnerId)
+ eventJSON.addValue("android_app_set_id", event.appSetId)
event.plan?. let {
eventJSON.put("plan", it.toJSONObject())
}
@@ -157,50 +158,51 @@ internal fun JSONArray.toIntArray(): IntArray {
internal fun JSONObject.toBaseEvent(): BaseEvent {
val event = BaseEvent()
event.eventType = this.getString("event_type")
- event.userId = this.optString("user_id", null)
- event.deviceId = this.optString("device_id", null)
+ event.userId = this.optionalString("user_id", null)
+ event.deviceId = this.optionalString("device_id", null)
event.timestamp = if (this.has("time")) this.getLong("time") else null
- event.eventProperties = this.optJSONObject("event_properties", null)
- event.userProperties = this.optJSONObject("user_properties", null)
- event.groups = this.optJSONObject("groups", null)
- event.appVersion = this.optString("app_version", null)
- event.platform = this.optString("platform", null)
- event.osName = this.optString("os_name", null)
- event.osVersion = this.optString("os_version", null)
- event.deviceBrand = this.optString("device_brand", null)
- event.deviceManufacturer = this.optString("device_manufacturer", null)
- event.deviceModel = this.optString("device_model", null)
- event.carrier = this.optString("carrier", null)
- event.country = this.optString("country", null)
- event.region = this.optString("region", null)
- event.city = this.optString("city", null)
- event.dma = this.optString("dma", null)
- event.language = this.optString("language", null)
+ event.eventProperties = this.optionalJSONObject("event_properties", null)
+ event.userProperties = this.optionalJSONObject("user_properties", null)
+ event.groups = this.optionalJSONObject("groups", null)
+ event.appVersion = this.optionalString("app_version", null)
+ event.platform = this.optionalString("platform", null)
+ event.osName = this.optionalString("os_name", null)
+ event.osVersion = this.optionalString("os_version", null)
+ event.deviceBrand = this.optionalString("device_brand", null)
+ event.deviceManufacturer = this.optionalString("device_manufacturer", null)
+ event.deviceModel = this.optionalString("device_model", null)
+ event.carrier = this.optionalString("carrier", null)
+ event.country = this.optionalString("country", null)
+ event.region = this.optionalString("region", null)
+ event.city = this.optionalString("city", null)
+ event.dma = this.optionalString("dma", null)
+ event.language = this.optionalString("language", null)
event.price = if (this.has("price")) this.getDouble("price") else null
event.quantity = if (this.has("quantity")) this.getInt("quantity") else null
event.revenue = if (this.has("revenue")) this.getDouble("revenue") else null
- event.productId = this.optString("productId", null)
- event.revenueType = this.optString("revenueType", null)
+ event.productId = this.optionalString("productId", null)
+ event.revenueType = this.optionalString("revenueType", null)
event.locationLat = if (this.has("location_lat")) this.getDouble("location_lat") else null
event.locationLng = if (this.has("location_lng")) this.getDouble("location_lng") else null
- event.ip = this.optString("ip", null)
- event.idfa = this.optString("idfa", null)
- event.idfv = this.optString("idfv", null)
- event.adid = this.optString("adid", null)
- event.androidId = this.optString("android_id", null)
+ event.ip = this.optionalString("ip", null)
+ event.idfa = this.optionalString("idfa", null)
+ event.idfv = this.optionalString("idfv", null)
+ event.adid = this.optionalString("adid", null)
+ event.androidId = this.optionalString("android_id", null)
+ event.appSetId = this.optString("android_app_set_id", null)
event.eventId = if (this.has("event_id")) this.getInt("event_id") else null
event.sessionId = this.getLong("session_id")
- event.insertId = this.optString("insert_id", null)
+ event.insertId = this.optionalString("insert_id", null)
event.library = if (this.has("library")) this.getString("library") else null
- event.partnerId = this.optString("partner_id", null)
+ event.partnerId = this.optionalString("partner_id", null)
event.plan = if (this.has("plan")) Plan.fromJSONObject(this.getJSONObject("plan")) else null
return event
}
internal fun JSONArray.toEvents(): List {
val events = mutableListOf()
- this.forEach {
- events.add((it as JSONObject).toBaseEvent())
+ (0 until this.length()).forEach {
+ events.add((this.getJSONObject(it)).toBaseEvent())
}
return events
}
@@ -209,11 +211,11 @@ internal fun JSONArray.split(): Pair {
val mid = this.length() / 2
val firstHalf = JSONArray()
val secondHalf = JSONArray()
- this.forEachIndexed { index, obj ->
+ (0 until this.length()).forEach { index, ->
if (index < mid) {
- firstHalf.put(obj)
+ firstHalf.put(this.getJSONObject(index))
} else {
- secondHalf.put(obj)
+ secondHalf.put(this.getJSONObject(index))
}
}
return Pair(firstHalf.toString(), secondHalf.toString())
@@ -224,3 +226,17 @@ internal fun JSONObject.addValue(key: String, value: Any?) {
this.put(key, value)
}
}
+
+inline fun JSONObject.optionalJSONObject(key: String, defaultValue: JSONObject?): JSONObject? {
+ if (this.has(key)) {
+ return this.getJSONObject(key)
+ }
+ return defaultValue
+}
+
+inline fun JSONObject.optionalString(key: String, defaultValue: String?): String? {
+ if (this.has(key)) {
+ return this.getString(key)
+ }
+ return defaultValue
+}
diff --git a/samples/kotlin-android-app/build.gradle b/samples/kotlin-android-app/build.gradle
new file mode 100644
index 00000000..6f46a9e2
--- /dev/null
+++ b/samples/kotlin-android-app/build.gradle
@@ -0,0 +1,49 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+ext {
+ AMPLITUDE_API_KEY = ""
+}
+
+android {
+ compileSdk 31
+
+ defaultConfig {
+ applicationId "com.amplitude.android.sample"
+ minSdk 16
+ targetSdk 31
+ versionCode 1
+ versionName "1.0"
+ multiDexEnabled true
+ buildConfigField "String", "AMPLITUDE_API_KEY", "\"${AMPLITUDE_API_KEY}\""
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ implementation project(':android')
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'com.google.android.material:material:1.5.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+ implementation 'com.google.android.gms:play-services-ads:20.6.0'
+ implementation 'com.google.android.gms:play-services-appset:16.0.2'
+}
diff --git a/samples/kotlin-android-app/proguard-rules.pro b/samples/kotlin-android-app/proguard-rules.pro
new file mode 100644
index 00000000..a226f984
--- /dev/null
+++ b/samples/kotlin-android-app/proguard-rules.pro
@@ -0,0 +1,23 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+play-services-ads.-keep class com.google.android.gms.ads.** { *; }
+
diff --git a/samples/kotlin-android-app/src/main/AndroidManifest.xml b/samples/kotlin-android-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..f37c3b3e
--- /dev/null
+++ b/samples/kotlin-android-app/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/kotlin-android-app/src/main/java/com/amplitude/android/sample/MainActivity.kt b/samples/kotlin-android-app/src/main/java/com/amplitude/android/sample/MainActivity.kt
new file mode 100644
index 00000000..5235b1fc
--- /dev/null
+++ b/samples/kotlin-android-app/src/main/java/com/amplitude/android/sample/MainActivity.kt
@@ -0,0 +1,47 @@
+package com.amplitude.android.sample
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.amplitude.core.events.EventOptions
+import com.amplitude.core.events.Identify
+import com.amplitude.core.events.Plan
+import com.amplitude.core.events.Revenue
+import org.json.JSONObject
+
+class MainActivity : AppCompatActivity() {
+ private val amplitude = MainApplication.amplitude
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ // set user properties
+ val identify = Identify()
+ identify.set("user-platform", "android")
+ .set("custom-properties", "sample")
+ val options = EventOptions()
+ options.plan = Plan(branch = "test")
+ amplitude.identify(identify, options)
+
+ // set groups fro this user
+ val groupType = "test-group-type"
+ val groupName = "android-kotlin-sample"
+ amplitude.setGroup(groupType, groupName)
+ amplitude.setGroup("orgId", "15")
+ amplitude.setGroup("sport", arrayOf("tennis", "soccer")) // list values
+
+ // group identify to set group properties
+ val groupIdentifyObj = Identify().set("key", "value")
+ amplitude.groupIdentify(groupType, groupName, groupIdentifyObj)
+
+ // log revenue call
+ val revenue = Revenue()
+ revenue.productId = "com.company.productId"
+ revenue.price = 3.99
+ revenue.quantity = 3
+ amplitude.revenue(revenue)
+
+ // track event with event properties
+ amplitude.track("test event properties", JSONObject().put("test", "test event property value"))
+ }
+}
diff --git a/samples/kotlin-android-app/src/main/java/com/amplitude/android/sample/MainApplication.kt b/samples/kotlin-android-app/src/main/java/com/amplitude/android/sample/MainApplication.kt
new file mode 100644
index 00000000..a2d23b50
--- /dev/null
+++ b/samples/kotlin-android-app/src/main/java/com/amplitude/android/sample/MainApplication.kt
@@ -0,0 +1,44 @@
+package com.amplitude.android.sample
+
+import android.app.Application
+import com.amplitude.android.Amplitude
+import com.amplitude.android.Configuration
+import com.amplitude.core.events.BaseEvent
+import com.amplitude.core.platform.Plugin
+import org.json.JSONObject
+
+class MainApplication : Application() {
+ companion object {
+ lateinit var amplitude: Amplitude
+ const val AMPLITUDE_API_KEY = BuildConfig.AMPLITUDE_API_KEY
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ // init instance
+ amplitude = Amplitude(
+ Configuration(
+ apiKey = AMPLITUDE_API_KEY,
+ context = applicationContext
+ )
+ )
+
+ // add sample plugin
+ amplitude.add(object : Plugin {
+ override val type: Plugin.Type = Plugin.Type.Enrichment
+ override lateinit var amplitude: com.amplitude.core.Amplitude
+
+ override fun execute(event: BaseEvent): BaseEvent? {
+ event.eventProperties = event.eventProperties ?: JSONObject()
+ event.eventProperties?.let {
+ it.put("custom android event property", "test")
+ }
+ return event
+ }
+ })
+
+ // identify a sample user
+ amplitude.setUserId("android-kotlin-sample-user")
+ }
+}
diff --git a/samples/kotlin-android-app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/kotlin-android-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/samples/kotlin-android-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/kotlin-android-app/src/main/res/drawable/ic_launcher_background.xml b/samples/kotlin-android-app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/samples/kotlin-android-app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/kotlin-android-app/src/main/res/layout/activity_main.xml b/samples/kotlin-android-app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..109b1ab9
--- /dev/null
+++ b/samples/kotlin-android-app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/kotlin-android-app/src/main/res/mipmap-hdpi/ic_launcher.webp b/samples/kotlin-android-app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
Binary files /dev/null and b/samples/kotlin-android-app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/samples/kotlin-android-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/samples/kotlin-android-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b2dfe3d1
Binary files /dev/null and b/samples/kotlin-android-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/samples/kotlin-android-app/src/main/res/mipmap-mdpi/ic_launcher.webp b/samples/kotlin-android-app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
Binary files /dev/null and b/samples/kotlin-android-app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/samples/kotlin-android-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/samples/kotlin-android-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..62b611da
Binary files /dev/null and b/samples/kotlin-android-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/samples/kotlin-android-app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/samples/kotlin-android-app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
Binary files /dev/null and b/samples/kotlin-android-app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/samples/kotlin-android-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/samples/kotlin-android-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..1b9a6956
Binary files /dev/null and b/samples/kotlin-android-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/samples/kotlin-android-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/samples/kotlin-android-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
Binary files /dev/null and b/samples/kotlin-android-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/samples/kotlin-android-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/samples/kotlin-android-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9287f508
Binary files /dev/null and b/samples/kotlin-android-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/samples/kotlin-android-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/samples/kotlin-android-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
Binary files /dev/null and b/samples/kotlin-android-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/samples/kotlin-android-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/samples/kotlin-android-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9126ae37
Binary files /dev/null and b/samples/kotlin-android-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/samples/kotlin-android-app/src/main/res/values-night/themes.xml b/samples/kotlin-android-app/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000..5ad6e039
--- /dev/null
+++ b/samples/kotlin-android-app/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/samples/kotlin-android-app/src/main/res/values/colors.xml b/samples/kotlin-android-app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..f8c6127d
--- /dev/null
+++ b/samples/kotlin-android-app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/samples/kotlin-android-app/src/main/res/values/strings.xml b/samples/kotlin-android-app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..2f0d4c89
--- /dev/null
+++ b/samples/kotlin-android-app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ kotlin-android-app
+
\ No newline at end of file
diff --git a/samples/kotlin-android-app/src/main/res/values/themes.xml b/samples/kotlin-android-app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..a2b9cc47
--- /dev/null
+++ b/samples/kotlin-android-app/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 6f6ab2b1..06e2a56e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -15,3 +15,5 @@ include 'android'
project(':android').projectDir = file('android')
include 'id'
project(':id').projectDir = file('id')
+include 'samples:kotlin-android-app'
+project(':samples:kotlin-android-app').projectDir = file('samples/kotlin-android-app')