Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AmplitudeUnified/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
36 changes: 36 additions & 0 deletions AmplitudeUnified/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Amplitude Unified SDK – Android

The Amplitude Unified SDK for Android provides a single integration point for Amplitude analytics, experimentation, and session replay features in Android applications. This SDK is designed to streamline setup, ensure consistency across Amplitude's product suite, and support extensibility for future capabilities.

This repository is part of Amplitude's cross-platform Unified SDK initiative, alongside [Web](https://github.com/amplitude/Amplitude-TypeScript/tree/main/packages/unified) and [iOS](https://github.com/amplitude/AmplitudeUnified-Swift) implementations.

## Key Points

- Access analytics, experimentation, and session replay through a unified API.
- Reduce integration complexity and maintenance overhead.
- Enable remote configuration and feature toggling without additional engineering effort.
- Ensure consistent identity and session management across Amplitude features.

---

## Implementation Details

_TODO: Describe architecture, core modules, and extensibility mechanisms._

## Getting Started

_TODO: Installation, initialization, and basic usage instructions._

## Configuration

_TODO: Configuration options, remote config, and feature toggling._

## Contributing

_TODO: Contribution guidelines and pull request process._

## License

_TODO: License information._

---
63 changes: 63 additions & 0 deletions AmplitudeUnified/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("com.amplitude.publish-module-plugin")
}

android {
namespace = "com.amplitude.android.unified"
compileSdk = BuildConfig.Versions.Android.COMPILE_SDK

defaultConfig {
minSdk = BuildConfig.Versions.Android.MIN_SDK
multiDexEnabled = true

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")

buildConfigField("String", "AMPLITUDE_VERSION", "\"${version}\"")
}

buildTypes {
release {
isMinifyEnabled = 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"
}
testOptions {
unitTests.isIncludeAndroidResources = true
unitTests.isReturnDefaultValues = true
}
buildFeatures {
buildConfig = true
}
}

publication {
name = "Amplitude Unified Android SDK"
description = "Amplitude Unified client-side SDK for Android"
artifactId = "analytics-unified-android"
}

dependencies {
api(project(":android"))
api(libs.sessionReplayAndroid)

implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.material)

testImplementation(libs.junit4)
androidTestImplementation(libs.test.ext.junit)
androidTestImplementation(libs.espresso.core)
}
3 changes: 3 additions & 0 deletions AmplitudeUnified/config/ktlint/baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<baseline version="1.0">
</baseline>
25 changes: 25 additions & 0 deletions AmplitudeUnified/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# We rely on these Google Play services AppSet classes when useAppSetIdForDeviceId = true
-keep class com.google.android.gms.appset.AppSet {
public getClient(android.content.Context);
}
-keep class com.google.android.gms.appset.AppSetIdClient {
public getAppSetIdInfo();
}
-keep class com.google.android.gms.appset.AppSetIdInfo {
public getId();
}
-keep class com.google.android.gms.tasks.Tasks {
public await(com.google.android.gms.tasks.Task);
}
-keep class com.google.android.gms.tasks.Task

#################### START: Compose Proguard Rules ####################

# The Android SDK checks at runtime if these classes are available via Class.forName
-keepnames interface androidx.compose.ui.node.Owner
-keep class com.amplitude.android.internal.locators.ComposeViewTargetLocator

-keepnames class androidx.compose.foundation.ClickableElement
-keepnames class androidx.compose.foundation.CombinedClickableElement

#################### END: Compose Proguard Rules ####################
21 changes: 21 additions & 0 deletions AmplitudeUnified/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.amplitude.android.unified

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.amplitude.android.unified.test", appContext.packageName)
}
}
4 changes: 4 additions & 0 deletions AmplitudeUnified/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.amplitude.android.unified

import android.content.Context
import com.amplitude.android.Amplitude
import com.amplitude.android.Configuration
import com.amplitude.android.unified.plugins.AmplitudeSessionReplayPlugin
import com.amplitude.id.IdentityConfiguration

