diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index f36e7752c..1b1e1d171 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -8,16 +8,21 @@ on:
jobs:
build:
- runs-on: macos-13
+ runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v4.1.7
+ - name: Enable KVM
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
- name: Run Unit test
- uses: reactivecircus/android-emulator-runner@v2.32.0
+ uses: reactivecircus/android-emulator-runner@v2.35.0
with:
api-level: 34
- profile: Nexus 5X
arch: x86_64
+ emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
script: ./gradlew createDebugCoverageReport
- name: Lint
run: ./gradlew lint
@@ -26,17 +31,17 @@ jobs:
if: always()
with:
name: Unit Test Report
- path: /Users/runner/work/mixpanel-android/mixpanel-android/build/reports/androidTests/connected
+ path: build/reports/androidTests/connected
- name: Upload test coverage report
uses: actions/upload-artifact@v4
with:
name: Test Coverage Report
- path: /Users/runner/work/mixpanel-android/mixpanel-android/build/reports/coverage/debug/
+ path: build/reports/coverage/debug/
- name: Upload lint report
uses: actions/upload-artifact@v4
with:
name: Lint Report
- path: /Users/runner/work/mixpanel-android/mixpanel-android/build/reports/lint-results.html
+ path: build/reports/lint-results.html
- name: Android docs
run: ./gradlew --info androidJavadocs
diff --git a/src/androidTest/AndroidManifest.xml b/src/androidTest/AndroidManifest.xml
new file mode 100644
index 000000000..748fda232
--- /dev/null
+++ b/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/src/androidTest/java/com/mixpanel/android/mpmetrics/MixpanelBasicTest.java b/src/androidTest/java/com/mixpanel/android/mpmetrics/MixpanelBasicTest.java
index 5c374909e..038915d45 100644
--- a/src/androidTest/java/com/mixpanel/android/mpmetrics/MixpanelBasicTest.java
+++ b/src/androidTest/java/com/mixpanel/android/mpmetrics/MixpanelBasicTest.java
@@ -17,6 +17,7 @@
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -1358,8 +1359,8 @@ public void testIdentifyCall() throws JSONException {
new ArrayList();
final AnalyticsMessages listener =
new AnalyticsMessages(
- InstrumentationRegistry.getInstrumentation().getContext(),
- MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getContext(), null)) {
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ MPConfig.getInstance(InstrumentationRegistry.getInstrumentation().getTargetContext(), null)) {
@Override
public void eventsMessage(EventDescription heard) {
if (!heard.isAutomatic()) {
@@ -1371,9 +1372,10 @@ public void eventsMessage(EventDescription heard) {
// Track calls to the flags endpoint
final List flagsEndpointCalls = new ArrayList<>();
+ // Create MixpanelAPI first to register lifecycle callbacks before launching activity
MixpanelAPI metrics =
new TestUtils.CleanMixpanelAPI(
- InstrumentationRegistry.getInstrumentation().getContext(),
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
mMockPreferences,
"Test Identify Call") {
@Override
@@ -1424,36 +1426,39 @@ public RemoteService.RequestResult performRequest(
// Clear any flags calls from constructor
flagsEndpointCalls.clear();
- // First identify should trigger loadFlags since distinctId changes
- metrics.identify(newDistinctId);
+ // Launch an activity to bring the app to foreground, which triggers onForeground()
+ try (ActivityScenario scenario = ActivityScenario.launch(TestActivity.class)) {
+ // First identify should trigger loadFlags since distinctId changes
+ metrics.identify(newDistinctId);
- // Give the async flag loading some time to execute
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- // Ignore
- }
+ // Give the async flag loading some time to execute
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+
+ // Second and third identify should NOT trigger loadFlags since distinctId doesn't change
+ metrics.identify(newDistinctId);
+ metrics.identify(newDistinctId);
- // Second and third identify should NOT trigger loadFlags since distinctId doesn't change
- metrics.identify(newDistinctId);
- metrics.identify(newDistinctId);
-
- // Verify that only one $identify event was tracked
- assertEquals(1, messages.size());
- AnalyticsMessages.EventDescription identifyEventDescription = messages.get(0);
- assertEquals("$identify", identifyEventDescription.getEventName());
- String newDistinctIdIdentifyTrack =
- identifyEventDescription.getProperties().getString("distinct_id");
- String anonDistinctIdIdentifyTrack =
- identifyEventDescription.getProperties().getString("$anon_distinct_id");
-
- assertEquals(newDistinctId, newDistinctIdIdentifyTrack);
- assertEquals(oldDistinctId, anonDistinctIdIdentifyTrack);
-
- // Assert that loadFlags was called (flags endpoint was hit) when distinctId changed
- assertTrue(
- "loadFlags should have been called when distinctId changed",
- flagsEndpointCalls.size() >= 1);
+ // Verify that only one $identify event was tracked
+ assertEquals(1, messages.size());
+ AnalyticsMessages.EventDescription identifyEventDescription = messages.get(0);
+ assertEquals("$identify", identifyEventDescription.getEventName());
+ String newDistinctIdIdentifyTrack =
+ identifyEventDescription.getProperties().getString("distinct_id");
+ String anonDistinctIdIdentifyTrack =
+ identifyEventDescription.getProperties().getString("$anon_distinct_id");
+
+ assertEquals(newDistinctId, newDistinctIdIdentifyTrack);
+ assertEquals(oldDistinctId, anonDistinctIdIdentifyTrack);
+
+ // Assert that loadFlags was called (flags endpoint was hit) when distinctId changed
+ assertTrue(
+ "loadFlags should have been called when distinctId changed",
+ flagsEndpointCalls.size() >= 1);
+ }
}
@Test
diff --git a/src/androidTest/java/com/mixpanel/android/mpmetrics/TestActivity.java b/src/androidTest/java/com/mixpanel/android/mpmetrics/TestActivity.java
new file mode 100644
index 000000000..9a559aca0
--- /dev/null
+++ b/src/androidTest/java/com/mixpanel/android/mpmetrics/TestActivity.java
@@ -0,0 +1,14 @@
+package com.mixpanel.android.mpmetrics;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * Simple test activity for instrumented tests that need an activity lifecycle.
+ */
+public class TestActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+}
diff --git a/src/main/java/com/mixpanel/android/mpmetrics/MixpanelAPI.java b/src/main/java/com/mixpanel/android/mpmetrics/MixpanelAPI.java
index 360febfd3..605726881 100644
--- a/src/main/java/com/mixpanel/android/mpmetrics/MixpanelAPI.java
+++ b/src/main/java/com/mixpanel/android/mpmetrics/MixpanelAPI.java
@@ -36,6 +36,7 @@
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -222,8 +223,6 @@ public class MixpanelAPI implements FeatureFlagDelegate {
getHttpService(),
new FlagsConfig(options.areFeatureFlagsEnabled(), options.getFeatureFlagsContext()));
- mFeatureFlagManager.loadFlags();
-
if (options.isOptOutTrackingDefault()
&& (hasOptedOutTracking() || !mPersistentIdentity.hasOptOutFlag(token))) {
optOutTracking();
@@ -825,7 +824,10 @@ public void identify(String distinctId, boolean usePeople) {
mPersistentIdentity.setEventsDistinctId(distinctId);
mPersistentIdentity.setAnonymousIdIfAbsent(currentEventsDistinctId);
mPersistentIdentity.markEventsUserIdPresent();
- mFeatureFlagManager.loadFlags();
+ // Ensure app has previously launched in foreground before network call.
+ if (mHasAppForegrounded.get()) {
+ mFeatureFlagManager.loadFlags();
+ }
try {
JSONObject identifyPayload = new JSONObject();
identifyPayload.put("$anon_distinct_id", currentEventsDistinctId);
@@ -2093,6 +2095,11 @@ public boolean isAppInForeground() {
/* package */ void onForeground() {
mSessionMetadata.initSession();
+ // Ensure app has previously launched in foreground before network call.
+ mHasAppForegrounded.set(true);
+ if (mInitialFeatureFlagLoad.compareAndSet(false, true)) {
+ mFeatureFlagManager.loadFlags();
+ }
}
// Package-level access. Used (at least) by MixpanelFCMMessagingService
@@ -2798,6 +2805,10 @@ RemoteService getHttpService() {
private final SessionMetadata mSessionMetadata;
private FeatureFlagManager mFeatureFlagManager;
private RemoteService mHttpService;
+ // Flag to track if app has entered foreground
+ private final AtomicBoolean mHasAppForegrounded = new AtomicBoolean(false);
+ // Flag to track if initial feature flags load has been initiated
+ private final AtomicBoolean mInitialFeatureFlagLoad = new AtomicBoolean(false);
// Maps each token to a singleton MixpanelAPI instance
private static final Map> sInstanceMap =