Skip to content

fix: autocapture missing events fix #232

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion android/src/main/java/com/amplitude/android/Amplitude.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.amplitude.android

import android.app.Application
import android.content.Context
import com.amplitude.android.migration.MigrationManager
import com.amplitude.android.plugins.AnalyticsConnectorIdentityPlugin
Expand All @@ -8,6 +9,7 @@ import com.amplitude.android.plugins.AndroidContextPlugin
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.Amplitude
import com.amplitude.core.events.BaseEvent
import com.amplitude.core.platform.plugins.AmplitudeDestination
Expand All @@ -25,8 +27,15 @@ open class Amplitude(
return (timeline as Timeline).sessionId
}

private val activityLifecycleCallbacks = ActivityLifecycleObserver()

init {
registerShutdownHook()
if (AutocaptureOption.APP_LIFECYCLES in configuration.autocapture) {
with(configuration.context as Application) {
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
}
}
}

override fun createTimeline(): Timeline {
Expand Down Expand Up @@ -63,7 +72,7 @@ open class Amplitude(
}
add(androidContextPlugin)
add(GetAmpliExtrasPlugin())
add(AndroidLifecyclePlugin())
add(AndroidLifecyclePlugin(activityLifecycleCallbacks))
add(AnalyticsConnectorIdentityPlugin())
add(AnalyticsConnectorPlugin())
add(AmplitudeDestination())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,101 @@ import android.app.Application
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import com.amplitude.android.AutocaptureOption
import com.amplitude.android.Configuration
import com.amplitude.android.ExperimentalAmplitudeFeature
import com.amplitude.android.utilities.ActivityCallbackType
import com.amplitude.android.utilities.ActivityLifecycleObserver
import com.amplitude.android.utilities.DefaultEventUtils
import com.amplitude.core.Amplitude
import com.amplitude.core.platform.Plugin
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import com.amplitude.android.Amplitude as AndroidAmplitude

class AndroidLifecyclePlugin : Application.ActivityLifecycleCallbacks, Plugin {
class AndroidLifecyclePlugin(
private val activityLifecycleObserver: ActivityLifecycleObserver
) : Application.ActivityLifecycleCallbacks, Plugin {
override val type: Plugin.Type = Plugin.Type.Utility
override lateinit var amplitude: Amplitude
private lateinit var packageInfo: PackageInfo
private lateinit var androidAmplitude: com.amplitude.android.Amplitude
private lateinit var androidAmplitude: AndroidAmplitude
private lateinit var androidConfiguration: Configuration

private val hasTrackedApplicationLifecycleEvents = AtomicBoolean(false)
private val numberOfActivities = AtomicInteger(1)
private val isFirstLaunch = AtomicBoolean(false)
private val created: MutableSet<Int> = mutableSetOf()
private val started: MutableSet<Int> = mutableSetOf()

private var appInBackground = false

@VisibleForTesting
internal var eventJob: Job? = null

override fun setup(amplitude: Amplitude) {
super.setup(amplitude)
androidAmplitude = amplitude as com.amplitude.android.Amplitude
androidAmplitude = amplitude as AndroidAmplitude
androidConfiguration = amplitude.configuration as Configuration

val application = androidConfiguration.context as Application
val packageManager: PackageManager = application.packageManager
packageInfo = try {
packageManager.getPackageInfo(application.packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
// This shouldn't happen, but in case it happens, fallback to empty package info.
amplitude.logger.error("Cannot find package with application.packageName: " + application.packageName)
PackageInfo()

if (AutocaptureOption.APP_LIFECYCLES in androidConfiguration.autocapture) {
packageInfo = try {
application.packageManager.getPackageInfo(application.packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
// This shouldn't happen, but in case it happens, fallback to empty package info.
amplitude.logger.error("Cannot find package with application.packageName: " + application.packageName)
PackageInfo()
}

DefaultEventUtils(androidAmplitude).trackAppUpdatedInstalledEvent(packageInfo)

eventJob = amplitude.amplitudeScope.launch {
for (event in activityLifecycleObserver.eventChannel) {
event.activity.get()?.let { activity ->
when (event.type) {
ActivityCallbackType.Created -> onActivityCreated(
activity,
activity.intent?.extras
)
ActivityCallbackType.Started -> onActivityStarted(activity)
ActivityCallbackType.Resumed -> onActivityResumed(activity)
ActivityCallbackType.Paused -> onActivityPaused(activity)
ActivityCallbackType.Stopped -> onActivityStopped(activity)
ActivityCallbackType.Destroyed -> onActivityDestroyed(activity)
}
}
}
}
}
application.registerActivityLifecycleCallbacks(this)
}

override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
if (!hasTrackedApplicationLifecycleEvents.getAndSet(true) && AutocaptureOption.APP_LIFECYCLES in androidConfiguration.autocapture) {
numberOfActivities.set(0)
isFirstLaunch.set(true)
DefaultEventUtils(androidAmplitude).trackAppUpdatedInstalledEvent(packageInfo)
}
if (AutocaptureOption.DEEP_LINKS in androidConfiguration.autocapture) {
DefaultEventUtils(androidAmplitude).trackDeepLinkOpenedEvent(activity)
}
created.add(activity.hashCode())

if (AutocaptureOption.SCREEN_VIEWS in androidConfiguration.autocapture) {
DefaultEventUtils(androidAmplitude).startFragmentViewedEventTracking(activity)
}
}

override fun onActivityStarted(activity: Activity) {
if (!created.contains(activity.hashCode())) {
// We check for On Create in case if sdk was initialised in Main Activity
onActivityCreated(activity, activity.intent.extras)
}
started.add(activity.hashCode())

if (AutocaptureOption.APP_LIFECYCLES in androidConfiguration.autocapture && started.size == 1) {
DefaultEventUtils(androidAmplitude).trackAppOpenedEvent(
packageInfo = packageInfo,
isFromBackground = appInBackground
)
appInBackground = false
}

if (AutocaptureOption.DEEP_LINKS in androidConfiguration.autocapture) {
DefaultEventUtils(androidAmplitude).trackDeepLinkOpenedEvent(activity)
}

if (AutocaptureOption.SCREEN_VIEWS in androidConfiguration.autocapture) {
DefaultEventUtils(androidAmplitude).trackScreenViewedEvent(activity)
}
Expand All @@ -65,11 +108,6 @@ class AndroidLifecyclePlugin : Application.ActivityLifecycleCallbacks, Plugin {
override fun onActivityResumed(activity: Activity) {
androidAmplitude.onEnterForeground(getCurrentTimeMillis())

// numberOfActivities makes sure it only fires after activity creation or activity stopped
if (AutocaptureOption.APP_LIFECYCLES in androidConfiguration.autocapture && numberOfActivities.incrementAndGet() == 1) {
val isFromBackground = !isFirstLaunch.getAndSet(false)
DefaultEventUtils(androidAmplitude).trackAppOpenedEvent(packageInfo, isFromBackground)
}
@OptIn(ExperimentalAmplitudeFeature::class)
if (AutocaptureOption.ELEMENT_INTERACTIONS in androidConfiguration.autocapture) {
DefaultEventUtils(androidAmplitude).startUserInteractionEventTracking(activity)
Expand All @@ -78,31 +116,36 @@ class AndroidLifecyclePlugin : Application.ActivityLifecycleCallbacks, Plugin {

override fun onActivityPaused(activity: Activity) {
androidAmplitude.onExitForeground(getCurrentTimeMillis())

@OptIn(ExperimentalAmplitudeFeature::class)
if (AutocaptureOption.ELEMENT_INTERACTIONS in androidConfiguration.autocapture) {
DefaultEventUtils(androidAmplitude).stopUserInteractionEventTracking(activity)
}
}

override fun onActivityStopped(activity: Activity) {
// numberOfActivities makes sure it only fires after setup or activity resumed
if (AutocaptureOption.APP_LIFECYCLES in androidConfiguration.autocapture && numberOfActivities.decrementAndGet() == 0) {
started.remove(activity.hashCode())

if (AutocaptureOption.APP_LIFECYCLES in androidConfiguration.autocapture && started.isEmpty()) {
DefaultEventUtils(androidAmplitude).trackAppBackgroundedEvent()
appInBackground = true
}
}

override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {
}

override fun onActivityDestroyed(activity: Activity) {
created.remove(activity.hashCode())

if (AutocaptureOption.SCREEN_VIEWS in androidConfiguration.autocapture) {
DefaultEventUtils(androidAmplitude).stopFragmentViewedEventTracking(activity)
}
}

override fun teardown() {
super.teardown()
(androidConfiguration.context as Application).unregisterActivityLifecycleCallbacks(this)
eventJob?.cancel()
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.amplitude.android.utilities

import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import kotlinx.coroutines.channels.Channel
import java.lang.ref.WeakReference

class ActivityLifecycleObserver : ActivityLifecycleCallbacks {

internal val eventChannel = Channel<ActivityCallbackEvent>(Channel.UNLIMITED)

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
eventChannel.trySend(
ActivityCallbackEvent(
WeakReference(activity),
ActivityCallbackType.Created
)
)
}

override fun onActivityStarted(activity: Activity) {
eventChannel.trySend(
ActivityCallbackEvent(
WeakReference(activity),
ActivityCallbackType.Started
)
)
}

override fun onActivityResumed(activity: Activity) {
eventChannel.trySend(
ActivityCallbackEvent(
WeakReference(activity),
ActivityCallbackType.Resumed
)
)
}

override fun onActivityPaused(activity: Activity) {
eventChannel.trySend(
ActivityCallbackEvent(
WeakReference(activity),
ActivityCallbackType.Paused
)
)
}

override fun onActivityStopped(activity: Activity) {
eventChannel.trySend(
ActivityCallbackEvent(
WeakReference(activity),
ActivityCallbackType.Stopped
)
)
}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}

override fun onActivityDestroyed(activity: Activity) {
eventChannel.trySend(
ActivityCallbackEvent(
WeakReference(activity),
ActivityCallbackType.Destroyed
)
)
}
}

enum class ActivityCallbackType {
Created, Started, Resumed, Paused, Stopped, Destroyed
}

data class ActivityCallbackEvent(
val activity: WeakReference<Activity>,
val type: ActivityCallbackType
)
Original file line number Diff line number Diff line change
Expand Up @@ -162,26 +162,25 @@ class DefaultEventUtils(private val amplitude: Amplitude) {
} ?: amplitude.logger.error("Failed to stop user interaction event tracking: Activity window is null")
}

private val isFragmentActivityAvailable by lazy {
LoadClass.isClassAvailable(FRAGMENT_ACTIVITY_CLASS_NAME, amplitude.logger)
}

fun startFragmentViewedEventTracking(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
LoadClass.isClassAvailable(FRAGMENT_ACTIVITY_CLASS_NAME, amplitude.logger)
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isFragmentActivityAvailable) {
activity.registerFragmentLifecycleCallbacks(amplitude::track, amplitude.logger)
}
}

fun stopFragmentViewedEventTracking(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
LoadClass.isClassAvailable(FRAGMENT_ACTIVITY_CLASS_NAME, amplitude.logger)
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isFragmentActivityAvailable) {
activity.unregisterFragmentLifecycleCallbacks(amplitude.logger)
}
}

companion object {
private const val FRAGMENT_ACTIVITY_CLASS_NAME = "androidx.fragment.app.FragmentActivity"
internal val Activity.screenName: String?
@Throws(PackageManager.NameNotFoundException::class, Exception::class)
get() {
val packageManager = packageManager
val info =
Expand All @@ -203,21 +202,18 @@ class DefaultEventUtils(private val amplitude: Amplitude) {
return activity.referrer
} else {
var referrerUri: Uri? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val intent = activity.intent
referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER)

if (referrerUri == null) {
referrerUri =
intent.getStringExtra("android.intent.extra.REFERRER_NAME")?.let {
try {
Uri.parse(it)
} catch (e: ParseException) {
amplitude.logger.error("Failed to parse the referrer uri: $it")
null
}
val intent = activity.intent
referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER)
if (referrerUri == null) {
referrerUri =
intent.getStringExtra("android.intent.extra.REFERRER_NAME")?.let {
try {
Uri.parse(it)
} catch (e: ParseException) {
amplitude.logger.error("Failed to parse the referrer uri: $it")
null
}
}
}
}
return referrerUri
}
Expand Down
Loading
Loading