diff --git a/.github/workflows/build-v2.yml b/.github/workflows/build-v2.yml
index 3f680c502..f49894a75 100644
--- a/.github/workflows/build-v2.yml
+++ b/.github/workflows/build-v2.yml
@@ -2,15 +2,14 @@ name: CI for v2
on:
push:
- branches: [ 'master-v2' ]
+ branches: ["master-v2"]
pull_request:
- branches: [ 'master-v2', 'develop-v2' ]
- types: [ 'opened', 'reopened', 'edited', 'synchronize' ]
+ branches: ["master-v2", "develop-v2"]
+ types: ["opened", "reopened", "edited", "synchronize"]
workflow_dispatch:
jobs:
cancel_previous-v2:
-
runs-on: ubuntu-latest
steps:
- uses: styfle/cancel-workflow-action@0.9.1
@@ -29,8 +28,8 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v3
with:
- distribution: 'temurin'
- java-version: '11'
+ distribution: "temurin"
+ java-version: "17"
- name: Setup Ruby
uses: ruby/setup-ruby@v1
@@ -38,8 +37,8 @@ jobs:
- name: Set Node 16
uses: actions/setup-node@v3
with:
- node-version-file: '.nvmrc'
- cache: 'npm'
+ node-version-file: ".nvmrc"
+ cache: "npm"
- name: Install node_modules
run: |
@@ -69,4 +68,4 @@ jobs:
npm run test
- name: Android Test Report
uses: asadmansr/android-test-report-action@v1.2.0
- if: ${{ always() }}
\ No newline at end of file
+ if: ${{ always() }}
diff --git a/.github/workflows/draft_new_release-v2.yml b/.github/workflows/draft_new_release-v2.yml
index f36c52675..edd0be8a7 100644
--- a/.github/workflows/draft_new_release-v2.yml
+++ b/.github/workflows/draft_new_release-v2.yml
@@ -92,4 +92,4 @@ jobs:
github_token: ${{ secrets.PAT }}
pr_title: 'chore(release): pulling ${{ steps.create-release.outputs.branch_name }} into master-v2'
pr_body: ':crown: *An automated PR*'
- pr_reviewer: 'itsdebs'
\ No newline at end of file
+ pr_reviewer: '@rudderlabs/sdk-android'
\ No newline at end of file
diff --git a/.github/workflows/publish-new-github-release-v2.yml b/.github/workflows/publish-new-github-release-v2.yml
index 88f05a193..77e51fce2 100644
--- a/.github/workflows/publish-new-github-release-v2.yml
+++ b/.github/workflows/publish-new-github-release-v2.yml
@@ -77,7 +77,7 @@ jobs:
github_token: ${{ secrets.PAT }}
pr_title: 'chore(release): pulling master-v2 into develop-v2 post release v${{ steps.extract-version-v2.outputs.release_version }}'
pr_body: ':crown: *An automated PR*'
- pr_reviewer: 'itsdebs'
+ pr_reviewer: '@rudderlabs/sdk-android'
- name: Delete hotfix release branch v2
uses: koj-co/delete-merged-action@master
diff --git a/.gitignore b/.gitignore
index fa8a72116..2f2129949 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,6 @@ local.properties
node_modules
/.idea/
*/build
+
+# Local Rudderstack file properties
+rudderstack.properties
diff --git a/CODEOWNERS b/CODEOWNERS
index 55364db51..dfe2e63fd 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1 +1 @@
-* @pallabmaiti @itsdebs
\ No newline at end of file
+* @rudderlabs/sdk-android
\ No newline at end of file
diff --git a/app/.gitignore b/android/.gitignore
similarity index 100%
rename from app/.gitignore
rename to android/.gitignore
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 000000000..c0ce2f3b6
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,88 @@
+plugins {
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+
+ namespace = "com.rudderstack.android"
+
+ compileSdk = RudderstackBuildConfig.Android.COMPILE_SDK
+
+ buildFeatures {
+ buildFeatures {
+ buildConfig = true
+ }
+ }
+ defaultConfig {
+ minSdk = RudderstackBuildConfig.Android.MIN_SDK
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ buildConfigField(
+ "String",
+ "LIBRARY_VERSION_NAME",
+ RudderstackBuildConfig.Version.VERSION_NAME
+ )
+ }
+
+ buildTypes {
+ named("release") {
+ isMinifyEnabled = false
+ setProguardFiles(
+ listOf(
+ getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
+ )
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION
+ targetCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION
+ }
+ kotlinOptions {
+ jvmTarget = RudderstackBuildConfig.Build.JVM_TARGET
+ javaParameters = true
+ }
+ testOptions {
+ unitTests {
+ this.isIncludeAndroidResources = true
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.android.core.ktx)
+ api(project(":core"))
+ api(project(":repository"))
+
+ compileOnly(project(":jacksonrudderadapter"))
+ compileOnly(project(":gsonrudderadapter"))
+ compileOnly(project(":moshirudderadapter"))
+ compileOnly(project(":rudderjsonadapter"))
+ compileOnly(libs.work)
+ compileOnly(libs.work.multiprocess)
+
+ testImplementation(project(":jacksonrudderadapter"))
+ testImplementation(project(":gsonrudderadapter"))
+ testImplementation(project(":moshirudderadapter"))
+ testImplementation(project(":rudderjsonadapter"))
+ testImplementation(project(":libs:test-common"))
+ testImplementation(libs.android.x.test)
+ testImplementation(libs.android.x.testrules)
+ testImplementation(libs.android.x.test.ext.junitktx)
+ testImplementation(libs.awaitility)
+ testImplementation(libs.hamcrest)
+ testImplementation(libs.junit)
+ testImplementation(libs.mockito.core)
+ testImplementation(libs.mockito.kotlin)
+ testImplementation(libs.mockk)
+ testImplementation(libs.mockk.agent)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.work.test)
+
+ androidTestImplementation(libs.android.x.test.ext.junitktx)
+}
+
+apply(from = "${project.projectDir.parentFile}/gradle/artifacts-aar.gradle")
+apply(from = "${project.projectDir.parentFile}/gradle/mvn-publish.gradle")
+apply(from = "${project.projectDir.parentFile}/gradle/codecov.gradle")
diff --git a/android/consumer-rules.pro b/android/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/android/gradle.properties b/android/gradle.properties
index e6e099b42..776df7697 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,4 +1,5 @@
POM_ARTIFACT_ID=android
+GROUP=com.rudderstack.android.sdk
POM_PACKAGING=aar
android.useAndroidX=true
android.enableJetifier=true
diff --git a/app/proguard-rules.pro b/android/proguard-rules.pro
similarity index 100%
rename from app/proguard-rules.pro
rename to android/proguard-rules.pro
diff --git a/android/project.json b/android/project.json
index 6dd90a1c0..01ec24022 100644
--- a/android/project.json
+++ b/android/project.json
@@ -6,6 +6,16 @@
"targets": {
"build": {
"executor": "@jnxplus/nx-gradle:build",
+ "dependsOn": [
+ {
+ "projects": [
+ "core",
+ "repository",
+ "rudderreporter"
+ ],
+ "target": "build"
+ }
+ ],
"outputs": [
"{projectRoot}/android/build"
]
diff --git a/app/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
similarity index 66%
rename from app/src/main/AndroidManifest.xml
rename to android/src/main/AndroidManifest.xml
index d55dee4fb..885f61da3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
-
-
\ No newline at end of file
diff --git a/android/src/main/java/com/rudderstack/android/AnalyticsRegistry.kt b/android/src/main/java/com/rudderstack/android/AnalyticsRegistry.kt
new file mode 100644
index 000000000..83eceb781
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/AnalyticsRegistry.kt
@@ -0,0 +1,59 @@
+package com.rudderstack.android
+
+import androidx.annotation.VisibleForTesting
+import com.rudderstack.core.utilities.FutureUse
+import com.rudderstack.core.Analytics
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * AnalyticsRegistry is a singleton object responsible for managing and providing instances of the Analytics class.
+ * It maintains a mapping between write keys and Analytics instances using concurrent hash maps.
+ *
+ * The class provides methods for registering new Analytics instances with unique write keys,
+ * as well as retrieving instances based on write keys.
+ *
+ * Usage:
+ * - To register a new Analytics instance, use the [register] method, providing a write key and the Analytics instance.
+ * - To retrieve an Analytics instance, use the [getInstance] method, passing the write key.
+ *
+ * Note: The class is marked as internal, indicating that it is intended for use within the same module and should not be accessed
+ * from outside the module.
+ */
+
+@FutureUse("This class will be utilized when multiple instances are implemented.")
+internal object AnalyticsRegistry {
+
+ private val writeKeyToInstance: ConcurrentHashMap = ConcurrentHashMap()
+
+ /**
+ * Registers a new Analytics instance with the provided write key and Analytics instance.
+ * If an instance with the same write key already exists, it will not be overwritten.
+ *
+ * @param writeKey The unique identifier associated with the Analytics instance.
+ * @param analytics The Analytics instance to be registered.
+ */
+ @JvmStatic
+ fun register(writeKey: String, analytics: Analytics) {
+ writeKeyToInstance.putIfAbsent(writeKey, analytics)
+ }
+
+ /**
+ * Retrieves an Analytics instance based on the provided write key.
+ *
+ * @param writeKey The write key associated with the desired Analytics instance.
+ * @return The Analytics instance if found, otherwise null.
+ */
+ @JvmStatic
+ fun getInstance(writeKey: String): Analytics? {
+ return writeKeyToInstance[writeKey]
+ }
+
+ /**
+ * Clears the mapping of write keys to Analytics instances.
+ * Note: This method is intended for use in testing scenarios only.
+ */
+ @VisibleForTesting
+ fun clear() {
+ writeKeyToInstance.clear()
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/rudderstack/android/AndroidUtils.kt b/android/src/main/java/com/rudderstack/android/AndroidUtils.kt
new file mode 100644
index 000000000..a97631a04
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/AndroidUtils.kt
@@ -0,0 +1,141 @@
+package com.rudderstack.android
+
+import android.app.Application
+import android.app.UiModeManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.os.Build
+import android.provider.Settings
+import android.text.TextUtils
+import android.util.Base64
+import android.util.DisplayMetrics
+import android.view.WindowManager
+import com.rudderstack.android.models.RudderApp
+import com.rudderstack.android.models.RudderDeviceInfo
+import com.rudderstack.android.models.RudderScreenInfo
+import com.rudderstack.core.Base64Generator
+import java.io.UnsupportedEncodingException
+import java.util.*
+
+internal object AndroidUtils {
+ fun generateAnonymousId(isCollectDeviceId: Boolean, application: Application): String {
+ return if (!isCollectDeviceId) UUID.randomUUID().toString() else getDeviceId(application)
+ }
+
+ internal fun getDeviceId(application: Application): String = run {
+
+ val androidId =
+ Settings.System.getString(application.contentResolver, Settings.Secure.ANDROID_ID)
+ if (!TextUtils.isEmpty(androidId) && "9774d56d682e549c" != androidId && "unknown" != androidId && "000000000000000" != androidId) {
+ return androidId
+ }
+ androidId ?: UUID.randomUUID().toString()
+ }
+
+ fun getWriteKeyFromStrings(context: Context): String? {
+ val id = context.resources.getIdentifier(
+ context.packageName, "string", "rudder_write_key"
+ )
+ return if (id != 0) {
+ context.resources.getString(id)
+ } else {
+ null
+ }
+ }
+
+ internal fun getUTF8Length(message: String): Int {
+ return try {
+ message.toByteArray(charset("UTF-8")).size
+ } catch (ex: UnsupportedEncodingException) {
+// RudderLogger.logError(ex);
+ -1
+ }
+ }
+
+ internal fun getUTF8Length(message: StringBuilder): Int {
+ return getUTF8Length(message.toString())
+ }
+
+ internal fun isOnClassPath(className: String): Boolean {
+ return try {
+ Class.forName(className)
+ true
+ } catch (e: ClassNotFoundException) {
+ false
+ }
+ }
+
+ /**
+ * Returns whether the app is running on a TV device.
+ *
+ * @param context Any context.
+ * @return Whether the app is running on a TV device.
+ */
+ internal fun Context.isTv(): Boolean {
+ val uiModeManager =
+ applicationContext.getSystemService(Context.UI_MODE_SERVICE) as? UiModeManager
+ return (uiModeManager != null && uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION)
+ }
+
+ internal fun defaultBase64Generator() = Base64Generator {
+ Base64.encodeToString(
+ String.format(Locale.US, "%s:", it).toByteArray(Charsets.UTF_8), Base64.NO_WRAP
+ )
+ }
+
+ private fun Application.generateDeviceInfo(
+ advertisingId: String?,
+ deviceToken: String,
+ collectDeviceId: Boolean
+ ): RudderDeviceInfo {
+ val deviceId = if (collectDeviceId) getDeviceId(this) else null
+ return RudderDeviceInfo(
+ deviceId = deviceId,
+ manufacturer = Build.MANUFACTURER,
+ model = Build.MODEL,
+ name = Build.DEVICE,
+ token = deviceToken,
+ isAdTrackingEnabled = advertisingId != null
+ ).also {
+ if (advertisingId != null) it.advertisingId = advertisingId
+ }
+ }
+
+
+ private val Application.rudderApp: RudderApp?
+ get() = try {
+ val packageName: String = this.packageName
+ val packageManager: PackageManager = packageManager
+ val packageInfo = packageManager.getPackageInfo(packageName, 0)
+ val build =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode.toString() else packageInfo.versionCode.toString()
+ val name = packageInfo.applicationInfo.loadLabel(packageManager).toString()
+ val version = packageInfo.versionName
+ RudderApp(
+ name = name, version = version, build = build, nameSpace = packageName
+ )
+ } catch (ex: PackageManager.NameNotFoundException) {
+// ReportManager.reportError(ex)
+// RudderLogger.logError(ex.cause)
+ null
+ }
+ private val Application.screen: RudderScreenInfo?
+ get() = run {
+ val manager = getSystemService(Context.WINDOW_SERVICE) as? WindowManager
+ return@run if (manager == null) {
+ null
+ } else {
+ val display = manager.defaultDisplay
+ val displayMetrics = DisplayMetrics()
+ display.getMetrics(displayMetrics)
+ RudderScreenInfo(
+ displayMetrics.densityDpi,
+ displayMetrics.widthPixels,
+ displayMetrics.heightPixels
+ )
+ }
+
+ }
+
+}
diff --git a/android/src/main/java/com/rudderstack/android/ConfigurationAndroid.kt b/android/src/main/java/com/rudderstack/android/ConfigurationAndroid.kt
new file mode 100644
index 000000000..9cc45848a
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/ConfigurationAndroid.kt
@@ -0,0 +1,95 @@
+/*
+ * Creator: Debanjan Chatterjee on 28/11/23, 5:37 pm Last modified: 28/11/23, 10:00 am
+ * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain a
+ * copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.rudderstack.android
+
+import android.app.Application
+import androidx.annotation.RestrictTo
+import com.rudderstack.android.internal.AndroidLogger
+import com.rudderstack.core.Base64Generator
+import com.rudderstack.core.Configuration
+import com.rudderstack.core.Logger
+import com.rudderstack.core.RetryStrategy
+import com.rudderstack.core.RudderOption
+import com.rudderstack.rudderjsonadapter.JsonAdapter
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+/**
+ * Data class representing the Android-specific configuration for the RudderStack analytics SDK.
+ *
+ */
+data class ConfigurationAndroid @JvmOverloads constructor(
+ val application: Application,
+ val anonymousId: String? = null,
+ val trackLifecycleEvents: Boolean = TRACK_LIFECYCLE_EVENTS,
+ val recordScreenViews: Boolean = RECORD_SCREEN_VIEWS,
+ val isPeriodicFlushEnabled: Boolean = IS_PERIODIC_FLUSH_ENABLED,
+ val autoCollectAdvertId: Boolean = AUTO_COLLECT_ADVERT_ID,
+ val multiProcessEnabled: Boolean = MULTI_PROCESS_ENABLED,
+ val defaultProcessName: String? = DEFAULT_PROCESS_NAME,
+ val advertisingId: String? = null,
+ val deviceToken: String? = null,
+ val logLevel: Logger.LogLevel = Logger.DEFAULT_LOG_LEVEL,
+ val collectDeviceId: Boolean = COLLECT_DEVICE_ID,
+ val advertisingIdFetchExecutor: ExecutorService = Executors.newCachedThreadPool(),
+ val trackAutoSession: Boolean = AUTO_SESSION_TRACKING,
+ val sessionTimeoutMillis: Long = SESSION_TIMEOUT,
+ override val jsonAdapter: JsonAdapter,
+ override val options: RudderOption = RudderOption(),
+ override val flushQueueSize: Int = DEFAULT_FLUSH_QUEUE_SIZE,
+ override val maxFlushInterval: Long = DEFAULT_MAX_FLUSH_INTERVAL,
+ override val shouldVerifySdk: Boolean = SHOULD_VERIFY_SDK,
+ override val gzipEnabled: Boolean = GZIP_ENABLED,
+ override val sdkVerifyRetryStrategy: RetryStrategy = RetryStrategy.exponential(),
+ override val dataPlaneUrl: String = DEFAULT_ANDROID_DATAPLANE_URL,
+ override val controlPlaneUrl: String = DEFAULT_ANDROID_CONTROLPLANE_URL,
+ override val analyticsExecutor: ExecutorService = Executors.newSingleThreadExecutor(),
+ override val networkExecutor: ExecutorService = Executors.newCachedThreadPool(),
+ override val base64Generator: Base64Generator = AndroidUtils.defaultBase64Generator(),
+) : Configuration (
+ jsonAdapter = jsonAdapter,
+ options = options,
+ flushQueueSize = flushQueueSize,
+ maxFlushInterval = maxFlushInterval,
+ shouldVerifySdk = shouldVerifySdk,
+ gzipEnabled = gzipEnabled,
+ sdkVerifyRetryStrategy = sdkVerifyRetryStrategy,
+ dataPlaneUrl = dataPlaneUrl,
+ controlPlaneUrl = controlPlaneUrl,
+ logger = AndroidLogger(logLevel),
+ analyticsExecutor = analyticsExecutor,
+ networkExecutor = networkExecutor,
+ base64Generator = base64Generator,
+) {
+ companion object {
+ const val COLLECT_DEVICE_ID: Boolean = true
+ const val DEFAULT_ANDROID_DATAPLANE_URL = "https://hosted.rudderlabs.com"
+ const val DEFAULT_ANDROID_CONTROLPLANE_URL = "https://api.rudderlabs.com"
+ const val GZIP_ENABLED: Boolean = true
+ const val SHOULD_VERIFY_SDK: Boolean = true
+ const val TRACK_LIFECYCLE_EVENTS = true
+ const val RECORD_SCREEN_VIEWS = false
+ const val IS_PERIODIC_FLUSH_ENABLED = false
+ const val AUTO_COLLECT_ADVERT_ID = false
+ const val MULTI_PROCESS_ENABLED = false
+ @JvmField
+ var DEFAULT_PROCESS_NAME: String? = null
+ const val USE_CONTENT_PROVIDER = false
+ const val DEFAULT_FLUSH_QUEUE_SIZE = 30
+ const val DEFAULT_MAX_FLUSH_INTERVAL = 10 * 1000L
+ const val SESSION_TIMEOUT: Long = 300000
+ const val AUTO_SESSION_TRACKING = true
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/LifecycleListenerPlugin.kt b/android/src/main/java/com/rudderstack/android/LifecycleListenerPlugin.kt
new file mode 100644
index 000000000..3e4913c01
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/LifecycleListenerPlugin.kt
@@ -0,0 +1,82 @@
+/*
+ * Creator: Debanjan Chatterjee on 21/11/23, 12:13 pm Last modified: 21/11/23, 12:13 pm
+ * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain a
+ * copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.rudderstack.android
+
+import android.app.Activity
+import com.rudderstack.core.InfrastructurePlugin
+import com.rudderstack.core.Plugin
+
+/**
+ * Any [InfrastructurePlugin] or [Plugin] that wants to listen to lifecycle events should implement
+ * this interface.
+ *
+ * onAppForegrounded() and onAppBackgrounded() are called when the app goes to foreground and background respectively.
+ * These methods are called only if [ConfigurationAndroid.trackLifecycleEvents] is set to true.
+ *
+ * onActivityStarted(), onActivityStopped() and screenChange() are called when an activity is
+ * started and stopped respectively.
+ * These methods are called only if both [ConfigurationAndroid.trackLifecycleEvents] and
+ * [ConfigurationAndroid.recordScreenViews] are set to true.
+ *
+ * Getting callbacks is subject to availability of Infrastructure plugins that are designed
+ * to listen to lifecycle events.
+ *
+ */
+interface LifecycleListenerPlugin {
+ /**
+ * Called when at least one activity is started
+ *
+ */
+ fun onAppForegrounded(){}
+ /**
+ * Called when all activities are stopped
+ *
+ */
+ fun onAppBackgrounded(){}
+
+ /**
+ * Called when an activity is started
+ *
+ * @param activityName The name of the activity that is started
+ */
+ fun onActivityStarted(activityName: String){}
+ /**
+ * Called when the current activity is changed
+ * Will be null if the app is backgrounded
+ * If using this method, make sure to make it thread safe.
+ * Storing the activity as a strong reference might lead to memory leaks.
+ * If you're only interested in the name of the activity, use [onActivityStarted] instead.
+ *
+ * @param activity The activity that is started
+ */
+ fun setCurrentActivity(activity: Activity?){}
+
+ /**
+ * Called when an activity is stopped
+ *
+ * @param activityName The name of the activity that is stopped
+ */
+ fun onActivityStopped(activityName: String){}
+
+ /**
+ * Called when the screen changes.
+ * This is mostly associated to fragment or compose navigation.
+ *
+ *
+ * @param name Name of the compose route or fragment label
+ * @param arguments The arguments passed to the to the composable or fragment in question.
+ */
+ fun onScreenChange(name: String, arguments: Map?){}
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/rudderstack/android/RudderAnalytics.kt b/android/src/main/java/com/rudderstack/android/RudderAnalytics.kt
new file mode 100644
index 000000000..39fc441de
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/RudderAnalytics.kt
@@ -0,0 +1,63 @@
+package com.rudderstack.android
+
+import com.rudderstack.android.storage.AndroidStorageImpl
+import com.rudderstack.android.utilities.onShutdown
+import com.rudderstack.android.utilities.startup
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.ConfigDownloadService
+import com.rudderstack.core.DataUploadService
+import com.rudderstack.core.Storage
+
+/**
+ * Singleton class for RudderAnalytics to manage the analytics instance.
+ *
+ * This class ensures that only one instance of the Analytics object is created.
+ */
+class RudderAnalytics private constructor() {
+
+ companion object {
+
+ @Volatile
+ private var instance: Analytics? = null
+
+ /**
+ * Returns the singleton instance of [Analytics], creating it if necessary.
+ *
+ * @param writeKey The write key for authentication.
+ * @param configuration The configuration settings for Android.
+ * @param storage The storage implementation for storing data. Defaults to [AndroidStorageImpl].
+ * @param dataUploadService The service responsible for uploading data. Defaults to null.
+ * @param configDownloadService The service responsible for downloading configuration. Defaults to null.
+ * @param initializationListener A listener for initialization events. Defaults to null.
+ * @return The singleton instance of [Analytics].
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun getInstance(
+ writeKey: String,
+ configuration: ConfigurationAndroid,
+ storage: Storage = AndroidStorageImpl(
+ configuration.application,
+ writeKey = writeKey,
+ useContentProvider = ConfigurationAndroid.USE_CONTENT_PROVIDER
+ ),
+ dataUploadService: DataUploadService? = null,
+ configDownloadService: ConfigDownloadService? = null,
+ initializationListener: ((success: Boolean, message: String?) -> Unit)? = null,
+ ) = instance ?: synchronized(this) {
+ instance ?: Analytics(
+ writeKey = writeKey,
+ configuration = configuration,
+ dataUploadService = dataUploadService,
+ configDownloadService = configDownloadService,
+ storage = storage,
+ initializationListener = initializationListener,
+ shutdownHook = { onShutdown() }
+ ).apply {
+ startup()
+ }.also {
+ instance = it
+ }
+ }
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/compat/ConfigurationAndroidBuilder.java b/android/src/main/java/com/rudderstack/android/compat/ConfigurationAndroidBuilder.java
new file mode 100644
index 000000000..38e374785
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/compat/ConfigurationAndroidBuilder.java
@@ -0,0 +1,118 @@
+package com.rudderstack.android.compat;
+
+import android.app.Application;
+
+import com.rudderstack.android.AndroidUtils;
+import com.rudderstack.android.ConfigurationAndroid;
+import com.rudderstack.core.compat.ConfigurationBuilder;
+import com.rudderstack.rudderjsonadapter.JsonAdapter;
+import com.rudderstack.core.Logger;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+//Java compatible Builder for [ConfigurationAndroid]
+public class ConfigurationAndroidBuilder extends ConfigurationBuilder {
+ private final Application application ;
+ private String anonymousId;
+ private Boolean trackLifecycleEvents = ConfigurationAndroid.TRACK_LIFECYCLE_EVENTS;
+ private Boolean recordScreenViews = ConfigurationAndroid.RECORD_SCREEN_VIEWS;
+ private Boolean isPeriodicFlushEnabled = ConfigurationAndroid.IS_PERIODIC_FLUSH_ENABLED;
+ private Boolean autoCollectAdvertId = ConfigurationAndroid.AUTO_COLLECT_ADVERT_ID;
+ private Boolean multiProcessEnabled = ConfigurationAndroid.MULTI_PROCESS_ENABLED;
+ private String defaultProcessName= ConfigurationAndroid.DEFAULT_PROCESS_NAME;
+ private String advertisingId = null;
+ private String deviceToken = null;
+ private boolean collectDeviceId = ConfigurationAndroid.COLLECT_DEVICE_ID;
+ private ExecutorService advertisingIdFetchExecutor = Executors.newCachedThreadPool();
+ private boolean trackAutoSession = ConfigurationAndroid.AUTO_SESSION_TRACKING;
+ private long sessionTimeoutMillis = ConfigurationAndroid.SESSION_TIMEOUT;
+ private Logger.LogLevel logLevel = Logger.DEFAULT_LOG_LEVEL;
+
+ public ConfigurationAndroidBuilder(Application application, JsonAdapter jsonAdapter) {
+ super(jsonAdapter);
+ this.application = application;
+ anonymousId = AndroidUtils.INSTANCE.generateAnonymousId(collectDeviceId, application);
+ }
+ public ConfigurationBuilder withAnonymousId(String anonymousId) {
+ this.anonymousId = anonymousId;
+ return this;
+ }
+ public ConfigurationBuilder withTrackLifecycleEvents(Boolean trackLifecycleEvents) {
+ this.trackLifecycleEvents = trackLifecycleEvents;
+ return this;
+ }
+ public ConfigurationBuilder withRecordScreenViews(Boolean recordScreenViews) {
+ this.recordScreenViews = recordScreenViews;
+ return this;
+ }
+ public ConfigurationBuilder withIsPeriodicFlushEnabled(Boolean isPeriodicFlushEnabled) {
+ this.isPeriodicFlushEnabled = isPeriodicFlushEnabled;
+ return this;
+ }
+ public ConfigurationBuilder withAutoCollectAdvertId(Boolean autoCollectAdvertId) {
+ this.autoCollectAdvertId = autoCollectAdvertId;
+ return this;
+ }
+ public ConfigurationBuilder withMultiProcessEnabled(Boolean multiProcessEnabled) {
+ this.multiProcessEnabled = multiProcessEnabled;
+ return this;
+ }
+ public ConfigurationBuilder withDefaultProcessName(String defaultProcessName) {
+ this.defaultProcessName = defaultProcessName;
+ return this;
+ }
+ public ConfigurationBuilder withAdvertisingId(String advertisingId) {
+ this.advertisingId = advertisingId;
+ return this;
+ }
+ public ConfigurationBuilder withDeviceToken(String deviceToken) {
+ this.deviceToken = deviceToken;
+ return this;
+ }
+ public ConfigurationBuilder withAdvertisingIdFetchExecutor(ExecutorService advertisingIdFetchExecutor) {
+ this.advertisingIdFetchExecutor = advertisingIdFetchExecutor;
+ return this;
+ }
+ public ConfigurationBuilder withTrackAutoSession(boolean trackAutoSession) {
+ this.trackAutoSession = trackAutoSession;
+ return this;
+ }
+ public ConfigurationBuilder withSessionTimeoutMillis(long sessionTimeoutMillis) {
+ this.sessionTimeoutMillis = sessionTimeoutMillis;
+ return this;
+ }
+
+ public ConfigurationBuilder withLogLevel(Logger.LogLevel logLevel) {
+ this.logLevel = logLevel;
+ return this;
+ }
+
+ public ConfigurationBuilder withCollectDeviceId(boolean collectDeviceId) {
+ this.collectDeviceId = collectDeviceId;
+ return this;
+ }
+
+ @Override
+ public ConfigurationAndroid build() {
+ return new ConfigurationAndroid(
+ application,
+ anonymousId,
+ trackLifecycleEvents,
+ recordScreenViews,
+ isPeriodicFlushEnabled,
+ autoCollectAdvertId,
+ multiProcessEnabled,
+ defaultProcessName,
+ advertisingId,
+ deviceToken,
+ logLevel,
+ collectDeviceId,
+ advertisingIdFetchExecutor,
+ trackAutoSession,
+ sessionTimeoutMillis,
+ jsonAdapter
+ );
+ }
+
+}
diff --git a/android/src/main/java/com/rudderstack/android/compat/RudderAnalyticsBuilderCompat.java b/android/src/main/java/com/rudderstack/android/compat/RudderAnalyticsBuilderCompat.java
new file mode 100644
index 000000000..8afc4b279
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/compat/RudderAnalyticsBuilderCompat.java
@@ -0,0 +1,75 @@
+package com.rudderstack.android.compat;
+
+
+import androidx.annotation.NonNull;
+
+import com.rudderstack.android.ConfigurationAndroid;
+import com.rudderstack.android.RudderAnalytics;
+import com.rudderstack.android.storage.AndroidStorage;
+import com.rudderstack.android.storage.AndroidStorageImpl;
+import com.rudderstack.core.Analytics;
+import com.rudderstack.core.ConfigDownloadService;
+import com.rudderstack.core.DataUploadService;
+
+import java.util.concurrent.Executors;
+
+import kotlin.Unit;
+
+/**
+ * To be used by java projects
+ */
+public final class RudderAnalyticsBuilderCompat {
+
+ private @NonNull String writeKey;
+ private @NonNull ConfigurationAndroid configuration;
+ private DataUploadService dataUploadService = null;
+ private ConfigDownloadService configDownloadService = null;
+ private InitializationListener initializationListener = null;
+ private AndroidStorage storage = new AndroidStorageImpl(configuration.getApplication(),
+ ConfigurationAndroid.USE_CONTENT_PROVIDER,
+ writeKey,
+ Executors.newSingleThreadExecutor());
+
+ public RudderAnalyticsBuilderCompat(@NonNull String writeKey, @NonNull ConfigurationAndroid configuration) {
+ this.writeKey = writeKey;
+ this.configuration = configuration;
+ }
+ public RudderAnalyticsBuilderCompat withDataUploadService(DataUploadService dataUploadService) {
+ this.dataUploadService = dataUploadService;
+ return this;
+ }
+ public RudderAnalyticsBuilderCompat withConfigDownloadService(ConfigDownloadService configDownloadService) {
+ this.configDownloadService = configDownloadService;
+ return this;
+ }
+ public RudderAnalyticsBuilderCompat withInitializationListener(InitializationListener initializationListener) {
+ this.initializationListener = initializationListener;
+ return this;
+ }
+ public Analytics build() {
+
+ return RudderAnalytics.getInstance(
+ writeKey,
+ configuration,
+ storage,
+ dataUploadService,
+ configDownloadService,
+ (success, message) -> {
+ if (initializationListener != null) {
+ initializationListener.onInitialized(success, message);
+ }
+ return Unit.INSTANCE;
+ }
+ );
+ }
+
+ public interface InitializationListener {
+ void onInitialized(boolean success, String message);
+ }
+
+ public RudderAnalyticsBuilderCompat withStorage(AndroidStorage storage) {
+ this.storage = storage;
+ return this;
+ }
+
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/AndroidLogger.kt b/android/src/main/java/com/rudderstack/android/internal/AndroidLogger.kt
new file mode 100644
index 000000000..0df5f0e34
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/AndroidLogger.kt
@@ -0,0 +1,43 @@
+package com.rudderstack.android.internal
+
+import android.util.Log
+import com.rudderstack.core.Logger
+
+/**
+ * Logger implementation specifically for android.
+ *
+ */
+class AndroidLogger(
+ initialLogLevel: Logger.LogLevel = Logger.DEFAULT_LOG_LEVEL
+) : Logger {
+ private var logLevel: Logger.LogLevel = initialLogLevel
+ @Synchronized set
+ @Synchronized get
+
+ override fun activate(level: Logger.LogLevel) {
+ logLevel = level
+ }
+
+ override fun info(tag: String, log: String) {
+ if (Logger.LogLevel.INFO >= logLevel)
+ Log.i(tag, log)
+ }
+
+ override fun debug(tag: String, log: String) {
+ if (Logger.LogLevel.DEBUG >= logLevel)
+ Log.d(tag, log)
+ }
+
+ override fun warn(tag: String, log: String) {
+ if (Logger.LogLevel.WARN >= logLevel)
+ Log.w(tag, log)
+ }
+
+ override fun error(tag: String, log: String, throwable: Throwable?) {
+ if (Logger.LogLevel.ERROR >= logLevel)
+ Log.e(tag, log, throwable)
+ }
+
+ override val level: Logger.LogLevel
+ get() = logLevel
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/DmtUtils.kt b/android/src/main/java/com/rudderstack/android/internal/DmtUtils.kt
new file mode 100644
index 000000000..7c4430cd4
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/DmtUtils.kt
@@ -0,0 +1,29 @@
+package com.rudderstack.android.internal
+
+
+/**
+ * [STATUS_NEW] should be the initial status.
+ * Any further operations should be masked with appropriate status
+ * for eg:
+ * for setting cloud mode done the way should be
+ ```
+ var status = STATUS_NEW
+ status = status maskWith STATUS_CLOUD_MODE_DONE
+ ```
+ */
+internal const val STATUS_NEW = 0
+internal const val STATUS_CLOUD_MODE_DONE = 2
+internal const val STATUS_DEVICE_MODE_DONE = 1
+
+infix fun Int.maskWith(status: Int) : Int{
+ return this or (1 shl status)
+}
+infix fun Int.unmaskWith(status: Int) : Int{
+ return this and (1 shl status).inv()
+}
+fun Int.isDeviceModeDone() : Boolean{
+ return this and (1 shl STATUS_DEVICE_MODE_DONE) != 0
+}
+fun Int.isCloudModeDone() : Boolean{
+ return this and (1 shl STATUS_CLOUD_MODE_DONE) != 0
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/RudderPreferenceManager.kt b/android/src/main/java/com/rudderstack/android/internal/RudderPreferenceManager.kt
new file mode 100644
index 000000000..36b059874
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/RudderPreferenceManager.kt
@@ -0,0 +1,237 @@
+/*
+ * Creator: Debanjan Chatterjee on 04/07/22, 4:45 PM Last modified: 04/07/22, 4:45 PM
+ * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain a
+ * copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.rudderstack.android.internal
+
+import android.app.Application
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.content.SharedPreferences
+import android.os.Build
+import java.io.File
+
+// keys
+private const val RUDDER_PREFS = "rl_prefs"
+private const val RUDDER_SERVER_CONFIG_LAST_UPDATE_KEY = "rl_server_last_updated"
+private const val RUDDER_TRAITS_KEY = "rl_traits"
+private const val RUDDER_APPLICATION_INFO_KEY = "rl_application_info_key"
+private const val RUDDER_TRACK_AUTO_SESSION_KEY = "rl_track_auto_session_key"
+private const val RUDDER_EXTERNAL_ID_KEY = "rl_external_id"
+private const val RUDDER_OPT_STATUS_KEY = "rl_opt_status"
+private const val RUDDER_OPT_IN_TIME_KEY = "rl_opt_in_time"
+private const val RUDDER_OPT_OUT_TIME_KEY = "rl_opt_out_time"
+private const val RUDDER_ANONYMOUS_ID_KEY = "rl_anonymous_id_key"
+private const val RUDDER_USER_ID_KEY = "rl_user_id_key"
+private const val RUDDER_PERIODIC_WORK_REQUEST_ID_KEY = "rl_periodic_work_request_key"
+private const val RUDDER_SESSION_ID_KEY = "rl_session_id_key"
+private const val RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY =
+ "rl_last_event_timestamp_key"
+private const val RUDDER_ADVERTISING_ID_KEY = "rl_advertising_id_key"
+
+private const val RUDDER_APPLICATION_VERSION_KEY = "rl_application_version_key"
+private const val RUDDER_APPLICATION_BUILD_KEY = "rl_application_build_key"
+internal class RudderPreferenceManager(private val application: Application,
+ private val writeKey: String) {
+
+ private val String.key: String
+ get() = "$this-$writeKey"
+
+ private var preferences: SharedPreferences =
+ application.getSharedPreferences(RUDDER_PREFS.key, Context.MODE_PRIVATE)
+ private var preferencesV1: SharedPreferences =
+ application.getSharedPreferences(RUDDER_PREFS, Context.MODE_PRIVATE)
+ val lastUpdatedTime: Long
+ get() = preferences.getLong(RUDDER_SERVER_CONFIG_LAST_UPDATE_KEY.key,
+ -1)
+
+ fun updateLastUpdatedTime() {
+ preferences.edit().putLong(RUDDER_SERVER_CONFIG_LAST_UPDATE_KEY.key, System
+ .currentTimeMillis())
+ .apply()
+ }
+
+ val traits: String?
+ get() = preferences.getString(RUDDER_TRAITS_KEY.key, null)
+
+ fun saveTraits(traitsJson: String?) {
+ preferences.edit().putString(RUDDER_TRAITS_KEY.key, traitsJson).apply()
+ }
+ val advertisingId: String?
+ get() = preferences.getString(RUDDER_ADVERTISING_ID_KEY.key, null)
+
+ fun saveAdvertisingId(advertisingId: String?) {
+ preferences.edit().putString(RUDDER_ADVERTISING_ID_KEY.key, advertisingId).apply()
+ }
+
+ val buildVersionCode: Int
+ get() = preferences.getInt(RUDDER_APPLICATION_INFO_KEY.key, -1)
+
+ fun saveBuildVersionCode(versionCode: Int) {
+ preferences.edit().putInt(RUDDER_APPLICATION_INFO_KEY.key, versionCode).apply()
+ }
+
+ val externalIds: String?
+ get() = preferences.getString(RUDDER_EXTERNAL_ID_KEY.key, null)
+
+ fun saveExternalIds(externalIdsJson: String?) {
+ preferences.edit().putString(RUDDER_EXTERNAL_ID_KEY.key, externalIdsJson).apply()
+ }
+
+ fun clearExternalIds() {
+ preferences.edit().remove(RUDDER_EXTERNAL_ID_KEY.key).apply()
+ }
+
+ fun saveAnonymousId(anonymousId: String?) {
+ preferences.edit().putString(RUDDER_ANONYMOUS_ID_KEY.key, anonymousId).apply()
+ }
+ fun saveSessionId(sessionId: Long?) {
+ preferences.edit().putLong(RUDDER_SESSION_ID_KEY.key, sessionId?:-1L).apply()
+ }
+ fun clearSessionId() {
+ preferences.edit().remove(RUDDER_SESSION_ID_KEY.key).apply()
+ }
+ val sessionId: Long
+ get() = preferences.getLong(RUDDER_SESSION_ID_KEY.key, -1L)
+ fun saveLastActiveTimestamp(lastActiveTimestamp: Long?) {
+ preferences.edit().putLong(RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY.key, lastActiveTimestamp
+ ?: -1L).apply()
+ }
+ fun clearLastActiveTimestamp() {
+ preferences.edit().remove(RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY.key).apply()
+ }
+ val lastActiveTimestamp: Long
+ get() = preferences.getLong(RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY.key, -1L)
+
+ val anonymousId: String?
+ get() = preferences.getString(RUDDER_ANONYMOUS_ID_KEY.key, null)
+ fun saveUserId(userId: String?) {
+ preferences.edit().putString(RUDDER_USER_ID_KEY.key, userId).apply()
+ }
+ fun saveTrackAutoSession(trackAutoSession: Boolean) {
+ preferences.edit().putBoolean(RUDDER_TRACK_AUTO_SESSION_KEY.key, trackAutoSession).apply()
+ }
+
+ internal val trackAutoSession: Boolean
+ get() = preferences.getBoolean(RUDDER_TRACK_AUTO_SESSION_KEY, false)
+
+ val userId: String?
+ get() = preferences.getString(RUDDER_USER_ID_KEY.key, null)
+
+ fun updateOptInTime() {
+ preferences.edit().putLong(RUDDER_OPT_IN_TIME_KEY.key, System.currentTimeMillis()).apply()
+ }
+
+ fun updateOptOutTime() {
+ preferences.edit().putLong(RUDDER_OPT_OUT_TIME_KEY.key, System.currentTimeMillis()).apply()
+ }
+
+ val optInTime: Long
+ get() = preferences.getLong(RUDDER_OPT_IN_TIME_KEY.key, -1)
+ val optOutTime: Long
+ get() = preferences.getLong(RUDDER_OPT_OUT_TIME_KEY.key, -1)
+
+ fun savePeriodicWorkRequestId(periodicWorkRequestId: String?) {
+ preferences.edit().putString(RUDDER_PERIODIC_WORK_REQUEST_ID_KEY.key, periodicWorkRequestId)
+ .apply()
+ }
+
+ fun resetV1AnonymousId() {
+ preferencesV1.edit().remove(RUDDER_ANONYMOUS_ID_KEY).apply()
+ }
+ fun resetV1Traits() {
+ preferencesV1.edit().remove(RUDDER_TRAITS_KEY).apply()
+ }
+ fun resetV1ExternalIds() {
+ preferencesV1.edit().remove(RUDDER_EXTERNAL_ID_KEY).apply()
+ }
+ fun resetV1OptOut() {
+ preferencesV1.edit().remove(RUDDER_OPT_STATUS_KEY).apply()
+ }
+
+ val periodicWorkRequestId: String?
+ get() = preferences.getString(RUDDER_PERIODIC_WORK_REQUEST_ID_KEY.key, null)
+ val v1AnonymousId
+ get() = preferencesV1.getString(RUDDER_ANONYMOUS_ID_KEY, null)
+
+ internal val v1ExternalIdsJson: String?
+ get() = preferencesV1.getString(RUDDER_EXTERNAL_ID_KEY, null)
+
+ internal val v1Traits
+ get() = preferencesV1.getString(RUDDER_TRAITS_KEY, null)
+
+ internal val v1optOutStatus: Boolean
+ get() = preferencesV1.getBoolean(RUDDER_OPT_STATUS_KEY, false)
+
+ internal val v1LastActiveTimestamp: Long?
+ get() = preferencesV1.getLong(RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY, -1L).takeIf {
+ it > 0
+ }
+
+ internal val v1SessionId : Long
+ get() = preferencesV1.getLong(RUDDER_SESSION_ID_KEY, -1)
+ internal val v1Build: Int
+ get() = preferencesV1.getInt(RUDDER_APPLICATION_BUILD_KEY, -1)
+ internal val v1VersionName: String?
+ get() = preferencesV1.getString(RUDDER_APPLICATION_VERSION_KEY, null)
+ fun saveOptStatus(optStatus: Boolean) {
+ preferences.edit().putBoolean(RUDDER_OPT_STATUS_KEY.key, optStatus).apply()
+ }
+
+ val optStatus: Boolean
+ get() = preferences.getBoolean(RUDDER_OPT_STATUS_KEY.key, false)
+
+ fun saveVersionName(versionName: String) {
+ preferences.edit().putString(RUDDER_APPLICATION_VERSION_KEY.key, versionName).apply()
+ }
+
+ val versionName: String?
+ get() = preferences.getString(RUDDER_APPLICATION_VERSION_KEY.key, null)
+
+ fun saveBuild(build: Int) {
+ preferences.edit().putInt(RUDDER_APPLICATION_BUILD_KEY.key, build).apply()
+ }
+
+ val build: Int
+ get() = preferences.getInt(RUDDER_APPLICATION_BUILD_KEY.key, -1)
+
+ val v1AdvertisingId: String?
+ get() = preferencesV1.getString(RUDDER_ADVERTISING_ID_KEY, null)
+
+ fun resetV1AdvertisingId() {
+ preferencesV1.edit().remove(RUDDER_ADVERTISING_ID_KEY).apply()
+ }
+ fun resetV1SessionId() {
+ preferencesV1.edit().remove(RUDDER_SESSION_ID_KEY).apply()
+ }
+ fun resetV1LastActiveTimestamp() {
+ preferencesV1.edit().remove(RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY).apply()
+ }
+ fun resetV1VersionName(){
+ preferencesV1.edit().remove(RUDDER_APPLICATION_VERSION_KEY).apply()
+ }
+
+ fun resetV1Build() {
+ preferencesV1.edit().remove(RUDDER_APPLICATION_BUILD_KEY).apply()
+ }
+
+ fun deleteV1PreferencesFile(): Boolean{
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ return application.deleteSharedPreferences(RUDDER_PREFS)
+ } else {
+ application.getSharedPreferences(RUDDER_PREFS, MODE_PRIVATE).edit().clear().apply()
+ val dir = File(application.applicationInfo.dataDir, "shared_prefs")
+ return File(dir, "$RUDDER_PREFS.xml").delete()
+ }
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/extensions/UserSessionExtensions.kt b/android/src/main/java/com/rudderstack/android/internal/extensions/UserSessionExtensions.kt
new file mode 100644
index 000000000..fa6ad45d8
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/extensions/UserSessionExtensions.kt
@@ -0,0 +1,24 @@
+package com.rudderstack.android.internal.extensions
+
+import com.rudderstack.core.models.MessageContext
+import com.rudderstack.core.with
+
+private const val CONTEXT_SESSION_ID_KEY = "sessionId"
+private const val CONTEXT_SESSION_START_KEY = "sessionStart"
+
+internal fun MessageContext.withSessionId(sessionId: String): MessageContext {
+ return this.with(CONTEXT_SESSION_ID_KEY to sessionId)
+}
+
+internal fun MessageContext.withSessionStart(sessionStart: Boolean): MessageContext {
+ return this.with(CONTEXT_SESSION_START_KEY to sessionStart)
+}
+
+internal fun MessageContext.removeSessionContext(): MessageContext {
+ return this.minus(listOf(CONTEXT_SESSION_ID_KEY, CONTEXT_SESSION_START_KEY))
+}
+
+internal val MessageContext.sessionId: String?
+ get() = this[CONTEXT_SESSION_ID_KEY] as? String
+internal val MessageContext.sessionStart: Boolean?
+ get() = this[CONTEXT_SESSION_START_KEY] as? Boolean
diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/ActivityBroadcasterPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ActivityBroadcasterPlugin.kt
new file mode 100644
index 000000000..d574ed704
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ActivityBroadcasterPlugin.kt
@@ -0,0 +1,122 @@
+package com.rudderstack.android.internal.infrastructure
+
+import android.app.Activity
+import android.app.Application
+import android.os.Bundle
+import com.rudderstack.android.LifecycleListenerPlugin
+import com.rudderstack.android.utilities.currentConfigurationAndroid
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.InfrastructurePlugin
+import java.util.concurrent.atomic.AtomicInteger
+
+/** Tracks the Activities in the application and broadcasts the same */
+internal class ActivityBroadcasterPlugin : InfrastructurePlugin {
+
+ override lateinit var analytics: Analytics
+ private val application: Application?
+ get() = analytics.currentConfigurationAndroid?.application
+
+ private val activityCount = AtomicInteger()
+
+ override fun setup(analytics: Analytics) {
+ super.setup(analytics)
+ application?.registerActivityLifecycleCallbacks(lifecycleCallback)
+ }
+
+ private val lifecycleCallback by lazy {
+ object : Application.ActivityLifecycleCallbacks {
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+ incrementActivityCount()
+ if (activityCount.get() == 1) {
+ broadCastApplicationStart()
+ }
+ broadcastActivityStart(activity)
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ //nothing to implement
+ }
+
+ override fun onActivityPaused(activity: Activity) {
+ //nothing to implement
+ }
+
+ override fun onActivityStopped(activity: Activity) {
+ if (analytics.currentConfigurationAndroid?.trackLifecycleEvents == true) {
+ decrementActivityCount()
+ if (activityCount.get() == 0) {
+ broadCastApplicationStop()
+ }
+ }
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
+ //nothing to implement
+ }
+
+ override fun onActivityDestroyed(activity: Activity) {
+ //nothing to implement
+ }
+ }
+ }
+
+ private fun broadcastActivityStart(activity: Activity) {
+ analytics.applyInfrastructureClosure {
+ if (this is LifecycleListenerPlugin) {
+ setCurrentActivity(activity)
+ onActivityStarted(activity.localClassName)
+ }
+ }
+ analytics.applyMessageClosure {
+ if (this is LifecycleListenerPlugin) {
+ setCurrentActivity(activity)
+ onActivityStarted(activity.localClassName)
+ }
+ }
+ }
+
+ private fun decrementActivityCount() {
+ activityCount.decrementAndGet()
+ }
+
+ private fun incrementActivityCount() {
+ activityCount.incrementAndGet()
+ }
+
+
+ private fun broadCastApplicationStart() {
+ analytics.applyInfrastructureClosure {
+ if (this is LifecycleListenerPlugin) {
+ this.onAppForegrounded()
+ }
+ }
+ analytics.applyMessageClosure {
+ if (this is LifecycleListenerPlugin) {
+ this.onAppForegrounded()
+ }
+ }
+ }
+
+ private fun broadCastApplicationStop() {
+ analytics.applyInfrastructureClosure {
+ if (this is LifecycleListenerPlugin) {
+ setCurrentActivity(null)
+ this.onAppBackgrounded()
+ }
+ }
+ analytics.applyMessageClosure {
+
+ if (this is LifecycleListenerPlugin) {
+ setCurrentActivity(null)
+ this.onAppBackgrounded()
+ }
+ }
+ }
+
+ override fun shutdown() {
+ application?.unregisterActivityLifecycleCallbacks(lifecycleCallback)
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/AnonymousIdHeaderPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/AnonymousIdHeaderPlugin.kt
new file mode 100644
index 000000000..5ff69ca29
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/AnonymousIdHeaderPlugin.kt
@@ -0,0 +1,37 @@
+package com.rudderstack.android.internal.infrastructure
+
+import com.rudderstack.android.AndroidUtils
+import com.rudderstack.android.ConfigurationAndroid
+import com.rudderstack.android.utilities.applyConfigurationAndroid
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.Configuration
+import com.rudderstack.core.DataUploadService
+import com.rudderstack.core.InfrastructurePlugin
+
+internal class AnonymousIdHeaderPlugin : InfrastructurePlugin {
+
+ override lateinit var analytics: Analytics
+ private var dataUploadService: DataUploadService? = null
+
+ override fun setup(analytics: Analytics) {
+ super.setup(analytics)
+ dataUploadService = analytics.dataUploadService
+ }
+
+ override fun updateConfiguration(configuration: Configuration) {
+ if (configuration !is ConfigurationAndroid) return
+ val anonId = configuration.anonymousId ?: AndroidUtils.generateAnonymousId(
+ configuration.collectDeviceId,
+ configuration.application
+ ).also {
+ analytics.applyConfigurationAndroid {
+ copy(anonymousId = it)
+ }
+ }
+ dataUploadService?.addHeaders(mapOf("Anonymous-Id" to anonId))
+ }
+
+ override fun shutdown() {
+ dataUploadService = null
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/AppInstallUpdateTrackerPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/AppInstallUpdateTrackerPlugin.kt
new file mode 100644
index 000000000..0fa6c0948
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/AppInstallUpdateTrackerPlugin.kt
@@ -0,0 +1,129 @@
+package com.rudderstack.android.internal.infrastructure
+
+import android.content.pm.PackageManager
+import android.os.Build
+import com.rudderstack.android.storage.AndroidStorage
+import com.rudderstack.android.utilities.androidStorage
+import com.rudderstack.android.utilities.currentConfigurationAndroid
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.InfrastructurePlugin
+import com.rudderstack.core.Plugin
+import com.rudderstack.core.models.AppVersion
+import com.rudderstack.core.models.Message
+
+private const val PREVIOUS_VERSION = "previous_version"
+private const val PREVIOUS_BUILD = "previous_build"
+private const val VERSION = "version"
+private const val BUILD = "build"
+
+private const val EVENT_NAME_APPLICATION_INSTALLED = "Application Installed"
+private const val EVENT_NAME_APPLICATION_UPDATED = "Application Updated"
+
+private const val DEFAULT_BUILD = -1
+private const val DEFAULT_VERSION_NAME = ""
+
+/**
+ * Plugin to check and send the application installed/updated events.
+ * However an [InfrastructurePlugin] is not suited for generating events on setup
+ * as [InfrastructurePlugin]s are initialized way before the [Analytics] object is ready to
+ * transmit events.
+ * Whereas [Plugin]s are setup after [InfrastructurePlugin]s and are capable to transmit events
+ * * */
+class AppInstallUpdateTrackerPlugin : Plugin {
+
+ override lateinit var analytics: Analytics
+ private lateinit var appVersion: AppVersion
+ override fun intercept(chain: Plugin.Chain): Message {
+ // no change made to message
+ return chain.proceed(chain.message())
+ }
+
+ override fun setup(analytics: Analytics) {
+ super.setup(analytics)
+ this.appVersion = getAppVersion(analytics)
+ storeVersionNameAndBuild(analytics.androidStorage)
+ if (this.analytics.currentConfigurationAndroid?.trackLifecycleEvents == true) {
+ trackApplicationStatus()
+ }
+ }
+
+ private fun getAppVersion(analytics: Analytics): AppVersion {
+ val previousBuild: Int? = analytics.androidStorage.build
+ val previousVersionName: String? = analytics.androidStorage.versionName
+ var currentBuild: Int? = null
+ var currentVersionName: String? = null
+
+ try {
+ val packageName = analytics.currentConfigurationAndroid?.application?.packageName
+ val packageManager: PackageManager? = analytics.currentConfigurationAndroid?.application?.packageManager
+ val packageInfo = packageName?.let {
+ packageManager?.getPackageInfo(it, 0)
+ }
+
+ currentVersionName = packageInfo?.versionName
+ currentBuild = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ packageInfo?.longVersionCode?.toInt()
+ } else {
+ packageInfo?.versionCode
+ }
+ } catch (ex: PackageManager.NameNotFoundException) {
+ analytics.logger.error(log = "Failed to get app version info: ${ex.message}")
+ }
+
+ return AppVersion(
+ previousBuild = previousBuild ?: DEFAULT_BUILD,
+ previousVersionName = previousVersionName ?: DEFAULT_VERSION_NAME,
+ currentBuild = currentBuild ?: DEFAULT_BUILD,
+ currentVersionName = currentVersionName ?: DEFAULT_VERSION_NAME,
+ )
+ }
+
+ private fun storeVersionNameAndBuild(analyticsStorage: AndroidStorage) {
+ analyticsStorage.setVersionName(this.appVersion.currentVersionName)
+ analyticsStorage.setBuild(this.appVersion.currentBuild)
+ }
+
+ private fun trackApplicationStatus() {
+ if (this.isApplicationInstalled()) {
+ sendApplicationInstalledEvent()
+ } else if (this.isApplicationUpdated()) {
+ sendApplicationUpdatedEvent()
+ }
+ }
+
+ private fun isApplicationInstalled(): Boolean {
+ return this.appVersion.previousBuild == -1
+ }
+
+ private fun isApplicationUpdated(): Boolean {
+ return this.appVersion.previousBuild != -1 && this.appVersion.previousBuild != this.appVersion.currentBuild
+ }
+
+ private fun sendApplicationInstalledEvent() {
+ this.analytics.logger.debug(log = "Tracking Application Installed event")
+ val trackProperties = mutableMapOf()
+ trackProperties[VERSION] = this.appVersion.currentVersionName
+ trackProperties[BUILD] = this.appVersion.currentBuild
+
+ sendEvent(EVENT_NAME_APPLICATION_INSTALLED, trackProperties)
+ }
+
+ private fun sendApplicationUpdatedEvent() {
+ this.analytics.logger.debug(log = "Tracking Application Updated event")
+ val trackProperties = mutableMapOf()
+ trackProperties[PREVIOUS_VERSION] = this.appVersion.previousVersionName
+ trackProperties[PREVIOUS_BUILD] = this.appVersion.previousBuild
+ trackProperties[VERSION] = this.appVersion.currentVersionName
+ trackProperties[BUILD] = this.appVersion.currentBuild
+ sendEvent(EVENT_NAME_APPLICATION_UPDATED, trackProperties)
+ }
+
+ private fun sendEvent(eventName: String, properties: Map) {
+ analytics.track {
+ event(eventName)
+ trackProperties {
+ add(properties)
+ }
+ }
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/LifecycleObserverPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/LifecycleObserverPlugin.kt
new file mode 100644
index 000000000..7e75e6d27
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/LifecycleObserverPlugin.kt
@@ -0,0 +1,153 @@
+package com.rudderstack.android.internal.infrastructure
+
+import android.os.SystemClock
+import com.rudderstack.android.LifecycleListenerPlugin
+import com.rudderstack.android.utilities.androidStorage
+import com.rudderstack.android.utilities.currentConfigurationAndroid
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.ConfigDownloadService
+import com.rudderstack.core.Configuration
+import com.rudderstack.core.InfrastructurePlugin
+import com.rudderstack.core.Plugin
+import com.rudderstack.core.models.Message
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicLong
+
+const val EVENT_NAME_APPLICATION_OPENED = "Application Opened"
+const val EVENT_NAME_APPLICATION_STOPPED = "Application Backgrounded"
+private const val MAX_CONFIG_DOWNLOAD_INTERVAL = 90 * 60 * 1000 // 90 MINUTES
+
+private const val AUTOMATIC = "automatic"
+
+/**
+ * We use a [Plugin] instead of [InfrastructurePlugin] as sending events from Infrastructure plugin might
+ * not ensure all plugins to be ready
+ *
+ * @property currentMillisGenerator
+ */
+internal class LifecycleObserverPlugin(
+ val currentMillisGenerator: (() -> Long) = { SystemClock.uptimeMillis() }
+) : Plugin, LifecycleListenerPlugin {
+
+ override lateinit var analytics: Analytics
+
+ private var _isFirstLaunch = AtomicBoolean(true)
+
+ private var _lastSuccessfulDownloadTimeInMillis = AtomicLong(-1L)
+ private var currentActivityName: String? = null
+ private val listener = ConfigDownloadService.Listener {
+ if (it) {
+ _lastSuccessfulDownloadTimeInMillis.set(currentMillisGenerator())
+ }
+ }
+
+ override fun setup(analytics: Analytics) {
+ super.setup(analytics)
+ analytics.applyInfrastructureClosure {
+ if (this is ConfigDownloadService) {
+ addListener(listener, 1)
+ }
+ }
+ }
+
+ private fun sendLifecycleStart() {
+ withTrackLifeCycle {
+ analytics.also { analytics ->
+ analytics.track {
+ event(EVENT_NAME_APPLICATION_OPENED)
+ trackProperties {
+ _isFirstLaunch.getAndSet(false).also { isFirstLaunch ->
+ add("from_background" to !isFirstLaunch)
+ }.takeIf { it }?.let {
+ add("version" to (analytics.androidStorage.versionName ?: ""))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun sendLifecycleStop() {
+ withTrackLifeCycle {
+ analytics.track {
+ event(EVENT_NAME_APPLICATION_STOPPED)
+ }
+ }
+ }
+
+ override fun intercept(chain: Plugin.Chain): Message {
+ return chain.proceed(chain.message())
+ }
+
+ override fun onShutDown() {
+ _lastSuccessfulDownloadTimeInMillis.set(0)
+ analytics.applyInfrastructureClosure {
+ if (this is ConfigDownloadService) {
+ removeListener(listener)
+ }
+ }
+ }
+
+ override fun updateConfiguration(configuration: Configuration) {
+ // no -op
+ }
+
+ override fun onAppForegrounded() {
+ sendLifecycleStart()
+ checkAndDownloadSourceConfig()
+ }
+
+ private fun checkAndDownloadSourceConfig() {
+ analytics.takeIf { it.currentConfiguration?.shouldVerifySdk == true }?.apply {
+ if (currentMillisGenerator() - (_lastSuccessfulDownloadTimeInMillis.get()) >= MAX_CONFIG_DOWNLOAD_INTERVAL) {
+ analytics.updateSourceConfig()
+ }
+ }
+ }
+
+ override fun onAppBackgrounded() {
+ sendLifecycleStop()
+ analytics.flush()
+ }
+
+ override fun onActivityStarted(activityName: String) {
+ currentActivityName = activityName
+ withRecordScreenViews {
+ analytics.screen {
+ screenName(activityName)
+ screenProperties {
+ add(AUTOMATIC to true)
+ }
+ }
+ }
+ }
+
+ override fun onActivityStopped(activityName: String) {
+ //No-Ops
+ }
+
+ override fun onScreenChange(name: String, arguments: Map?) {
+ val activityName = currentActivityName ?: ""
+ withRecordScreenViews {
+ analytics.screen {
+ screenName(activityName)
+ this.category(name)
+ this.screenProperties {
+ add(arguments ?: mapOf())
+ }
+ }
+ }
+ }
+
+ private fun withTrackLifeCycle(body: () -> Unit) {
+ if (analytics.currentConfigurationAndroid?.trackLifecycleEvents == true) {
+ body()
+ }
+ }
+
+ private fun withRecordScreenViews(body: () -> Unit) {
+ if (analytics.currentConfigurationAndroid?.recordScreenViews == true) {
+ body()
+ }
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/ReinstatePlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ReinstatePlugin.kt
new file mode 100644
index 000000000..8dbd23d52
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ReinstatePlugin.kt
@@ -0,0 +1,188 @@
+package com.rudderstack.android.internal.infrastructure
+
+import com.rudderstack.android.AndroidUtils
+import com.rudderstack.android.ConfigurationAndroid
+import com.rudderstack.android.utilities.androidStorage
+import com.rudderstack.android.utilities.contextState
+import com.rudderstack.android.utilities.currentConfigurationAndroid
+import com.rudderstack.android.utilities.initializeSessionManagement
+import com.rudderstack.android.utilities.processNewContext
+import com.rudderstack.android.utilities.setAnonymousId
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.DataUploadService
+import com.rudderstack.core.InfrastructurePlugin
+import com.rudderstack.core.models.createContext
+
+/**
+ * This plugin is used to reinstate the cache data in V2 SDK. In case no
+ * cached data preset for v2, we check if the sourceId is present in the V1
+ * cached server config, then the data is migrated to V2 SDK Servers the
+ * following purposes: Reinstate anonymous id, or create new one if absent
+ * Reinstate user id Initialise session management for the user Reinstate
+ * traits and external ids
+ */
+internal class ReinstatePlugin : InfrastructurePlugin {
+
+ override lateinit var analytics: Analytics
+
+ override fun setup(analytics: Analytics) {
+ super.setup(analytics)
+ setReinstated(false)
+ reinstate()
+ }
+
+ private fun setReinstated(isReinstated: Boolean) {
+ synchronized(this) {
+ if (isReinstated) {
+ analytics.applyInfrastructureClosure {
+ if (this is DataUploadService)
+ this.resume()
+ }
+ } else {
+ analytics.applyInfrastructureClosure {
+ if (this is DataUploadService)
+ this.pause()
+ }
+ }
+ }
+ }
+
+ private fun reinstate() {
+ if (isV2DataAvailable()) {
+ reinstateV2FromCache()
+ setReinstated(true)
+ return
+ }
+ migrateV1DataIfAvailable()
+ if (analytics.currentConfigurationAndroid?.anonymousId == null) {
+ analytics.currentConfigurationAndroid?.fillDefaults()
+ setReinstated(true)
+ return
+ }
+ }
+
+ private fun ConfigurationAndroid.fillDefaults() {
+ analytics?.setAnonymousId(AndroidUtils.generateAnonymousId(collectDeviceId, application))
+ analytics?.initializeSessionManagement(
+ analytics?.androidStorage?.sessionId,
+ analytics?.androidStorage?.lastActiveTimestamp
+ )
+ }
+
+ private fun reinstateV2FromCache() {
+ val anonId = analytics.androidStorage.anonymousId ?: analytics.currentConfigurationAndroid?.let {
+ AndroidUtils.generateAnonymousId(
+ it.collectDeviceId,
+ it.application
+ )
+ }
+ val context = analytics.androidStorage.context
+ context?.let {
+ analytics.processNewContext(context)
+ }
+ if (anonId != null)
+ analytics.setAnonymousId(anonId)
+ analytics.initializeSessionManagement(
+ analytics.androidStorage.sessionId, analytics.androidStorage.lastActiveTimestamp
+ )
+ }
+
+ private fun isV2DataAvailable(): Boolean {
+ return !analytics.androidStorage.anonymousId.isNullOrEmpty() ||
+ !analytics.androidStorage.userId.isNullOrEmpty() ||
+ !analytics.contextState?.value.isNullOrEmpty()
+ }
+
+ private fun migrateV1DataIfAvailable() {
+ // migrate user id/ anon id
+ analytics.setUserIdFromV1()
+ analytics.migrateAnonymousIdFromV1()
+ analytics.migrateOptOutFromV1()
+ analytics.migrateContextFromV1()
+ //we do not store v1 advertising id
+ analytics.androidStorage.resetV1AdvertisingId()
+ analytics.migrateSession()
+ analytics.migrateV1LifecycleProperties()
+ analytics.androidStorage.migrateV1StorageToV2 {
+ setReinstated(true)
+ }
+ analytics.androidStorage.deleteV1SharedPreferencesFile()
+ analytics.androidStorage.deleteV1ConfigFiles()
+ }
+
+ private fun Analytics.migrateSession() {
+ initializeSessionManagement(
+ androidStorage.v1SessionId,
+ androidStorage.v1LastActiveTimestamp
+ )
+ resetV1SessionValues()
+ }
+
+ private fun Analytics.migrateV1LifecycleProperties() {
+ migrateV1Build()
+ migrateV1Version()
+ }
+
+ private fun Analytics.setUserIdFromV1() {
+ val traits = androidStorage.v1Traits
+ val userId = traits?.get("userId") as? String ?: traits?.get("id") as? String
+ if (userId.isNullOrEmpty() || !this.androidStorage.userId.isNullOrEmpty()) return
+ androidStorage.setUserId(userId)
+ }
+
+ private fun Analytics.migrateAnonymousIdFromV1() {
+ currentConfigurationAndroid?.apply {
+ (androidStorage.v1AnonymousId
+ ?: getV1AnonymousIdFromTraits()
+ ?: AndroidUtils.generateAnonymousId(
+ collectDeviceId, application
+ )).let {
+ logger.error(log = "Unable to migrate anonymousId from V1. Generating new anonymousId")
+ analytics.setAnonymousId(it)
+ }
+ androidStorage.resetV1AnonymousId()
+ }
+ }
+
+ private fun Analytics.getV1AnonymousIdFromTraits(): String? {
+ val traits = androidStorage.v1Traits
+ return traits?.get("anonymousId") as? String
+ }
+
+ private fun Analytics.migrateV1Build() {
+ androidStorage.v1Build?.let {
+ androidStorage.setBuild(it)
+ }
+ androidStorage.resetV1Build()
+ }
+
+ private fun Analytics.resetV1SessionValues() {
+ androidStorage.resetV1SessionId()
+ androidStorage.resetV1SessionLastActiveTimestamp()
+ }
+
+ private fun Analytics.migrateV1Version() {
+ androidStorage.v1VersionName?.let {
+ androidStorage.setVersionName(it)
+ }
+ androidStorage.resetV1Version()
+ }
+
+
+ private fun Analytics.migrateOptOutFromV1() {
+ val optOut = androidStorage.v1OptOut
+ if (!optOut || this.androidStorage.isOptedOut) return //only relevant if optout is true
+ analytics.optOut(true)
+ androidStorage.resetV1OptOut()
+ }
+
+ private fun Analytics.migrateContextFromV1() {
+ createContext(
+ traits = androidStorage.v1Traits, externalIds = androidStorage.v1ExternalIds
+ ).let {
+ processNewContext(it)
+ androidStorage.resetV1Traits()
+ androidStorage.resetV1ExternalIds()
+ }
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/ResetImplementationPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ResetImplementationPlugin.kt
new file mode 100644
index 000000000..dc058bbee
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ResetImplementationPlugin.kt
@@ -0,0 +1,25 @@
+package com.rudderstack.android.internal.infrastructure
+
+import com.rudderstack.android.utilities.contextState
+import com.rudderstack.android.utilities.processNewContext
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.InfrastructurePlugin
+import com.rudderstack.core.models.createContext
+import com.rudderstack.core.models.updateWith
+
+class ResetImplementationPlugin : InfrastructurePlugin {
+
+ override lateinit var analytics: Analytics
+
+ private val contextState
+ get() = analytics.contextState
+
+ override fun reset() {
+ analytics.processNewContext(
+ contextState?.value?.updateWith(
+ traits = mapOf(),
+ externalIds = listOf()
+ ) ?: createContext()
+ )
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/plugins/ExtractStatePlugin.kt b/android/src/main/java/com/rudderstack/android/internal/plugins/ExtractStatePlugin.kt
new file mode 100644
index 000000000..d84a2bc97
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/plugins/ExtractStatePlugin.kt
@@ -0,0 +1,144 @@
+package com.rudderstack.android.internal.plugins
+
+import com.rudderstack.android.utilities.androidStorage
+import com.rudderstack.android.utilities.contextState
+import com.rudderstack.android.utilities.currentConfigurationAndroid
+import com.rudderstack.android.utilities.empty
+import com.rudderstack.android.utilities.processNewContext
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.Plugin
+import com.rudderstack.core.models.AliasMessage
+import com.rudderstack.core.models.IdentifyMessage
+import com.rudderstack.core.models.Message
+import com.rudderstack.core.models.MessageContext
+import com.rudderstack.core.models.optAddContext
+import com.rudderstack.core.models.traits
+import com.rudderstack.core.models.updateWith
+import com.rudderstack.core.optAdd
+
+/**
+ * Mutates the system state, if required, based on the Event.
+ * In case of [IdentifyMessage], it is expected to save the traits provided.
+ */
+internal class ExtractStatePlugin : Plugin {
+
+ override lateinit var analytics: Analytics
+
+ override fun intercept(chain: Plugin.Chain): Message {
+ val message = chain.message()
+
+ if (!(message is IdentifyMessage || message is AliasMessage)) {
+ return chain.proceed(message)
+ }
+ // alias message can change user id permanently
+ //save and update traits
+ //update userId
+ //save and update external ids
+ (message.context ?: return message).let {
+ if (it.traits?.get("anonymousId") == null)
+ analytics.currentConfigurationAndroid?.anonymousId.let { anonId ->
+ it.updateWith(traits = it.traits optAdd ("anonymousId" to anonId))
+ } else it
+ }.let {
+
+ //alias and identify messages are expected to contain user id.
+ //We check in context as well as context.traits with either keys "userId" and "id"
+ //user id can be retrieved if put directly in context or context.traits with the
+ //aforementioned ids
+ val newUserId = getUserId(message)
+
+ analytics.logger.debug(log = "New user id detected: $newUserId")
+ val prevId = analytics.androidStorage.userId ?: analytics.currentConfigurationAndroid?.anonymousId ?: String.empty()
+ // in case of identify, the stored traits (if any) are replaced by the ones provided
+ // if user id is different. else traits are added to it
+ val msg = when (message) {
+ is AliasMessage -> {
+ // in case of alias, we change the user id in traits
+ newUserId?.let { newId ->
+ updateNewAndPrevUserIdInContext(
+ newId, it
+ )
+ }?.let {
+ replaceContext(it)
+ message.copy(context = it, userId = newUserId, previousId = prevId)
+ }
+ }
+
+ is IdentifyMessage -> {
+ val updatedContext = if (newUserId != prevId) {
+ it
+ } else {
+ appendContextForIdentify(it)
+ }
+ replaceContext(updatedContext)
+ message.copy(context = updatedContext)
+ }
+
+ else -> {
+ message
+ }
+ } ?: message
+ msg.also {
+ newUserId?.let { id ->
+ analytics.androidStorage.setUserId(id)
+ }
+ }
+ return chain.proceed(msg)
+
+ }
+ }
+
+ private fun appendContextForIdentify(messageContext: MessageContext): MessageContext {
+ return analytics.contextState?.value?.let { savedContext ->
+ messageContext optAddContext savedContext
+ } ?: messageContext
+ }
+
+ private fun replaceContext(messageContext: MessageContext) {
+ analytics.processNewContext(messageContext)
+ }
+
+
+ /**
+ * Checks in the order
+ * "user_id" key at root
+ * "user_id" key at context.traits
+ * "userId" key at root
+ * "userId" key at context.traits
+ * "id" key at root
+ * "id" key at context.traits
+ *
+ *
+ */
+ private fun getUserId(message: Message): String? {
+ return message.context?.let {
+ (it[KeyConstants.CONTEXT_USER_ID_KEY]
+ ?: (it.traits?.get(KeyConstants.CONTEXT_USER_ID_KEY))
+ ?: (it[KeyConstants.CONTEXT_USER_ID_KEY_ALIAS])
+ ?: (it.traits?.get(KeyConstants.CONTEXT_USER_ID_KEY_ALIAS))
+ ?: (it[KeyConstants.CONTEXT_ID_KEY])
+ ?: (it.traits?.get(KeyConstants.CONTEXT_ID_KEY)))?.toString()
+ } ?: message.userId
+ }
+
+ private fun updateNewAndPrevUserIdInContext(
+ newUserId: String, messageContext: MessageContext
+ ): MessageContext {
+ val newTraits =
+ messageContext.traits optAdd mapOf(
+ KeyConstants.CONTEXT_ID_KEY to newUserId, KeyConstants.CONTEXT_USER_ID_KEY to newUserId
+ )
+
+ //also in case of alias, user id in context should also change, given it's
+ // present there
+ return messageContext.updateWith(
+ traits = newTraits
+ )
+ }
+
+ object KeyConstants {
+ const val CONTEXT_USER_ID_KEY = "user_id"
+ const val CONTEXT_USER_ID_KEY_ALIAS = "userId"
+ const val CONTEXT_ID_KEY = "id"
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/plugins/FillDefaultsPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/plugins/FillDefaultsPlugin.kt
new file mode 100644
index 000000000..0b0903957
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/plugins/FillDefaultsPlugin.kt
@@ -0,0 +1,77 @@
+package com.rudderstack.android.internal.plugins
+
+import com.rudderstack.android.utilities.androidStorage
+import com.rudderstack.android.utilities.contextState
+import com.rudderstack.android.utilities.currentConfigurationAndroid
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.MissingPropertiesException
+import com.rudderstack.core.Plugin
+import com.rudderstack.core.models.*
+
+/**
+ * Fill the defaults for a [Message]
+ * In case a message contains traits, external ids, custom contexts, this will override
+ * the values in storage for the same. That means if message contains traits
+ * {a:b, c:d} and saved traits contain {c:e, g:h, i:j}, the resultant will be
+ * that of the message, i.e {a:b, c:d}. This is applicable to traits, external ids and custom contexts
+ * The default context will be added irrespectively.
+ * In case default context contains externalIds/traits/custom contexts that are common with
+ * message external ids/traits/custom contexts respectively, the values will be amalgamated with
+ * preference given to those belonging to message, in case keys match.
+ * This plugin also adds the userId and anonymousId to the message, if not present.
+ * this plugin also changes the channel for the messages to android
+ *
+ */
+internal class FillDefaultsPlugin : Plugin {
+
+ override lateinit var analytics: Analytics
+
+ /**
+ * Fill default details for [Message]
+ * If message contains context, this will replace the ones present
+ * @throws [MissingPropertiesException] if neither of userId or anonymous id is present
+ */
+ @Throws(MissingPropertiesException::class)
+ private inline fun T.withDefaults(): T {
+ val anonId = this.anonymousId ?: analytics.currentConfigurationAndroid?.anonymousId
+ val userId = this.userId ?: analytics.androidStorage.userId
+ if (anonId == null && userId == null) {
+ val ex = MissingPropertiesException("Either Anonymous Id or User Id must be present");
+ analytics.currentConfigurationAndroid?.logger?.error(
+ log = "Missing both anonymous Id and user Id. Use settings to update " + "anonymous id in Analytics constructor",
+ throwable = ex
+ )
+ throw ex
+ }
+ //copying top level context to message context
+ val newContext =
+ // in case of alias we purposefully remove traits from context
+ analytics.contextState?.value?.let {
+ if (this is AliasMessage && this.userId != analytics.androidStorage.userId) it.updateWith(
+ traits = mapOf()
+ ) else it
+ } selectiveReplace context.let {
+ if (this !is IdentifyMessage) {
+ // remove any external ids present in the message
+ // this is in accordance to v1
+ it?.withExternalIdsRemoved()
+ } else it
+ }
+
+ return (this.copy(
+ context = newContext,
+ anonymousId = anonId,
+ userId = userId
+ ) as T)
+ }
+
+ private infix fun MessageContext?.selectiveReplace(context: MessageContext?): MessageContext? {
+ if (this == null) return context else if (context == null) return this
+ return this.updateWith(context)
+ }
+
+ override fun intercept(chain: Plugin.Chain): Message {
+ val message = chain.message().withDefaults()
+ return chain.proceed(message)
+ }
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/plugins/PlatformInputsPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/plugins/PlatformInputsPlugin.kt
new file mode 100644
index 000000000..09ce86aa8
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/plugins/PlatformInputsPlugin.kt
@@ -0,0 +1,333 @@
+/*
+ * Creator: Debanjan Chatterjee on 08/07/22, 11:06 AM Last modified: 08/07/22, 11:06 AM
+ * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain a
+ * copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.rudderstack.android.internal.plugins
+
+import android.app.Activity
+import android.app.Application
+import android.bluetooth.BluetoothAdapter
+import android.content.ContentResolver
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.wifi.WifiManager
+import android.os.Build
+import android.provider.Settings
+import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
+import com.rudderstack.android.AndroidUtils.getDeviceId
+import com.rudderstack.android.AndroidUtils.isOnClassPath
+import com.rudderstack.android.AndroidUtils.isTv
+import com.rudderstack.android.ConfigurationAndroid
+import com.rudderstack.android.LifecycleListenerPlugin
+import com.rudderstack.android.utilities.applyConfigurationAndroid
+import com.rudderstack.android.utilities.currentConfigurationAndroid
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.Configuration
+import com.rudderstack.core.Plugin
+import com.rudderstack.core.models.Message
+import com.rudderstack.core.models.MessageContext
+import com.rudderstack.core.optAdd
+import java.util.Locale
+import java.util.TimeZone
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * Sets the context specific to Android
+ *
+ * @constructor Initiates the values
+ *
+ */
+private const val CHANNEL = "mobile"
+
+internal class PlatformInputsPlugin : Plugin, LifecycleListenerPlugin {
+
+ override lateinit var analytics: Analytics
+ private val application
+ get() = analytics.currentConfigurationAndroid?.application
+
+ private var autoCollectAdvertisingId = false
+ set(value) {
+ field = value
+ if (value && _advertisingId.isNullOrEmpty()) application?.collectAdvertisingId()
+ else if (!value) synchronized(this) {
+ _advertisingId = null
+ }
+ }
+ private var _collectDeviceId = false
+ set(value) {
+ if (field == value) return
+ field = value
+ if (value) application?.collectDeviceId()
+ else synchronized(this) {
+ _deviceId = null
+ }
+ }
+
+ private var _deviceId: String? = null
+ private var _advertisingId: String? = null
+ private var _deviceToken: String? = null
+
+ private val _currentActivity: AtomicReference = AtomicReference()
+ private val currentActivity: Activity?
+ get() = _currentActivity.get()
+
+ override fun intercept(chain: Plugin.Chain): Message {
+ val msg = chain.message()
+ val newMsg =
+ msg.copy(context = msg.context optAdd application?.defaultAndroidContext()).also {
+ it.channel = CHANNEL
+ }
+ return chain.proceed(newMsg)
+ }
+
+ override fun setup(analytics: Analytics) {
+ super.setup(analytics)
+ analytics.currentConfigurationAndroid?.updateAdvertisingValues()
+ }
+
+ override fun updateConfiguration(configuration: Configuration) {
+ if (configuration !is ConfigurationAndroid) return
+ configuration.updateAdvertisingValues()
+ _defaultAndroidContext = null
+ }
+
+ private fun ConfigurationAndroid.updateAdvertisingValues() {
+ if (!advertisingId.isNullOrEmpty()) {
+ synchronized(this) {
+ if (_advertisingId != advertisingId)
+ _advertisingId = advertisingId
+ }
+ }
+ autoCollectAdvertisingId = autoCollectAdvertId
+ _collectDeviceId = collectDeviceId
+ }
+
+ /**
+ * Overriding advertising id, will disable auto collection if it's on
+ *
+ * @param advertisingId
+ */
+
+ @VisibleForTesting
+ internal fun setAdvertisingId(advertisingId: String) {
+ autoCollectAdvertisingId = false
+ synchronized(this) {
+ _advertisingId = advertisingId
+ }
+ }
+
+ internal fun putDeviceToken(deviceToken: String) {
+ synchronized(this) {
+ _deviceToken = deviceToken
+ }
+ }
+
+ private fun Application.collectAdvertisingId() {
+ if (!isOnClassPath("com.google.android.gms.ads.identifier.AdvertisingIdClient")) {
+ analytics.currentConfiguration?.logger?.debug(
+ log = "Not collecting advertising ID because "
+ + "com.google.android.gms.ads.identifier.AdvertisingIdClient "
+ + "was not found on the classpath."
+ )
+ return
+ }
+ analytics.currentConfigurationAndroid?.advertisingIdFetchExecutor?.submit {
+ val adId = try {
+ getGooglePlayServicesAdvertisingID()
+ } catch (ex: Exception) {
+ analytics.currentConfiguration?.logger?.error(log = "Error collecting play services ad id", throwable = ex)
+ null
+ } ?: try {
+ getAmazonFireAdvertisingID()
+ } catch (ex: Exception) {
+ analytics.currentConfiguration?.logger?.error(log = "Error collecting amazon fire ad id", throwable = ex)
+ null
+ }
+ analytics.currentConfiguration?.logger?.info(log = "Ad id collected is $adId")
+ if (adId != null) {
+ analytics.applyConfigurationAndroid {
+ copy(advertisingId = adId)
+ }
+ }
+ }
+ }
+
+ private fun Application.collectDeviceId() {
+ synchronized(this@PlatformInputsPlugin) {
+ _deviceId = getDeviceId(this)
+ }
+ }
+
+ @Throws(Exception::class)
+ private fun Application.getGooglePlayServicesAdvertisingID(): String? {
+ val advertisingInfo = Class.forName("com.google.android.gms.ads.identifier.AdvertisingIdClient")
+ .getMethod("getAdvertisingIdInfo", Context::class.java).invoke(null, this)
+ ?: return null
+ val isLimitAdTrackingEnabled =
+ advertisingInfo.javaClass.getMethod("isLimitAdTrackingEnabled")
+ .invoke(advertisingInfo) as? Boolean
+ if (isLimitAdTrackingEnabled == true) {
+ analytics.logger.debug(log = "Not collecting advertising ID because isLimitAdTrackingEnabled (Google Play Services) is true.")
+ return null
+ }
+ return advertisingInfo.javaClass.getMethod("getId").invoke(advertisingInfo) as? String
+ }
+
+ @Throws(Exception::class)
+ private fun Application.getAmazonFireAdvertisingID(): String? {
+
+ val contentResolver: ContentResolver = contentResolver
+ val limitAdTracking = Settings.Secure.getInt(contentResolver, "limit_ad_tracking") != 0
+ if (limitAdTracking) {
+ analytics.logger.debug(log = "Not collecting advertising ID because limit_ad_tracking (Amazon Fire OS) is true.")
+ return null
+ }
+ return Settings.Secure.getString(
+ contentResolver, "advertising_id"
+ )
+ }
+
+ private var _defaultAndroidContext: MessageContext? = null
+ private fun Application.defaultAndroidContext(): MessageContext {
+ return synchronized(this) { _defaultAndroidContext } ?: generateDefaultAndroidContext()
+ }
+
+ private fun Application.generateDefaultAndroidContext() = mapOf(
+ "app" to getAppDetails(),
+ "os" to getOsInfo(),
+ "screen" to getScreenInfo(),
+ "userAgent" to userAgent,
+ "locale" to locale,
+ "device" to getDeviceInfo(),
+ "network" to getRudderNetwork(),
+ "timezone" to timeZone
+ ).also {
+ synchronized(this) {
+ _defaultAndroidContext = it
+ }
+ }
+
+ private fun Application.getAppDetails(): Any? {
+ try {
+ val packageName = packageName
+ val packageManager = packageManager
+ val packageInfo = packageManager.getPackageInfo(packageName, 0)
+ val build =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode.toString() else packageInfo.versionCode.toString()
+ return mapOf(
+ "name" to packageInfo.applicationInfo.loadLabel(packageManager).toString(),
+ "build" to build,
+ "namespace" to packageName,
+ "version" to packageInfo.versionName
+ )
+ } catch (ex: PackageManager.NameNotFoundException) {
+ analytics.currentConfiguration?.logger?.error(
+ log = "Package Name Not Found",
+ throwable = ex
+ )
+ }
+ return null
+ }
+
+ private fun getOsInfo(): Any? {
+ return mapOf("name" to "Android", "version" to Build.VERSION.RELEASE)
+ }
+
+ private fun Application.getScreenInfo(): Any? {
+ return currentActivity?.resources?.displayMetrics?.let {
+ mapOf(
+ "density" to it.densityDpi,
+ "height" to it.heightPixels,
+ "width" to it.widthPixels
+ )
+ }
+
+ }
+
+ private fun Application.getDeviceInfo(): Any? {
+ return ((mapOf(
+ "id" to _deviceId,
+ "manufacturer" to Build.MANUFACTURER,
+ "model" to Build.MODEL,
+ "name" to Build.DEVICE,
+ "type" to "Android",
+ "adTrackingEnabled" to !_advertisingId.isNullOrEmpty()
+ ) optAdd (if (_deviceToken != null) mapOf("token" to _deviceToken) else null)) optAdd if (_advertisingId != null) mapOf(
+ "advertisingId" to _advertisingId
+ ) else null)
+ }
+
+ private fun Application.getRudderNetwork(): Map {
+ // carrier name
+ val carrier = if (!isTv()) {
+ val telephonyManager =
+ getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
+ if (telephonyManager != null) telephonyManager.networkOperatorName else "NA"
+ } else null
+
+ // wifi enabled
+ val isWifiEnabled =
+ try {
+ (this.getSystemService(Context.WIFI_SERVICE) as? WifiManager)?.isWifiEnabled ?: false
+ } catch (ex: Exception) {
+ analytics.currentConfiguration?.logger?.error(log = "Cannot detect wifi. Wifi Permission not available")
+ false
+ }
+
+
+ // bluetooth
+ val isBluetoothEnabled =
+ BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON
+
+ // cellular status
+ val tm = getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
+ val isCellularEnabled =
+ if (tm != null && tm.simState == TelephonyManager.SIM_STATE_READY) {
+ Settings.Global.getInt(contentResolver, "mobile_data", 1) == 1
+ } else false
+ val networkMap = HashMap()
+ if (carrier != null) {
+ networkMap["carrier"] = carrier
+ }
+ networkMap["wifi"] = isWifiEnabled
+ networkMap["bluetooth"] = isBluetoothEnabled
+ networkMap["cellular"] = isCellularEnabled
+ return networkMap
+
+ }
+
+ private val userAgent
+ get() = System.getProperty("http.agent")
+ private val locale
+ get() = Locale.getDefault().language + "-" + Locale.getDefault().country
+ private val timeZone
+ get() = TimeZone.getDefault().id
+
+ override fun onAppBackgrounded() {
+ _currentActivity.set(null)
+ }
+
+ override fun setCurrentActivity(activity: Activity?) {
+ _currentActivity.set(activity)
+ _defaultAndroidContext = null
+ //we generate the default context here because we need the activity to get the screen info
+ application?.generateDefaultAndroidContext()
+ }
+
+ override fun onShutDown() {
+ super.onShutDown()
+ analytics.currentConfigurationAndroid?.advertisingIdFetchExecutor?.shutdownNow()
+ }
+
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/plugins/SessionPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/plugins/SessionPlugin.kt
new file mode 100644
index 000000000..f84799e4c
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/plugins/SessionPlugin.kt
@@ -0,0 +1,72 @@
+package com.rudderstack.android.internal.plugins
+
+import com.rudderstack.android.ConfigurationAndroid
+import com.rudderstack.android.internal.extensions.withSessionId
+import com.rudderstack.android.internal.extensions.withSessionStart
+import com.rudderstack.android.models.UserSession
+import com.rudderstack.android.utilities.defaultLastActiveTimestamp
+import com.rudderstack.android.utilities.resetSession
+import com.rudderstack.android.utilities.startSessionIfNeeded
+import com.rudderstack.android.utilities.updateSessionEnd
+import com.rudderstack.android.utilities.userSessionState
+import com.rudderstack.core.Analytics
+import com.rudderstack.core.Configuration
+import com.rudderstack.core.Plugin
+import com.rudderstack.core.models.Message
+
+internal class SessionPlugin : Plugin {
+
+ override lateinit var analytics: Analytics
+
+ private var currentConfiguration: ConfigurationAndroid? = null
+
+ override fun updateConfiguration(configuration: Configuration) {
+ if (configuration !is ConfigurationAndroid) return
+ if (currentConfiguration?.trackAutoSession == configuration.trackAutoSession
+ && currentConfiguration?.trackLifecycleEvents == configuration.trackLifecycleEvents
+ ) return
+ if (!configuration.trackAutoSession || !configuration.trackLifecycleEvents) {
+ analytics.updateSessionEnd()
+ return
+ }
+ analytics.startSessionIfNeeded()
+ }
+
+
+ override fun intercept(chain: Plugin.Chain): Message {
+ //apply session id and session start
+ // update last active timestamp
+ // if difference between two events is more than session timeout, refresh session
+ val message = chain.message()
+ analytics.startSessionIfNeeded()
+ val newMsg = analytics.userSessionState?.value?.takeIf { it.isActive }?.let {
+ updateWithSession(message, it)
+ } ?: message
+
+ return chain.proceed(newMsg)
+ }
+
+ private fun updateWithSession(
+ message: Message, it: UserSession
+ ): Message {
+ val context = message.context
+ var newContext = context?.withSessionId(it.sessionId.toString())
+ if (it.sessionStart) newContext = newContext?.withSessionStart(true)
+
+ updateSessionState(it)
+ return message.copy(newContext)
+ }
+
+ private fun updateSessionState(it: UserSession) {
+ val updatedSession = it.copy(
+ lastActiveTimestamp = defaultLastActiveTimestamp, sessionStart = false
+ )
+ analytics.userSessionState?.update(updatedSession)
+ }
+
+
+ override fun reset() {
+ analytics.resetSession()
+ }
+
+}
diff --git a/android/src/main/java/com/rudderstack/android/internal/states/ContextState.kt b/android/src/main/java/com/rudderstack/android/internal/states/ContextState.kt
new file mode 100644
index 000000000..a38d4be54
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/states/ContextState.kt
@@ -0,0 +1,8 @@
+package com.rudderstack.android.internal.states
+
+import com.rudderstack.core.State
+import com.rudderstack.core.models.MessageContext
+
+internal class ContextState : State(
+ mapOf()
+)
diff --git a/android/src/main/java/com/rudderstack/android/internal/states/UserSessionState.kt b/android/src/main/java/com/rudderstack/android/internal/states/UserSessionState.kt
new file mode 100644
index 000000000..f1d7e8bd2
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/internal/states/UserSessionState.kt
@@ -0,0 +1,8 @@
+package com.rudderstack.android.internal.states
+
+import com.rudderstack.core.State
+import com.rudderstack.android.models.UserSession
+
+internal class UserSessionState : State(
+ UserSession()
+)
diff --git a/android/src/main/java/com/rudderstack/android/models/RudderApp.kt b/android/src/main/java/com/rudderstack/android/models/RudderApp.kt
new file mode 100644
index 000000000..535fd2023
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/models/RudderApp.kt
@@ -0,0 +1,24 @@
+package com.rudderstack.android.models
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.google.gson.annotations.SerializedName
+import com.squareup.moshi.Json
+
+data class RudderApp(
+ @SerializedName("build")
+ @JsonProperty("build")
+ @Json(name = "build")
+ private val build: String,
+ @SerializedName("name")
+ @JsonProperty("name")
+ @Json(name = "name")
+ private val name: String,
+ @SerializedName("namespace")
+ @JsonProperty("namespace")
+ @Json(name = "namespace")
+ private val nameSpace: String,
+ @SerializedName("version")
+ @JsonProperty("version")
+ @Json(name = "version")
+ private val version: String,
+)
diff --git a/android/src/main/java/com/rudderstack/android/models/RudderContext.kt b/android/src/main/java/com/rudderstack/android/models/RudderContext.kt
new file mode 100644
index 000000000..008ad220e
--- /dev/null
+++ b/android/src/main/java/com/rudderstack/android/models/RudderContext.kt
@@ -0,0 +1,29 @@
+package com.rudderstack.android.models
+
+class RudderContext(contextMap: Map) : HashMap(contextMap) {
+ constructor() : this(mapOf())
+
+ var app: RudderApp? by this
+
+ var traits: RudderTraits? by this
+
+ var library: RudderLibraryInfo? by this
+
+ var os: RudderOSInfo? by this
+
+ var screen: RudderScreenInfo? by this
+
+ var userAgent: String? by this
+
+ var locale: String? by this
+
+ var device: RudderDeviceInfo? by this
+
+ var network: RudderNetwork? by this
+
+ var timezone: String? by this
+
+ var externalId: MutableSet