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 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,53 +1,59 @@
package com.amplitude.android.plugins

import android.app.Activity
import android.app.ActivityManager
import android.app.Application
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import com.amplitude.android.Amplitude as AndroidAmplitude
import com.amplitude.android.AutocaptureOption
import com.amplitude.android.Configuration
import com.amplitude.android.ExperimentalAmplitudeFeature
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

class AndroidLifecyclePlugin : 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 val resumed: MutableSet<Int> = mutableSetOf()

private var appInBackground = false

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)

application.registerActivityLifecycleCallbacks(this)
}
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)
}
created.add(activity.hashCode())

if (AutocaptureOption.DEEP_LINKS in androidConfiguration.autocapture) {
DefaultEventUtils(androidAmplitude).trackDeepLinkOpenedEvent(activity)
}
Expand All @@ -57,26 +63,35 @@ class AndroidLifecyclePlugin : Application.ActivityLifecycleCallbacks, Plugin {
}

override fun onActivityStarted(activity: Activity) {
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.SCREEN_VIEWS in androidConfiguration.autocapture) {
DefaultEventUtils(androidAmplitude).trackScreenViewedEvent(activity)
}
}

override fun onActivityResumed(activity: Activity) {
resumed.add(activity.hashCode())

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)
}
}

override fun onActivityPaused(activity: Activity) {
resumed.remove(activity.hashCode())

androidAmplitude.onExitForeground(getCurrentTimeMillis())
@OptIn(ExperimentalAmplitudeFeature::class)
if (AutocaptureOption.ELEMENT_INTERACTIONS in androidConfiguration.autocapture) {
Expand All @@ -85,16 +100,20 @@ class AndroidLifecyclePlugin : Application.ActivityLifecycleCallbacks, Plugin {
}

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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,17 @@ class AndroidLifecyclePluginTest {
amplitude = Amplitude(configuration)

setDispatcher(testScheduler)

// Stored previous version/build
amplitude.storage.write(Storage.Constants.APP_BUILD, "55")
amplitude.storage.write(Storage.Constants.APP_VERSION, "5.0.0")

amplitude.add(androidLifecyclePlugin)

val mockedPlugin = spyk(StubPlugin())
amplitude.add(mockedPlugin)
amplitude.isBuilt.await()

// Stored previous version/build
amplitude.storage.write(Storage.Constants.APP_BUILD, "55")
amplitude.storage.write(Storage.Constants.APP_VERSION, "5.0.0")

val mockedActivity = mockk<Activity>()
val mockedBundle = mockk<Bundle>()
androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle)
Expand Down Expand Up @@ -328,9 +329,12 @@ class AndroidLifecyclePluginTest {

val tracks = mutableListOf<BaseEvent>()
verify { mockedPlugin.track(capture(tracks)) }
Assertions.assertEquals(1, tracks.count())
Assertions.assertEquals(2, tracks.count())

with(tracks[0]) {
Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_INSTALLED, eventType)
}
with(tracks[1]) {
Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_BACKGROUNDED, eventType)
}
}
Expand Down
Loading