open class Amplitude(
configuration: Configuration,
) : Amplitude(configuration) {
private val context: Context = configuration.context

override suspend fun buildInternal(identityConfiguration: IdentityConfiguration) {
super.buildInternal(identityConfiguration)
add(AmplitudeSessionReplayPlugin(context = context))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.amplitude.android.unified.plugins

import android.content.Context
import com.amplitude.android.sessionreplay.SessionReplay
import com.amplitude.core.context.AmplitudeContext
import com.amplitude.core.platform.plugins.AnalyticsClient
import com.amplitude.core.platform.plugins.UniversalPlugin

class AmplitudeSessionReplayPlugin(
private val context: Context,
) : UniversalPlugin {
companion object {
const val PLUGIN_NAME = "com.amplitude.android.sessionreplay"
}

override val name: String = PLUGIN_NAME
private var sessionReplay: SessionReplay? = null

override fun setup(
analyticsClient: AnalyticsClient,
amplitudeContext: AmplitudeContext,
) {
super.setup(analyticsClient, amplitudeContext)
sessionReplay?.stop()
sessionReplay =
SessionReplay(
apiKey = amplitudeContext.apiKey,
context = context,
deviceId = analyticsClient.identity.deviceId.orEmpty(),
sessionId = analyticsClient.sessionId,
logger = amplitudeContext.logger,
serverZone = amplitudeContext.serverZone,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.amplitude.android.unified

import org.junit.Assert.assertEquals
import org.junit.Test

/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
48 changes: 20 additions & 28 deletions android/src/main/java/com/amplitude/android/Amplitude.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,42 @@ import com.amplitude.android.plugins.AndroidLifecyclePlugin
import com.amplitude.android.plugins.AndroidNetworkConnectivityCheckerPlugin
import com.amplitude.android.storage.AndroidStorageContextV3
import com.amplitude.android.utilities.ActivityLifecycleObserver
import com.amplitude.core.State
import com.amplitude.core.platform.plugins.AmplitudeDestination
import com.amplitude.core.platform.plugins.GetAmpliExtrasPlugin
import com.amplitude.id.IdentityConfiguration
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import java.util.concurrent.Executors
import com.amplitude.core.Amplitude as CoreAmplitude

open class Amplitude internal constructor(
open class Amplitude(
configuration: Configuration,
state: State,
amplitudeScope: CoroutineScope = CoroutineScope(SupervisorJob()),
amplitudeDispatcher: CoroutineDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher(),
networkIODispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher(),
storageIODispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher(),
private val activityLifecycleCallbacks: ActivityLifecycleObserver = ActivityLifecycleObserver(),
) : CoreAmplitude(
configuration = configuration,
store = state,
amplitudeScope = amplitudeScope,
amplitudeDispatcher = amplitudeDispatcher,
networkIODispatcher = networkIODispatcher,
storageIODispatcher = storageIODispatcher,
) {
constructor(configuration: Configuration) : this(configuration, State())

private lateinit var androidContextPlugin: AndroidContextPlugin

val sessionId: Long
get() {
return (timeline as Timeline).sessionId
}

private lateinit var activityLifecycleCallbacks: ActivityLifecycleObserver

/**
* This build call is initiated by parent class and happens before this class
* init block
*/
override fun build(): Deferred<Boolean> {
activityLifecycleCallbacks = ActivityLifecycleObserver()
return super.build()
override fun track(
eventType: String,
eventProperties: Map<String, Any>?,
) {
track(
eventType = eventType,
eventProperties = eventProperties,
options = null,
)
}

init {
Expand All @@ -68,7 +58,7 @@ open class Amplitude internal constructor(
}

override fun createTimeline(): Timeline {
return Timeline(configuration.sessionId).also { it.amplitude = this }
return Timeline(this)
}

override fun createIdentityConfiguration(): IdentityConfiguration {
Expand All @@ -88,7 +78,7 @@ open class Amplitude internal constructor(
val migrationManager = MigrationManager(this)
migrationManager.migrateOldStorage()

this.createIdentityContainer(identityConfiguration)
createIdentityContainer(identityConfiguration)

if (this.configuration.offline != AndroidNetworkConnectivityCheckerPlugin.Disabled) {
add(AndroidNetworkConnectivityCheckerPlugin())
Expand All @@ -107,7 +97,7 @@ open class Amplitude internal constructor(
add(AnalyticsConnectorPlugin())
add(AmplitudeDestination())

(timeline as Timeline).start()
timeline.start()
}

/**
Expand All @@ -117,11 +107,13 @@ open class Amplitude internal constructor(
* @return the Amplitude instance
*/
override fun reset(): Amplitude {
this.setUserId(null)
amplitudeScope.launch(amplitudeDispatcher) {
isBuilt.await()
idContainer.identityManager.editIdentity().setDeviceId(null).commit()
androidContextPlugin.initializeDeviceId(configuration as Configuration)
idContainer.identityManager.editIdentity {
setUserId(null)
clearUserProperties()
}
androidContextPlugin.initializeDeviceId(forceReset = true)
}
return this
}
Expand All @@ -140,7 +132,7 @@ open class Amplitude internal constructor(
Runtime.getRuntime().addShutdownHook(
object : Thread() {
override fun run() {
(this@Amplitude.timeline as Timeline).stop()
timeline.stop()
}
},
)
Expand Down
Loading