diff --git a/.github/workflows/build_and_test_sdk.yml b/.github/workflows/build_and_test_sdk.yml
index 337a514f4..c4b9e82ad 100644
--- a/.github/workflows/build_and_test_sdk.yml
+++ b/.github/workflows/build_and_test_sdk.yml
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Install Docker to the Runner
- run: sudo apt-get install docker
+ run: sudo apt-get install containerd.io
- name: Pull Emulator from the Repo
run: docker pull ${{ env.EMULATOR_REPO }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7eaa44fff..5f4642eb7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,40 @@
-## XX.XX.XX
+## 25.4.0
+* ! Minor breaking change ! Removed Secure.ANDROID_ID usage in device id generation. The SDK now exclusively uses random UUIDs for device id generation.
+* ! Minor breaking change ! Server Configuration is now enabled by default. Changes made on SDK Manager > SDK Configuration on your server will affect SDK behavior directly.
+
+* Added a Content feature method "refreshContentZone" that does a manual refresh.
+* Extended server configuration capabilities of the SDK.
+* Added a config method to provide server config in the initialization "setSDKBehaviorSettings(String)".
+* Added a new interface "CountlyNotificationButtonURLHandler" to allow custom handling of URLs when notification buttons are clicked. Could be set by "CountlyConfigPush.setNotificationButtonURLHandler"
+
+* Mitigated an issue that caused PN message data collision if two message with same ID was received.
+
+* Removed the deprecated function "CountlyConfig.setIdMode(idMode)"
+
+* Deprecated the experimental configuration function enableServerConfiguration.
+
+## 25.1.1
+* Mitigated an issue where after closing a content, they were not being fetched again.
+
+## 25.1.0
+* Improved content size management of content blocks.
+
+* Mitigated an issue where, the action bar was overlapping with the content display.
+* Improved the custom CertificateTrustManager to handle domain-specific configurations by supporting hostname-aware checkServerTrusted calls.
+
+## 24.7.8
+* Added a config option to content (setZoneTimerInterval) to set content zone timer. (Experimental!)
+
+## 24.7.7
+* Mitigated an issue where an automatically closed autostopped view's duration could have increased when opening new views
+* Mitigated an issue where, on Android 35 and above, the navigation bar was overlapping with the content display.
+
+## 24.7.6
+* Added support for localization of content blocks.
* Mitigated an issue where visibility could have been wrongly assigned if a view was closed while going to background. (Experimental!)
+* Fixed a bug where passing the global content callback was not possible.
+* Mitigated an issue related to content actions navigation.
+* Mitigated an issue that parsing internal content event segmentation.
## 24.7.5
* ! Minor breaking change ! All active views will now automatically stop when consent for "views" is revoked.
diff --git a/app-kotlin/src/main/java/ly/count/android/demo/kotlin/App.kt b/app-kotlin/src/main/java/ly/count/android/demo/kotlin/App.kt
index 2cfe3aaa5..2a9a26d03 100644
--- a/app-kotlin/src/main/java/ly/count/android/demo/kotlin/App.kt
+++ b/app-kotlin/src/main/java/ly/count/android/demo/kotlin/App.kt
@@ -27,10 +27,9 @@ class App : Application() {
.setDeviceId(
"myDeviceId"
)
- .enableCrashReporting()
- .setRecordAllThreadsWithCrash()
.setLoggingEnabled(true)
- .setViewTracking(false)
+
+ countlyConfig.crashes.enableCrashReporting().enableRecordAllThreadsWithCrash()
Countly.sharedInstance().init(countlyConfig)
}
diff --git a/app-native/src/main/java/ly/count/android/demo/crash/App.java b/app-native/src/main/java/ly/count/android/demo/crash/App.java
index b803a2cb1..4593a8795 100644
--- a/app-native/src/main/java/ly/count/android/demo/crash/App.java
+++ b/app-native/src/main/java/ly/count/android/demo/crash/App.java
@@ -25,9 +25,11 @@ public class App extends Application {
CountlyConfig config = new CountlyConfig(this, COUNTLY_APP_KEY, COUNTLY_SERVER_URL).setDeviceId("4432")
.setLoggingEnabled(true)
- .enableCrashReporting()
- .setViewTracking(true)
+ .enableAutomaticViewTracking()
.setRequiresConsent(false);
+
+ config.crashes.enableCrashReporting();
+
Countly.sharedInstance().init(config);
CountlyNative.initNative(getApplicationContext());
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3449aa7ea..63643611c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -101,6 +101,11 @@
android:label="@string/activity_name_feedback"
android:configChanges="orientation|screenSize"/>
+
+
diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleAutoViewTracking.java b/app/src/main/java/ly/count/android/demo/ActivityExampleAutoViewTracking.java
index 64c570ca9..d9cdf6a31 100644
--- a/app/src/main/java/ly/count/android/demo/ActivityExampleAutoViewTracking.java
+++ b/app/src/main/java/ly/count/android/demo/ActivityExampleAutoViewTracking.java
@@ -4,6 +4,7 @@
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
+import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import ly.count.android.sdk.Countly;
@@ -40,7 +41,6 @@ public void onClickStartView2(View v) {
Toast.makeText(getApplicationContext(), "Clicked startView 2", Toast.LENGTH_SHORT).show();
}
-
public void onClickPauseViewWithID(View v) {
Countly.sharedInstance().views().pauseViewWithID(viewID);
Toast.makeText(getApplicationContext(), "Clicked pauseViewWithID 1", Toast.LENGTH_SHORT).show();
@@ -99,9 +99,8 @@ public void onStop() {
}
@Override
- public void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Countly.sharedInstance().onConfigurationChanged(newConfig);
}
-
}
diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleContentZone.java b/app/src/main/java/ly/count/android/demo/ActivityExampleContentZone.java
new file mode 100644
index 000000000..7d728f672
--- /dev/null
+++ b/app/src/main/java/ly/count/android/demo/ActivityExampleContentZone.java
@@ -0,0 +1,42 @@
+package ly.count.android.demo;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.EditText;
+import androidx.appcompat.app.AppCompatActivity;
+import java.util.UUID;
+import ly.count.android.sdk.Countly;
+
+public class ActivityExampleContentZone extends AppCompatActivity {
+
+ Activity activity;
+ EditText deviceIdEditText;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ activity = this;
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_example_content_zone);
+ deviceIdEditText = findViewById(R.id.editTextDeviceIdContentZone);
+ }
+
+ public void onClickEnterContentZone(View v) {
+ Countly.sharedInstance().contents().enterContentZone();
+ }
+
+ public void onClickExitContentZone(View v) {
+ Countly.sharedInstance().contents().exitContentZone();
+ }
+
+ public void onClickRefreshContentZone(View v) {
+ Countly.sharedInstance().contents().refreshContentZone();
+ }
+
+ public void onClickChangeDeviceIdContentZone(View v) {
+ String deviceId = deviceIdEditText.getText().toString();
+ String newDeviceId = deviceId.isEmpty() ? UUID.randomUUID().toString() : deviceId;
+
+ Countly.sharedInstance().deviceId().setID(newDeviceId);
+ }
+}
diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleFeedback.java b/app/src/main/java/ly/count/android/demo/ActivityExampleFeedback.java
index 0fcab479b..a1932e7b0 100644
--- a/app/src/main/java/ly/count/android/demo/ActivityExampleFeedback.java
+++ b/app/src/main/java/ly/count/android/demo/ActivityExampleFeedback.java
@@ -3,6 +3,7 @@
import android.os.Bundle;
import android.util.Log;
import android.view.View;
+import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.util.List;
@@ -23,6 +24,27 @@ public class ActivityExampleFeedback extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_example_feedback);
+
+ final Button presentSurvey = findViewById(R.id.presentSurvey);
+ presentSurvey.setOnClickListener(new View.OnClickListener() {
+ @Override public void onClick(View v) {
+ Countly.sharedInstance().feedback().presentSurvey(ActivityExampleFeedback.this);
+ }
+ });
+
+ final Button presentRating = findViewById(R.id.presentRating);
+ presentRating.setOnClickListener(new View.OnClickListener() {
+ @Override public void onClick(View v) {
+ Countly.sharedInstance().feedback().presentRating(ActivityExampleFeedback.this);
+ }
+ });
+
+ final Button presentNPS = findViewById(R.id.presentNPS);
+ presentNPS.setOnClickListener(new View.OnClickListener() {
+ @Override public void onClick(View v) {
+ Countly.sharedInstance().feedback().presentNPS(ActivityExampleFeedback.this);
+ }
+ });
}
public void onClickViewOther02(View v) {
diff --git a/app/src/main/java/ly/count/android/demo/ActivityExampleOthers.java b/app/src/main/java/ly/count/android/demo/ActivityExampleOthers.java
index a50b1938c..4cbccbe72 100644
--- a/app/src/main/java/ly/count/android/demo/ActivityExampleOthers.java
+++ b/app/src/main/java/ly/count/android/demo/ActivityExampleOthers.java
@@ -137,12 +137,4 @@ public void onClickUpdateSession(View v) {
public void onClickEndSession(View v) {
Countly.sharedInstance().sessions().endSession();
}
-
- public void onClickFetchContents(View v) {
- Countly.sharedInstance().contents().enterContentZone();
- }
-
- public void onClickExitContents(View v) {
- Countly.sharedInstance().contents().exitContentZone();
- }
}
diff --git a/app/src/main/java/ly/count/android/demo/App.java b/app/src/main/java/ly/count/android/demo/App.java
index b5e63aa66..42f90d236 100644
--- a/app/src/main/java/ly/count/android/demo/App.java
+++ b/app/src/main/java/ly/count/android/demo/App.java
@@ -25,7 +25,8 @@
import java.util.concurrent.ConcurrentHashMap;
import ly.count.android.sdk.Countly;
import ly.count.android.sdk.CountlyConfig;
-import ly.count.android.sdk.CrashFilterCallback;
+import ly.count.android.sdk.CrashData;
+import ly.count.android.sdk.GlobalCrashFilterCallback;
import ly.count.android.sdk.ModuleLog;
import ly.count.android.sdk.messaging.CountlyConfigPush;
import ly.count.android.sdk.messaging.CountlyPush;
@@ -171,17 +172,6 @@ public void onCreate() {
}
}
})
-
- .enableCrashReporting()
- .setRecordAllThreadsWithCrash()
- .setCustomCrashSegment(customCrashSegmentation)
- .setCrashFilterCallback(new CrashFilterCallback() {
- @Override
- public boolean filterCrash(String crash) {
- return crash.contains("crash");
- }
- })
-
.enableAutomaticViewTracking()
// uncomment the line below to enable auto enrolling the user to AB experiments when downloading RC data
//.enrollABOnRCDownload()
@@ -234,6 +224,16 @@ public boolean filterCrash(String crash) {
.setUserProperties(customUserProperties);
+ config.crashes
+ .enableCrashReporting()
+ .enableRecordAllThreadsWithCrash()
+ .setCustomCrashSegmentation(customCrashSegmentation)
+ .setGlobalCrashFilterCallback(new GlobalCrashFilterCallback() {
+ @Override public boolean filterCrash(CrashData crash) {
+ return crash.getStackTrace().contains("secret");
+ }
+ });
+
config.apm.enableAppStartTimeTracking()
.enableForegroundBackgroundTracking()
.setAppStartTimestampOverride(applicationStartTimestamp);
diff --git a/app/src/main/java/ly/count/android/demo/MainActivity.java b/app/src/main/java/ly/count/android/demo/MainActivity.java
index 307e1a668..4b3cd2e78 100644
--- a/app/src/main/java/ly/count/android/demo/MainActivity.java
+++ b/app/src/main/java/ly/count/android/demo/MainActivity.java
@@ -141,4 +141,8 @@ public void onClickButtonDeviceId(View v) {
public void onClickButtonRatings(View v) {
startActivity(new Intent(this, ActivityExampleFeedback.class));
}
+
+ public void onClickContentZone(View v) {
+ startActivity(new Intent(this, ActivityExampleContentZone.class));
+ }
}
diff --git a/app/src/main/res/layout/activity_example_content_zone.xml b/app/src/main/res/layout/activity_example_content_zone.xml
new file mode 100644
index 000000000..27adc9c76
--- /dev/null
+++ b/app/src/main/res/layout/activity_example_content_zone.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_example_feedback.xml b/app/src/main/res/layout/activity_example_feedback.xml
index 72e13ca06..1f0dc79a7 100644
--- a/app/src/main/res/layout/activity_example_feedback.xml
+++ b/app/src/main/res/layout/activity_example_feedback.xml
@@ -1,100 +1,124 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_example_others.xml b/app/src/main/res/layout/activity_example_others.xml
index c249fd265..c34e5f215 100644
--- a/app/src/main/res/layout/activity_example_others.xml
+++ b/app/src/main/res/layout/activity_example_others.xml
@@ -110,14 +110,6 @@
android:onClick="onClickBeginSession"
android:text="Begin Session"
/>
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 23b8a3ac8..e4c0ef77c 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -99,6 +99,14 @@
android:text="Surveys and Ratings"
/>
+
+
manual report
Add a breadcrumb
Feedback examples
+ Content Zone
+ Device ID
diff --git a/gradle.properties b/gradle.properties
index d7097fa31..f78f3b9a4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -22,7 +22,7 @@ org.gradle.configureondemand=true
android.useAndroidX=true
android.enableJetifier=true
# RELEASE FIELD SECTION
-VERSION_NAME=24.7.5
+VERSION_NAME=25.4.0
GROUP=ly.count.android
POM_URL=https://github.com/Countly/countly-sdk-android
POM_SCM_URL=https://github.com/Countly/countly-sdk-android
diff --git a/sdk/build.gradle b/sdk/build.gradle
index e052f9a3f..95dc8cda7 100644
--- a/sdk/build.gradle
+++ b/sdk/build.gradle
@@ -47,7 +47,7 @@ android {
}
}
-def mockitoVersion = "2.28.2"
+def mockitoVersion = "4.11.0"
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java
index f0ae4d761..52745dd40 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java
@@ -77,6 +77,34 @@ public void setUp() {
@Override public boolean getTrackingEnabled() {
return true;
}
+
+ @Override public boolean getSessionTrackingEnabled() {
+ return false;
+ }
+
+ @Override public boolean getViewTrackingEnabled() {
+ return false;
+ }
+
+ @Override public boolean getCustomEventTrackingEnabled() {
+ return false;
+ }
+
+ @Override public boolean getContentZoneEnabled() {
+ return false;
+ }
+
+ @Override public boolean getCrashReportingEnabled() {
+ return true;
+ }
+
+ @Override public boolean getLocationTrackingEnabled() {
+ return true;
+ }
+
+ @Override public boolean getRefreshContentZoneEnabled() {
+ return true;
+ }
};
Countly.sharedInstance().setLoggingEnabled(true);
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionQueueTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionQueueTests.java
index b095de9ea..2341079d8 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionQueueTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionQueueTests.java
@@ -38,7 +38,7 @@ of this software and associated documentation files (the "Software"), to deal
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@@ -249,13 +249,13 @@ public void testUpdateSession_checkInternalState() {
@Test
public void testUpdateSession_zeroDuration() {
connQ.updateSession(0);
- verifyZeroInteractions(connQ.getExecutor(), connQ.storageProvider);
+ verifyNoInteractions(connQ.getExecutor(), connQ.storageProvider);
}
@Test
public void testUpdateSession_negativeDuration() {
connQ.updateSession(-1);
- verifyZeroInteractions(connQ.getExecutor(), connQ.storageProvider);
+ verifyNoInteractions(connQ.getExecutor(), connQ.storageProvider);
}
//@Test
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/CountlyConfigTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/CountlyConfigTests.java
index 1cd4da4c0..63c58dfb3 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/CountlyConfigTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/CountlyConfigTests.java
@@ -110,7 +110,6 @@ public boolean filterCrash(String crash) {
config.setCountlyStore(cs);
config.checkForNativeCrashDumps(false);
config.setDeviceId(s[2]);
- config.setIdMode(DeviceIdType.DEVELOPER_SUPPLIED);
config.setStarRatingSessionLimit(1335);
config.setStarRatingCallback(rc);
config.setStarRatingTextDismiss(s[3]);
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTests.java
index c2f0244f2..54388a139 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTests.java
@@ -43,7 +43,7 @@ of this software and associated documentation files (the "Software"), to deal
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@@ -460,7 +460,7 @@ public void testSendEventsIfNeeded_emptyQueue() {
mCountly.moduleRequestQueue.sendEventsIfNeeded(false);
verify(mCountly.moduleEvents.storageProvider, times(0)).getEventsForRequestAndEmptyEventQueue();
- verifyZeroInteractions(requestQueueProvider);
+ verifyNoInteractions(requestQueueProvider);
}
/**
@@ -477,7 +477,7 @@ public void testSendEventsIfNeeded_lessThanThreshold() {
mCountly.moduleRequestQueue.sendEventsIfNeeded(false);
verify(mCountly.moduleEvents.storageProvider, times(0)).getEventsForRequestAndEmptyEventQueue();
- verifyZeroInteractions(requestQueueProvider);
+ verifyNoInteractions(requestQueueProvider);
}
@Test
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTimerTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTimerTests.java
index 80d482fda..1bb96422f 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTimerTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTimerTests.java
@@ -39,7 +39,7 @@ public void startTimer_validDelay() {
Runnable mockRunnable = Mockito.mock(Runnable.class);
countlyTimer.startTimer(1, mockRunnable, mockLog);
- Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [1000 ms]");
+ Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [1000 ms], initialDelay: [0 ms]");
}
@Test
@@ -47,7 +47,7 @@ public void startTimer_invalidDelay() {
Runnable mockRunnable = Mockito.mock(Runnable.class);
countlyTimer.startTimer(-1, mockRunnable, mockLog);
- Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [1000 ms]");
+ Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [1000 ms], initialDelay: [0 ms]");
}
@Test
@@ -55,7 +55,7 @@ public void startTimer() {
Runnable mockRunnable = Mockito.mock(Runnable.class);
countlyTimer.startTimer(99, mockRunnable, mockLog);
- Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [99000 ms]");
+ Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [99000 ms], initialDelay: [0 ms]");
}
@Test
@@ -64,7 +64,7 @@ public void startTimer_withTimerDelayMS() {
Runnable mockRunnable = Mockito.mock(Runnable.class);
countlyTimer.startTimer(1, mockRunnable, mockLog);
- Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [500 ms]");
+ Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [500 ms], initialDelay: [0 ms]");
}
/**
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/DeviceIdTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/DeviceIdTests.java
index 6f13fa7ec..9ddc2a852 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/DeviceIdTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/DeviceIdTests.java
@@ -23,14 +23,14 @@ public class DeviceIdTests {
@Before
public void setUp() {
- store = TestUtils.getCountyStore();
+ store = TestUtils.getCountlyStore();
store.clear();
Countly.sharedInstance().halt();
Countly.sharedInstance().setLoggingEnabled(true);
openUDIDProvider = new OpenUDIDProvider() {
- @Override public String getOpenUDID() {
+ @Override public String getUUID() {
return currentOpenUDIDValue;
}
};
@@ -106,7 +106,7 @@ public void getType() {
store.clear();
assertEquals(DeviceIdType.OPEN_UDID, new DeviceId(null, store, mock(ModuleLog.class), new OpenUDIDProvider() {
- @Override public String getOpenUDID() {
+ @Override public String getUUID() {
return "abc";
}
}).getType());
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleAPMTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleAPMTests.java
index 5ef29add8..7697b834a 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleAPMTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleAPMTests.java
@@ -249,6 +249,46 @@ public void clearNetworkTraces() {
Assert.assertEquals(0, mCountly.moduleAPM.networkTraces.size());
}
+ /**
+ * Test that custom trace key is truncated to the correct length
+ * Max segmentation values limit is applied, with server config
+ * Validate custom metrics are merged and truncated to the correct length
+ * Validate that the custom trace is sent to the server with correct values
+ */
+ @Test
+ public void serverConfig_customTrace_keyLength_segmentationValues() throws JSONException {
+ CountlyConfig mConfig = TestUtils.createBaseConfig();
+ mConfig.immediateRequestGenerator = ModuleConfigurationTests.createIRGForSpecificResponse(new ServerConfigBuilder().keyLengthLimit(5).segmentationValuesLimit(3).build());
+ mCountly = new Countly().init(mConfig);
+ requestQueueProvider = TestUtils.setRequestQueueProviderToMock(mCountly, mock(RequestQueueProvider.class));
+
+ String key = "a_trace_to_track";
+ mCountly.apm().startTrace(key);
+
+ Assert.assertTrue(mCountly.moduleAPM.codeTraces.containsKey(key));
+
+ Map customMetrics = new HashMap<>();
+ customMetrics.put("a_trace_to_look", 1);
+ customMetrics.put("a_trace_to_inspect", 2);
+ customMetrics.put("look_here", 3);
+ customMetrics.put("microphone_show", 4);
+ customMetrics.put("berserk", 5);
+
+ mCountly.apm().endTrace(key, customMetrics);
+
+ customMetrics.clear();
+ if (Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT <= 25) {
+ customMetrics.put("micro", 4);
+ customMetrics.put("berse", 5);
+ customMetrics.put("look_", 3);
+ } else {
+ customMetrics.put("look_", 3);
+ customMetrics.put("a_tra", 2);
+ customMetrics.put("micro", 4);
+ }
+ verify(requestQueueProvider).sendAPMCustomTrace(eq("a_tra"), anyLong(), anyLong(), anyLong(), eq(customMetricsToString(customMetrics)));
+ }
+
/**
* Test that custom trace key is truncated to the correct length
* Max segmentation values limit is applied,
@@ -352,9 +392,13 @@ public void internalLimits_startNetworkTrace_keyLength() throws JSONException {
validateNetworkRequest(0, "a_tra", -1, 200, 123, 456);
}
- private void validateNetworkRequest(int rqIdx, String key, long duration, int responseCode, int requestPayloadSize, int responsePayloadSize) throws JSONException {
+ protected static void validateNetworkRequest(int rqIdx, String key, long duration, int responseCode, int requestPayloadSize, int responsePayloadSize) throws JSONException {
+ validateNetworkRequest(rqIdx, rqIdx + 1, key, duration, responseCode, requestPayloadSize, responsePayloadSize);
+ }
+
+ protected static void validateNetworkRequest(int rqIdx, int rqCount, String key, long duration, int responseCode, int requestPayloadSize, int responsePayloadSize) throws JSONException {
Map[] RQ = TestUtils.getCurrentRQ();
- Assert.assertEquals(rqIdx + 1, RQ.length);
+ Assert.assertEquals(rqCount, RQ.length);
JSONObject apm = new JSONObject(RQ[rqIdx].get("apm"));
Assert.assertEquals(key, apm.getString("name"));
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleAttributionTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleAttributionTests.java
index a225bbbd2..911c505e6 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleAttributionTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleAttributionTests.java
@@ -117,7 +117,6 @@ public void basicInitOnlyIABadValues() {
RequestQueueProvider rqp = mock(RequestQueueProvider.class);
Countly mCountly = new Countly().init(TestUtils.createAttributionCountlyConfig(false, null, null, rqp, null, null, ia_1));
-
verify(rqp, times(1)).sendIndirectAttribution(ia_1_string);
verify(rqp, times(0)).sendDirectAttributionLegacy(any(String.class), any(String.class));
}
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java
index 4b74e725e..db70edcc1 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java
@@ -1,7 +1,15 @@
package ly.count.android.sdk;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.After;
@@ -9,226 +17,1043 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-
-import static org.mockito.Mockito.mock;
+import org.mockito.Mockito;
@RunWith(AndroidJUnit4.class)
public class ModuleConfigurationTests {
- CountlyStore countlyStore;
+ private CountlyStore countlyStore;
+ private Countly countly;
@Before
public void setUp() {
- countlyStore = new CountlyStore(TestUtils.getContext(), mock(ModuleLog.class));
+ countlyStore = TestUtils.getCountlyStore();
countlyStore.clear();
+ Countly.sharedInstance().halt();
}
@After
public void tearDown() {
+ TestUtils.getCountlyStore().clear();
+ Countly.sharedInstance().halt();
}
+ // ================ Basic Configuration Tests ================
+
/**
- * Default values when server config is disabled and storage is empty
- * No server connection
+ * Test default configuration when server config is disabled and storage is empty
*/
@Test
- public void init_disabled_storageEmpty() {
+ public void defaultConfig_WhenServerConfigDisabledAndStorageEmpty() {
countlyStore.clear();
- CountlyConfig config = TestUtils.createConfigurationConfig(false, null);
- Countly countly = (new Countly()).init(config);
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(null);
+ countly = new Countly().init(config);
- Assert.assertFalse(countly.moduleConfiguration.serverConfigEnabled);
Assert.assertNull(countlyStore.getServerConfig());
- assertConfigDefault(countly);
+ assertDefaultConfigValues(countly);
}
/**
- * Default values when server config is enabled and storage is empty
- * No server connection
+ * Test default configuration when server config is enabled and storage is empty
*/
@Test
- public void init_enabled_storageEmpty() {
- CountlyConfig config = TestUtils.createConfigurationConfig(true, null);
- Countly countly = (new Countly()).init(config);
+ public void defaultConfig_WhenServerConfigEnabledAndStorageEmpty() {
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(null);
+ config.enableServerConfiguration();
+ countly = new Countly().init(config);
- Assert.assertTrue(countly.moduleConfiguration.serverConfigEnabled);
Assert.assertNull(countlyStore.getServerConfig());
- assertConfigDefault(countly);
+ assertDefaultConfigValues(countly);
}
+ // ================ Server Configuration Tests ================
+
/**
- * Server config enabled
- * All config properties are default/allowing
- * No server connection
- *
- * @throws JSONException
+ * Test configuration when server config is enabled and all properties are allowing
*/
@Test
- public void init_enabled_storageAllowing() throws JSONException {
- countlyStore.setServerConfig(getStorageString(true, true));
- CountlyConfig config = TestUtils.createConfigurationConfig(true, null);
- Countly countly = (new Countly()).init(config);
+ public void serverConfig_WhenEnabledAndAllPropertiesAllowing() throws JSONException {
+ countlyStore.setServerConfig(createStorageConfig(true, true, true));
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(null);
+ config.enableServerConfiguration();
+ countly = new Countly().init(config);
- Assert.assertTrue(countly.moduleConfiguration.serverConfigEnabled);
Assert.assertNotNull(countlyStore.getServerConfig());
- assertConfigDefault(countly);
+ assertDefaultConfigValues(countly);
}
/**
- * Server config enabled
- * All config properties are off default/disabling
- * No server connection
- *
- * @throws JSONException
+ * Test configuration when server config is enabled and all properties are forbidding
*/
@Test
- public void init_enabled_storageForbidding() throws JSONException {
- countlyStore.setServerConfig(getStorageString(false, false));
- CountlyConfig config = TestUtils.createConfigurationConfig(true, null);
- Countly countly = (new Countly()).init(config);
+ public void serverConfig_WhenEnabledAndAllPropertiesForbidding() throws JSONException {
+ countlyStore.setServerConfig(createStorageConfig(false, false, false));
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(null);
+ config.enableServerConfiguration();
+ countly = new Countly().init(config);
- Assert.assertTrue(countly.moduleConfiguration.serverConfigEnabled);
Assert.assertNotNull(countlyStore.getServerConfig());
Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled());
Assert.assertFalse(countly.moduleConfiguration.getTrackingEnabled());
+ Assert.assertFalse(countly.moduleConfiguration.getCrashReportingEnabled());
}
/**
- * Server config disabled
- * All config properties are default/allowing
- * No server connection
- *
- * @throws JSONException
+ * Test configuration when server config is disabled and all properties are allowing
*/
@Test
- public void init_disabled_storageAllowing() throws JSONException {
- countlyStore.setServerConfig(getStorageString(true, true));
- CountlyConfig config = TestUtils.createConfigurationConfig(false, null);
- Countly countly = Countly.sharedInstance().init(config);
+ public void serverConfig_WhenDisabledAndAllPropertiesAllowing() throws JSONException {
+ countlyStore.setServerConfig(createStorageConfig(true, true, true));
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(null);
+ countly = Countly.sharedInstance().init(config);
- Assert.assertFalse(countly.moduleConfiguration.serverConfigEnabled);
Assert.assertNotNull(countlyStore.getServerConfig());
- assertConfigDefault(countly);
+ assertDefaultConfigValues(countly);
}
/**
- * Server config disabled
- * All config properties are off default/disabling
- * No server connection
- *
- * @throws JSONException
+ * Test configuration when server config is disabled and all properties are forbidding
+ * This test is expected to fail as server config is deprecated
*/
- @Test
- public void init_disabled_storageForbidding() throws JSONException {
- countlyStore.setServerConfig(getStorageString(false, false));
- CountlyConfig config = TestUtils.createConfigurationConfig(false, null);
- Countly countly = (new Countly()).init(config);
+ @Test(expected = AssertionError.class)
+ public void serverConfig_WhenDisabledAndAllPropertiesForbidding() throws JSONException {
+ countlyStore.setServerConfig(createStorageConfig(false, false, false));
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(null);
+ countly = new Countly().init(config);
- Assert.assertFalse(countly.moduleConfiguration.serverConfigEnabled);
Assert.assertNotNull(countlyStore.getServerConfig());
- assertConfigDefault(countly);
+ assertDefaultConfigValues(countly);
+ }
+
+ // ================ Server Configuration Validation Tests ================
+
+ /**
+ * Tests that default server configuration values are correctly applied when no custom configuration is provided.
+ * Verifies that all default values match the expected configuration.
+ */
+ @Test
+ public void serverConfig_DefaultValues() throws InterruptedException {
+ countly = new Countly().init(TestUtils.createBaseConfig().setLoggingEnabled(false));
+ Thread.sleep(2000); // simulate sdk initialization delay
+ new ServerConfigBuilder().defaults().validateAgainst(countly);
+ }
+
+ /**
+ * Tests that custom server configuration values are correctly applied when provided directly.
+ * Verifies that the configuration is properly parsed and applied to the SDK.
+ */
+ @Test
+ public void serverConfig_ProvidedValues() throws InterruptedException, JSONException {
+ initServerConfigWithValues(CountlyConfig::setSDKBehaviorSettings);
+ }
+
+ /**
+ * Tests that server configuration values are correctly applied when using an immediate request generator.
+ * Verifies that the configuration is properly handled when received through the request generator.
+ */
+ @Test
+ public void serverConfig_WithImmediateRequestGenerator() throws InterruptedException, JSONException {
+ initServerConfigWithValues((countlyConfig, serverConfig) -> {
+ countlyConfig.immediateRequestGenerator = createIRGForSpecificResponse(serverConfig);
+ });
+ }
+
+ /**
+ * Tests that all features work correctly with default server configuration.
+ * Verifies that all SDK features (sessions, events, views, crashes, etc.) function as expected
+ * when using default configuration values.
+ */
+ @Test
+ public void serverConfig_Defaults_AllFeatures() throws JSONException, InterruptedException {
+ base_allFeatures((sc) -> {
+ }, 1, 1, 1, 2, 1);
+ }
+
+ /**
+ * Tests that all features are properly disabled when explicitly configured to be disabled.
+ * Verifies that no requests are generated and no data is collected when all features are disabled.
+ */
+ @Test
+ public void disable_allFeatures() throws JSONException, InterruptedException {
+ ServerConfigBuilder sc = new ServerConfigBuilder();
+ sc.networking(false).sessionTracking(false).customEventTracking(false).viewTracking(false)
+ .crashReporting(false).locationTracking(false).contentZone(false).refreshContentZone(false).tracking(false).consentRequired(false);
+
+ int[] counts = setupTest_allFeatures(sc.buildJson());
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, TestUtils.getCountlyStore().getEventQueueSize());
+
+ flow_allFeatures();
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+
+ immediateFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ feedbackFlow_allFeatures();
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ validateCounts(counts, 0, 0, 0, 0, 1);
}
/**
- * Making sure that a downloaded configuration is persistently stored across init's
+ * Tests that consent requirement is properly handled when enabled.
+ * Verifies that:
+ * 1. Initial consent request is sent
+ * 2. No data is collected until consent is given
+ * 3. Location is properly handled with empty value
*/
@Test
- public void scenario_1() {
- //initial state is fresh
+ public void consentEnabled_allFeatures() throws JSONException, InterruptedException {
+ ServerConfigBuilder sc = new ServerConfigBuilder();
+ sc.consentRequired(true);
+
+ int[] counts = setupTest_allFeatures(sc.buildJson());
+
+ Assert.assertEquals(2, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+ ModuleConsentTests.validateConsentRequest(TestUtils.commonDeviceId, 0, new boolean[] { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false });
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", ""), 1);
+
+ flow_allFeatures();
+ immediateFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+ feedbackFlow_allFeatures();
+
+ Assert.assertEquals(2, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ validateCounts(counts, 1, 0, 0, 0, 1);
+ }
+
+ /**
+ * Tests that session tracking is properly disabled when configured.
+ * Verifies that:
+ * 1. No session requests are generated
+ * 2. Other features (events, views, crashes) continue to work
+ * 3. Request counts and order are maintained correctly
+ */
+ @Test
+ public void sessionsDisabled_allFeatures() throws JSONException, InterruptedException {
+ ServerConfigBuilder sc = new ServerConfigBuilder();
+ sc.sessionTracking(false);
+ int[] counts = setupTest_allFeatures(sc.buildJson());
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, TestUtils.getCountlyStore().getEventQueueSize());
+
+ String stackTrace = flow_allFeatures();
+
+ ModuleCrashTests.validateCrash(stackTrace, "", false, false, 7, 0, TestUtils.map(), 0, TestUtils.map(), new ArrayList<>());
+ validateEventInRQ("test_event", TestUtils.map(), 1, 7, 0, 2);
+ validateEventInRQ("[CLY]_view", TestUtils.map("name", "test_view", "segment", "Android", "visit", "1"), 1, 7, 1, 2);
+ ModuleUserProfileTests.validateUserProfileRequest(2, 7, TestUtils.map(), TestUtils.map("test_property", "test_value"));
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", "gps"), 3);
+ ModuleAPMTests.validateNetworkRequest(4, 7, "test_trace", 1111, 400, 2000, 1111);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("attribution_data", "test_data"), 5);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("key", "value"), 6);
+
+ Assert.assertEquals(7, TestUtils.getCurrentRQ().length);
+
+ immediateFlow_allFeatures();
+
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ feedbackFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ validateEventInRQ("[CLY]_star_rating", TestUtils.map("platform", "android", "app_version", Countly.DEFAULT_APP_VERSION, "rating", "5", "widget_id", "test", "contactMe", true, "email", "test", "comment", "test"), 7, 8, 0, 2);
+ validateEventInRQ("[CLY]_nps", TestUtils.map("app_version", Countly.DEFAULT_APP_VERSION, "widget_id", "test", "closed", "1", "platform", "android"), 7, 8, 1, 2);
+
+ Assert.assertEquals(8, TestUtils.getCurrentRQ().length);
+
+ validateCounts(counts, 1, 1, 1, 2, 1);
+ }
+
+ /**
+ * Tests that crash reporting is properly disabled when configured.
+ * Verifies that:
+ * 1. Crash reports are not sent
+ * 2. Other features continue to work normally
+ * 3. Request counts and order are maintained correctly
+ */
+ @Test
+ public void crashReportingDisabled_allFeatures() throws JSONException, InterruptedException {
+ ServerConfigBuilder sc = new ServerConfigBuilder();
+ sc.crashReporting(false);
+ int[] counts = setupTest_allFeatures(sc.buildJson());
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, TestUtils.getCountlyStore().getEventQueueSize());
+
+ flow_allFeatures();
+
+ ModuleSessionsTests.validateSessionBeginRequest(0, TestUtils.commonDeviceId);
+ validateEventInRQ("[CLY]_orientation", TestUtils.map("mode", "portrait"), 1, 7, 0, 3);
+ validateEventInRQ("test_event", TestUtils.map(), 1, 7, 1, 3);
+ validateEventInRQ("[CLY]_view", TestUtils.map("name", "test_view", "segment", "Android", "visit", "1", "start", "1"), 1, 7, 2, 3);
+ ModuleUserProfileTests.validateUserProfileRequest(2, 7, TestUtils.map(), TestUtils.map("test_property", "test_value"));
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", "gps"), 3);
+ ModuleAPMTests.validateNetworkRequest(4, 7, "test_trace", 1111, 400, 2000, 1111);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("attribution_data", "test_data"), 5);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("key", "value"), 6);
+
+ Assert.assertEquals(7, TestUtils.getCurrentRQ().length);
+
+ immediateFlow_allFeatures();
+
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ feedbackFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ validateEventInRQ("[CLY]_star_rating", TestUtils.map("platform", "android", "app_version", Countly.DEFAULT_APP_VERSION, "rating", "5", "widget_id", "test", "contactMe", true, "email", "test", "comment", "test"), 7, 8, 0, 2);
+ validateEventInRQ("[CLY]_nps", TestUtils.map("app_version", Countly.DEFAULT_APP_VERSION, "widget_id", "test", "closed", "1", "platform", "android"), 7, 8, 1, 2);
+
+ Assert.assertEquals(8, TestUtils.getCurrentRQ().length);
+
+ validateCounts(counts, 1, 1, 1, 2, 1);
+ }
+
+ /**
+ * Tests that view tracking is properly disabled when configured.
+ * Verifies that:
+ * 1. View events are not sent
+ * 2. Other features continue to work normally
+ * 3. Request counts and order are maintained correctly
+ */
+ @Test
+ public void viewTrackingDisabled_allFeatures() throws JSONException, InterruptedException {
+ ServerConfigBuilder sc = new ServerConfigBuilder();
+ sc.viewTracking(false);
+ int[] counts = setupTest_allFeatures(sc.buildJson());
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, TestUtils.getCountlyStore().getEventQueueSize());
+
+ String stackTrace = flow_allFeatures();
+
+ ModuleSessionsTests.validateSessionBeginRequest(0, TestUtils.commonDeviceId);
+ ModuleCrashTests.validateCrash(stackTrace, "", false, false, 8, 1, TestUtils.map(), 0, TestUtils.map(), new ArrayList<>());
+ validateEventInRQ("[CLY]_orientation", TestUtils.map("mode", "portrait"), 2, 8, 0, 2);
+ validateEventInRQ("test_event", TestUtils.map(), 2, 8, 1, 2);
+ ModuleUserProfileTests.validateUserProfileRequest(3, 8, TestUtils.map(), TestUtils.map("test_property", "test_value"));
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", "gps"), 4);
+ ModuleAPMTests.validateNetworkRequest(5, 8, "test_trace", 1111, 400, 2000, 1111);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("attribution_data", "test_data"), 6);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("key", "value"), 7);
+
+ Assert.assertEquals(8, TestUtils.getCurrentRQ().length);
+
+ immediateFlow_allFeatures();
+
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ feedbackFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ validateEventInRQ("[CLY]_star_rating", TestUtils.map("platform", "android", "app_version", Countly.DEFAULT_APP_VERSION, "rating", "5", "widget_id", "test", "contactMe", true, "email", "test", "comment", "test"), 8, 9, 0, 2);
+ validateEventInRQ("[CLY]_nps", TestUtils.map("app_version", Countly.DEFAULT_APP_VERSION, "widget_id", "test", "closed", "1", "platform", "android"), 8, 9, 1, 2);
+
+ Assert.assertEquals(9, TestUtils.getCurrentRQ().length);
+
+ validateCounts(counts, 1, 1, 1, 2, 1);
+ }
+
+ /**
+ * Tests that custom event tracking is properly disabled when configured.
+ * Verifies that:
+ * 1. Custom events are not sent
+ * 2. Other features continue to work normally
+ * 3. Request counts and order are maintained correctly
+ */
+ @Test
+ public void customEventTrackingDisabled_allFeatures() throws JSONException, InterruptedException {
+ ServerConfigBuilder sc = new ServerConfigBuilder();
+ sc.customEventTracking(false);
+ int[] counts = setupTest_allFeatures(sc.buildJson());
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, TestUtils.getCountlyStore().getEventQueueSize());
+
+ String stackTrace = flow_allFeatures();
+
+ ModuleSessionsTests.validateSessionBeginRequest(0, TestUtils.commonDeviceId);
+ ModuleCrashTests.validateCrash(stackTrace, "", false, false, 8, 1, TestUtils.map(), 0, TestUtils.map(), new ArrayList<>());
+ validateEventInRQ("[CLY]_orientation", TestUtils.map("mode", "portrait"), 2, 8, 0, 2);
+ validateEventInRQ("[CLY]_view", TestUtils.map("name", "test_view", "segment", "Android", "visit", "1", "start", "1"), 2, 8, 1, 2);
+ ModuleUserProfileTests.validateUserProfileRequest(3, 8, TestUtils.map(), TestUtils.map("test_property", "test_value"));
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", "gps"), 4);
+ ModuleAPMTests.validateNetworkRequest(5, 8, "test_trace", 1111, 400, 2000, 1111);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("attribution_data", "test_data"), 6);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("key", "value"), 7);
+
+ Assert.assertEquals(8, TestUtils.getCurrentRQ().length);
+
+ immediateFlow_allFeatures();
+
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ feedbackFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ validateEventInRQ("[CLY]_star_rating", TestUtils.map("platform", "android", "app_version", Countly.DEFAULT_APP_VERSION, "rating", "5", "widget_id", "test", "contactMe", true, "email", "test", "comment", "test"), 8, 9, 0, 2);
+ validateEventInRQ("[CLY]_nps", TestUtils.map("app_version", Countly.DEFAULT_APP_VERSION, "widget_id", "test", "closed", "1", "platform", "android"), 8, 9, 1, 2);
+
+ Assert.assertEquals(9, TestUtils.getCurrentRQ().length);
+
+ validateCounts(counts, 1, 1, 1, 2, 1);
+ }
+
+ /**
+ * Tests that networking is properly disabled when configured.
+ * Verifies that no network requests are generated when networking is disabled.
+ */
+ @Test
+ public void networkingDisabled_allFeatures() throws JSONException, InterruptedException {
+ base_allFeatures((sc) -> sc.networking(false), 0, 0, 0, 0, 1);
+ }
+
+ /**
+ * Tests that location tracking is properly disabled when configured.
+ * Verifies that:
+ * 1. Location updates are not sent
+ * 2. Location is properly cleared
+ * 3. Other features continue to work normally
+ */
+ @Test
+ public void locationTrackingDisabled_allFeatures() throws JSONException, InterruptedException {
+ ServerConfigBuilder sc = new ServerConfigBuilder();
+ sc.locationTracking(false);
+ int[] counts = setupTest_allFeatures(sc.buildJson());
+
+ Assert.assertEquals(1, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", ""), 0);
+
+ String stackTrace = flow_allFeatures();
+
+ ModuleSessionsTests.validateSessionBeginRequest(1, TestUtils.commonDeviceId);
+ ModuleCrashTests.validateCrash(stackTrace, "", false, false, 8, 2, TestUtils.map(), 0, TestUtils.map(), new ArrayList<>());
+ validateEventInRQ("[CLY]_orientation", TestUtils.map("mode", "portrait"), 3, 8, 0, 3);
+ validateEventInRQ("test_event", TestUtils.map(), 3, 8, 1, 3);
+ validateEventInRQ("[CLY]_view", TestUtils.map("name", "test_view", "segment", "Android", "visit", "1", "start", "1"), 3, 8, 2, 3);
+ ModuleUserProfileTests.validateUserProfileRequest(4, 8, TestUtils.map(), TestUtils.map("test_property", "test_value"));
+ ModuleAPMTests.validateNetworkRequest(5, 8, "test_trace", 1111, 400, 2000, 1111);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("attribution_data", "test_data"), 6);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("key", "value"), 7);
+
+ Assert.assertEquals(8, TestUtils.getCurrentRQ().length);
+
+ immediateFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ feedbackFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ validateEventInRQ("[CLY]_star_rating", TestUtils.map("platform", "android", "app_version", Countly.DEFAULT_APP_VERSION, "rating", "5", "widget_id", "test", "contactMe", true, "email", "test", "comment", "test"), 8, 9, 0, 2);
+ validateEventInRQ("[CLY]_nps", TestUtils.map("app_version", Countly.DEFAULT_APP_VERSION, "widget_id", "test", "closed", "1", "platform", "android"), 8, 9, 1, 2);
+
+ Assert.assertEquals(9, TestUtils.getCurrentRQ().length);
+
+ validateCounts(counts, 1, 1, 1, 2, 1);
+ }
+
+ /**
+ * Tests that tracking is properly disabled when configured.
+ * Verifies that no tracking-related requests are generated when tracking is disabled.
+ */
+ @Test
+ public void trackingDisabled_allFeatures() throws JSONException, InterruptedException {
+ ServerConfigBuilder sc = new ServerConfigBuilder();
+ sc.tracking(false);
+ int[] counts = setupTest_allFeatures(sc.buildJson());
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ flow_allFeatures();
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+
+ immediateFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+ feedbackFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ Thread.sleep(1000); // wait for immediate requests to be processed
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+
+ Assert.assertEquals(1, counts[0]); // health check request
+ Assert.assertEquals(1, counts[1]);
+ Assert.assertEquals(1, counts[2]);
+ Assert.assertEquals(2, counts[3]);
+ Assert.assertEquals(1, counts[4]); // server config request
+ }
+
+ /**
+ * Tests that content zone refresh is properly disabled when configured.
+ * Verifies that content zone refresh requests are not generated.
+ */
+ @Test
+ public void refreshContentZoneDisabled_allFeatures() throws JSONException, InterruptedException {
+ base_allFeatures((sc) -> sc.refreshContentZone(false), 1, 1, 1, 1, 1);
+ }
+
+ /**
+ * Tests that content zone is properly enabled when configured.
+ * Verifies that content zone requests are generated with the correct frequency.
+ */
+ @Test
+ public void contentZoneEnabled_allFeatures() throws JSONException, InterruptedException {
+ base_allFeatures((sc) -> sc.contentZone(true), 1, 1, 1, 4, 1);
+ }
+
+ // ================ Configuration Persistence Tests ================
+
+ /**
+ * Test that downloaded configuration persists across multiple initializations
+ */
+ @Test
+ public void configurationPersistence_AcrossMultipleInits() {
+ // Initial state should be fresh
Assert.assertNull(countlyStore.getServerConfig());
- //first init fails receiving config, config getters return defaults, store is empty
+ // First init fails receiving config, should return defaults
initAndValidateConfigParsingResult(null, false);
- //second init succeeds receiving config
- Countly countly = initAndValidateConfigParsingResult("{'v':1,'t':2,'c':{'tracking':false,'networking':false}}", true);
+ // Second init succeeds receiving config
+ countly = initAndValidateConfigParsingResult("{'v':1,'t':2,'c':{'tracking':false,'networking':false}}", true);
Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled());
Assert.assertFalse(countly.moduleConfiguration.getTrackingEnabled());
- //third init is lacking a connection but still has the previously saved values
- CountlyConfig config = TestUtils.createConfigurationConfig(true, null);
+ // Third init lacks connection but should have previously saved values
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(null);
+ config.enableServerConfiguration();
countly = new Countly().init(config);
Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled());
Assert.assertFalse(countly.moduleConfiguration.getTrackingEnabled());
- //fourth init updates config values
+ // Fourth init updates config values
countly = initAndValidateConfigParsingResult("{'v':1,'t':2,'c':{'tracking':true,'networking':false}}", true);
Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled());
Assert.assertTrue(countly.moduleConfiguration.getTrackingEnabled());
}
+ // ================ Tracking Configuration Tests ================
+
/**
- * With tracking disabled, nothing should be written to the request and event queues
+ * Test that nothing is written to queues when tracking is disabled
*/
@Test
- public void validatingTrackingConfig() throws JSONException {
- //nothing in queues initially
+ public void trackingDisabled_NoQueueWrites() throws JSONException {
Assert.assertEquals("", countlyStore.getRequestQueueRaw());
Assert.assertEquals(0, countlyStore.getEvents().length);
- countlyStore.setServerConfig(getStorageString(false, false));
-
- CountlyConfig config = TestUtils.createConfigurationConfig(true, null);
- Countly countly = (new Countly()).init(config);
+ countlyStore.setServerConfig(createStorageConfig(false, false, false));
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(null);
+ config.enableServerConfiguration();
+ countly = new Countly().init(config);
Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled());
Assert.assertFalse(countly.moduleConfiguration.getTrackingEnabled());
+ Assert.assertFalse(countly.moduleConfiguration.getCrashReportingEnabled());
- //try events
+ // Try various operations that should be blocked
countly.events().recordEvent("d");
countly.events().recordEvent("1");
-
- //try a non event recording
countly.crashes().recordHandledException(new Exception());
-
- //try a direct request
countly.requestQueue().addDirectRequest(new HashMap<>());
-
countly.requestQueue().attemptToSendStoredRequests();
Assert.assertEquals("", countlyStore.getRequestQueueRaw());
Assert.assertEquals(0, countlyStore.getEvents().length);
}
+ // ================ Crash Reporting Tests ================
+
/**
- * Making sure that bad config responses are rejected
+ * Test unhandled crash reporting when crashes are disabled
*/
@Test
- public void init_enabled_rejectingRequests() {
- //{"v":1,"t":2,"c":{"aa":"bb"}}
+ public void crashReporting_UnhandledCrashesWhenDisabled() throws JSONException {
+ AtomicInteger callCount = new AtomicInteger(0);
+ RuntimeException unhandledException = new RuntimeException("Simulated unhandled exception");
+
+ Thread threadThrows = new Thread(() -> {
+ throw unhandledException;
+ });
+
+ Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
+ Assert.assertEquals(unhandledException, throwable);
+ Assert.assertEquals(threadThrows, thread);
+ callCount.incrementAndGet();
+ });
+
+ TestUtils.getCountlyStore().setServerConfig(createStorageConfig(true, true, false));
+ CountlyConfig config = TestUtils.createBaseConfig();
+ config.enableServerConfiguration().setEventQueueSizeToSend(2);
+ config.crashes.enableCrashReporting();
+ countly = new Countly().init(config);
+
+ Assert.assertTrue(countly.moduleConfiguration.getNetworkingEnabled());
+ Assert.assertTrue(countly.moduleConfiguration.getTrackingEnabled());
+ Assert.assertFalse(countly.moduleConfiguration.getCrashReportingEnabled());
+
+ threadThrows.start();
+ try {
+ threadThrows.join();
+ } catch (InterruptedException ignored) {
+ }
+
+ countly.events().recordEvent("d");
+ countly.events().recordEvent("1");
+ Assert.assertEquals(1, callCount.get());
+
+ countly.crashes().recordHandledException(new Exception());
+ countly.requestQueue().addDirectRequest(new HashMap<>());
+ countly.requestQueue().attemptToSendStoredRequests();
+
+ Assert.assertEquals(1, TestUtils.getCurrentRQ("Simulated unhandled exception").length);
+ Assert.assertNull(TestUtils.getCurrentRQ("Simulated unhandled exception")[0]);
+ }
+
+ // ================ Invalid Configuration Tests ================
+
+ /**
+ * Test rejection of various invalid configuration responses
+ */
+ @Test
+ public void invalidConfigResponses_AreRejected() {
Assert.assertNull(countlyStore.getServerConfig());
- //return null object
+ // Test various invalid configurations
initAndValidateConfigParsingResult(null, false);
-
- //return empty object
initAndValidateConfigParsingResult("{}", false);
-
- //returns all except 'v'
initAndValidateConfigParsingResult("{'t':2,'c':{'aa':'bb'}}", false);
-
- //returns all except 't'
initAndValidateConfigParsingResult("{'v':1,'c':{'aa':'bb'}}", false);
-
- //returns all except 'c'
initAndValidateConfigParsingResult("{'v':1,'t':2}", false);
-
- //returns all except 'c' wrong type (number)
initAndValidateConfigParsingResult("{'v':1,'t':2,'c':123}", false);
-
- //returns all except 'c' wrong type (bool)
initAndValidateConfigParsingResult("{'v':1,'t':2,'c':false}", false);
-
- //returns all except 'c' wrong type (string)
initAndValidateConfigParsingResult("{'v':1,'t':2,'c':'fdf'}", false);
}
- Countly initAndValidateConfigParsingResult(String targetResponse, boolean responseAccepted) {
- CountlyConfig config = TestUtils.createConfigurationConfig(true, createIRGForSpecificResponse(targetResponse));
- Countly countly = (new Countly()).init(config);
+ // ================ Configuration Parameter Tests ================
+
+ /**
+ * Test that all configuration parameters are properly defined
+ */
+ @Test
+ public void configurationParameterCount() {
+ int configParameterCount = 26; // plus config, timestamp and version parameters
+ int count = 0;
+ for (Field field : ModuleConfiguration.class.getDeclaredFields()) {
+ if (field.getName().startsWith("keyR")) {
+ count++;
+ }
+ }
+ Assert.assertEquals(configParameterCount, count);
+ }
+
+ // ================ Scenario Tests ================
+
+ /**
+ * Test a complete scenario where custom events are enabled first but disabled later
+ */
+ @Test
+ public void scenario_customEventTrackingDisabled() throws JSONException {
+ // Initial setup with all features enabled
+ ServerConfigBuilder serverConfigBuilder = new ServerConfigBuilder()
+ .defaults();
+
+ Countly countly = new Countly().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+
+ // Verify initial state
+ Assert.assertTrue(countly.moduleConfiguration.getCustomEventTrackingEnabled());
+
+ // Record some events to verify tracking
+ countly.events().recordEvent("test_event");
+ Assert.assertEquals(1, countlyStore.getEvents().length);
+
+ // Record some views to verify view tracking is not blocked by custom event tracking
+ countly.views().startAutoStoppedView("test_view");
+
+ Assert.assertEquals(2, countlyStore.getEvents().length); // 1 event + 1 auto stopped view start
+
+ // Update configuration to disable custom event tracking
+ serverConfigBuilder.customEventTracking(false);
+ countly = new Countly().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+
+ // Verify custom event tracking is disabled
+ Assert.assertFalse(countly.moduleConfiguration.getCustomEventTrackingEnabled());
+
+ // Try to record events - should be blocked
+ countly.events().recordEvent("blocked_event");
+ Assert.assertEquals(2, countlyStore.getEvents().length); // 1 event + 1 auto stopped view start no new events
+ }
+
+ /**
+ * Test that view tracking is properly disabled when configured
+ * View tracking is independent of custom event tracking
+ */
+ @Test
+ public void scenario_viewTrackingDisabled() throws JSONException {
+ // Initial setup with all features enabled
+ ServerConfigBuilder serverConfigBuilder = new ServerConfigBuilder()
+ .defaults();
+
+ Countly countly = new Countly().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+
+ // Verify initial state
+ Assert.assertTrue(countly.moduleConfiguration.getCustomEventTrackingEnabled());
+
+ // Record some events to verify tracking
+ countly.events().recordEvent("test_event");
+ Assert.assertEquals(1, countlyStore.getEvents().length);
+
+ // Record some views to verify view tracking is not blocked by custom event tracking
+ countly.views().startAutoStoppedView("test_view");
+
+ Assert.assertEquals(2, countlyStore.getEvents().length); // 1 event + 1 auto stopped view start
+
+ // Update configuration to disable custom event tracking
+ serverConfigBuilder.viewTracking(false);
+ countly = new Countly().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+
+ // Verify custom event tracking is disabled
+ Assert.assertFalse(countly.moduleConfiguration.getViewTrackingEnabled());
+
+ // Try to record events - should be blocked
+ countly.views().startAutoStoppedView("test_view_1");
+ Assert.assertEquals(2, countlyStore.getEvents().length); // 1 event + 1 auto stopped view start but no views
+ }
+
+ /**
+ * Test that tracking is properly disabled when configured
+ * When tracking is disabled, no new requests should be generated
+ */
+ @Test
+ public void scenario_trackingDisabled() throws JSONException, InterruptedException {
+ // Initial setup with all features enabled
+ ServerConfigBuilder serverConfigBuilder = new ServerConfigBuilder()
+ .defaults();
+
+ Countly countly = new Countly().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+ countly.onStartInternal(null);
+ // Verify initial state
+ Assert.assertTrue(countly.moduleConfiguration.getTrackingEnabled());
+ Thread.sleep(1000);
+
+ Assert.assertEquals(1, TestUtils.getCurrentRQ().length); // begin session request
+
+ serverConfigBuilder.tracking(false);
+ countly = new Countly().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+ countly.onStartInternal(null);
+ Thread.sleep(1000);
+ Assert.assertEquals(1, TestUtils.getCurrentRQ().length); // assert that no new request is added
+ }
+
+ /**
+ * Test that networking is properly disabled when configured
+ * When networking is disabled, request queue operations are skipped
+ */
+ @Test
+ public void scenario_networkingDisabled() throws JSONException, InterruptedException {
+ // Initial setup with all features enabled
+ ServerConfigBuilder serverConfigBuilder = new ServerConfigBuilder()
+ .defaults();
+
+ Countly.sharedInstance().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+ Countly.sharedInstance().onStartInternal(null);
+ ModuleLog mockLog = Mockito.mock(ModuleLog.class);
+
+ Countly.sharedInstance().L = mockLog;
+ // Verify initial state
+ Assert.assertTrue(Countly.sharedInstance().moduleConfiguration.getNetworkingEnabled());
+ Thread.sleep(1000);
+
+ Assert.assertEquals(1, TestUtils.getCurrentRQ().length); // begin session request
+
+ serverConfigBuilder.networking(false);
+ Countly.sharedInstance().onStopInternal();
+ Countly.sharedInstance().sdkIsInitialised = false;
+ Mockito.verify(mockLog, Mockito.never()).w("[ConnectionProcessor] run, Networking config is disabled, request queue skipped");
+
+ Assert.assertEquals(3, TestUtils.getCurrentRQ().length); //first begin + orientation + first end session
+
+ Countly.sharedInstance().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+ Countly.sharedInstance().onStartInternal(null);
+ Thread.sleep(1000);
+ Assert.assertFalse(Countly.sharedInstance().moduleConfiguration.getNetworkingEnabled());
+
+ Assert.assertEquals(4, TestUtils.getCurrentRQ().length); //first begin + orientation + first end session + second begin
+ Countly.sharedInstance().requestQueue().attemptToSendStoredRequests();
+ Thread.sleep(1000);
+
+ Mockito.verify(mockLog, Mockito.atLeastOnce()).w("[ConnectionProcessor] run, Networking config is disabled, request queue skipped");
+ }
+
+ /**
+ * Test that session tracking is properly disabled when configured
+ * When session tracking is disabled, no new session requests should be generated
+ */
+ @Test
+ public void scenario_sessionTrackingDisabled() throws JSONException, InterruptedException {
+ // Initial setup with all features enabled
+ ServerConfigBuilder serverConfigBuilder = new ServerConfigBuilder()
+ .defaults();
+
+ Countly.sharedInstance().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+ Countly.sharedInstance().onStartInternal(null);
+
+ // Verify initial state
+ Assert.assertTrue(Countly.sharedInstance().moduleConfiguration.getSessionTrackingEnabled());
+ Thread.sleep(1000);
+
+ Assert.assertEquals(1, TestUtils.getCurrentRQ().length); // begin session request
+ ModuleSessionsTests.validateSessionBeginRequest(0, TestUtils.commonDeviceId);
+
+ serverConfigBuilder.sessionTracking(false);
+ Countly.sharedInstance().onStopInternal();
+ Countly.sharedInstance().sdkIsInitialised = false;
+
+ Assert.assertEquals(3, TestUtils.getCurrentRQ().length); //first begin + orientation + first end session
+
+ Countly.sharedInstance().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+ Countly.sharedInstance().onStartInternal(null);
+ Thread.sleep(1000);
+ Assert.assertFalse(Countly.sharedInstance().moduleConfiguration.getSessionTrackingEnabled());
+
+ Assert.assertEquals(3, TestUtils.getCurrentRQ().length); // same error count because no session request generated
+ }
+
+ /**
+ * Test that session tracking is properly disabled when configured with manual session control
+ * Manual session control allows explicit session management
+ */
+ @Test
+ public void scenario_sessionTrackingDisabled_manualSessions() throws JSONException, InterruptedException {
+ // Initial setup with all features enabled
+ ServerConfigBuilder serverConfigBuilder = new ServerConfigBuilder()
+ .defaults();
+
+ Countly.sharedInstance().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build()))
+ .enableManualSessionControl());
+ Countly.sharedInstance().onStartInternal(null);
+
+ // Verify initial state
+ Assert.assertTrue(Countly.sharedInstance().moduleConfiguration.getSessionTrackingEnabled());
+ serverConfigBuilder.validateAgainst(Countly.sharedInstance());
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length); // no session request
+
+ Countly.sharedInstance().sessions().beginSession();
+ Assert.assertEquals(1, TestUtils.getCurrentRQ().length); // begin session request
+ ModuleSessionsTests.validateSessionBeginRequest(0, TestUtils.commonDeviceId);
+
+ Thread.sleep(1000);
+ Countly.sharedInstance().sessions().endSession();
+ Assert.assertEquals(3, TestUtils.getCurrentRQ().length); // begin session request + orientation + end session request
+
+ ModuleSessionsTests.validateSessionEndRequest(2, 1, TestUtils.commonDeviceId);
+
+ serverConfigBuilder.sessionTracking(false);
+ Countly.sharedInstance().onStopInternal();
+ Countly.sharedInstance().sdkIsInitialised = false;
+
+ Assert.assertEquals(3, TestUtils.getCurrentRQ().length); // same request count
+
+ Countly.sharedInstance().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build()))
+ .enableManualSessionControl());
+ Countly.sharedInstance().onStartInternal(null);
+ Thread.sleep(1000);
+ Assert.assertFalse(Countly.sharedInstance().moduleConfiguration.getSessionTrackingEnabled());
+ serverConfigBuilder.validateAgainst(Countly.sharedInstance());
+
+ Assert.assertFalse(Countly.sharedInstance().moduleSessions.sessionIsRunning());
+ Countly.sharedInstance().sessions().beginSession();
+ Assert.assertFalse(Countly.sharedInstance().moduleSessions.sessionIsRunning());
+
+ Thread.sleep(1000);
+ Countly.sharedInstance().sessions().updateSession();
+
+ Thread.sleep(1000);
+ Assert.assertFalse(Countly.sharedInstance().moduleSessions.sessionIsRunning());
+ Countly.sharedInstance().sessions().endSession();
+ Assert.assertFalse(Countly.sharedInstance().moduleSessions.sessionIsRunning());
+
+ Assert.assertEquals(3, TestUtils.getCurrentRQ().length); // same request count
+ }
+
+ /**
+ * Test that location tracking is properly disabled when configured
+ * Location updates are blocked when tracking is disabled
+ * Location updates include city, country, GPS coordinates and IP
+ */
+ @Test
+ public void scenario_locationTrackingDisabled() throws JSONException {
+ // Initial setup with all features enabled
+ ServerConfigBuilder serverConfigBuilder = new ServerConfigBuilder()
+ .defaults();
+
+ Countly.sharedInstance().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+ Countly.sharedInstance().onStartInternal(null);
+ // Verify initial state
+ Assert.assertTrue(Countly.sharedInstance().moduleConfiguration.getLocationTrackingEnabled());
+ serverConfigBuilder.validateAgainst(Countly.sharedInstance());
+ Assert.assertEquals(1, TestUtils.getCurrentRQ().length); // session request
+
+ Countly.sharedInstance().location().setLocation("country", "city", "gps", "ip");
+ Assert.assertEquals(2, TestUtils.getCurrentRQ().length); // location request
+ Assert.assertTrue(TestUtils.getCurrentRQ()[1].containsKey("location"));
+
+ serverConfigBuilder.locationTracking(false);
+ Countly.sharedInstance().onStopInternal();
+ Countly.sharedInstance().sdkIsInitialised = false; // reset sdk
+
+ Countly.sharedInstance().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+ Countly.sharedInstance().onStartInternal(null);
+ Assert.assertFalse(Countly.sharedInstance().moduleConfiguration.getLocationTrackingEnabled());
+ serverConfigBuilder.validateAgainst(Countly.sharedInstance());
+
+ Assert.assertEquals(6, TestUtils.getCurrentRQ().length);
+
+ Countly.sharedInstance().location().setLocation("country1", "city1", "gps1", "ip1");
+ Countly.sharedInstance().location().disableLocation();
+ Countly.sharedInstance().location().setLocation("country2", "city2", "gps2", "ip2");
+
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", ""), 4); // this will be from server config
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", ""), 6); // this is from disable location
+
+ // first begin session + location request + orientation + first end session + location reset + second begin session
+ Assert.assertEquals(7, TestUtils.getCurrentRQ().length); // same request count
+ }
+
+ /**
+ * Test that consent requirement is properly handled when enabled/disabled
+ * When consent is required, operations are blocked until consent is given
+ * Attribution is used as a test case since it's not directly affected by server config
+ * Need to verify both the request queue and consent state
+ */
+ @Test
+ public void scenario_consentRequiredDisabled() throws JSONException {
+ // Initial setup with all features enabled
+ ServerConfigBuilder serverConfigBuilder = new ServerConfigBuilder()
+ .defaults();
+
+ Countly.sharedInstance().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+ // Verify initial state
+ Assert.assertFalse(Countly.sharedInstance().config_.shouldRequireConsent);
+ serverConfigBuilder.validateAgainst(Countly.sharedInstance());
+
+ // use a feature that is not affected directly from the server configuration
+ Countly.sharedInstance().attribution().recordDirectAttribution("_special_test", "_special_test");
+ Assert.assertEquals(1, TestUtils.getCurrentRQ().length); // attribution request
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("attribution_data", "_special_test"), 0);
+
+ serverConfigBuilder.consentRequired(true);
+ Countly.sharedInstance().sdkIsInitialised = false;
+ Countly.sharedInstance().init(TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(serverConfigBuilder.build())));
+ Assert.assertTrue(Countly.sharedInstance().config_.shouldRequireConsent);
+ serverConfigBuilder.validateAgainst(Countly.sharedInstance());
+
+ Assert.assertEquals(3, TestUtils.getCurrentRQ().length); // first attribution request, empty consent, empty location
+
+ Countly.sharedInstance().attribution().recordDirectAttribution("_special_test", "_special_test");
+ Assert.assertEquals(3, TestUtils.getCurrentRQ().length); // changes nothing because no consent for attribution
+ ModuleConsentTests.validateConsentRequest(TestUtils.commonDeviceId, 1, new boolean[] { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false });
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", ""), 2);
+ }
+
+ /**
+ * Tests that the event queue size limit is properly enforced.
+ * Verifies that:
+ * 1. Events are queued until the size limit is reached
+ * 2. When limit is reached, events sent in a batch
+ * 3. New events queued after the batch sent
+ * 4. Event order maintained in the queue
+ */
+ @Test
+ public void eventQueueSize() throws JSONException {
+ CountlyConfig countlyConfig = TestUtils.createBaseConfig().setLoggingEnabled(false).enableManualSessionControl();
+ countlyConfig.immediateRequestGenerator = createIRGForSpecificResponse(new ServerConfigBuilder().eventQueueSize(3).build());
+ Countly.sharedInstance().init(countlyConfig);
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, TestUtils.getCountlyStore().getEventQueueSize());
+
+ Countly.sharedInstance().events().recordEvent("test_event");
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(1, TestUtils.getCountlyStore().getEventQueueSize());
+
+ Countly.sharedInstance().events().recordEvent("test_event_1");
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(2, TestUtils.getCountlyStore().getEventQueueSize());
+
+ Countly.sharedInstance().events().recordEvent("test_event_2");
+ Assert.assertEquals(1, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, TestUtils.getCountlyStore().getEventQueueSize());
+
+ Countly.sharedInstance().events().recordEvent("test_event_3");
+ Assert.assertEquals(1, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(1, TestUtils.getCountlyStore().getEventQueueSize());
+
+ validateEventInRQ("test_event", TestUtils.map(), 0, 1, 0, 3);
+ validateEventInRQ("test_event_1", TestUtils.map(), 0, 1, 1, 3);
+ validateEventInRQ("test_event_2", TestUtils.map(), 0, 1, 2, 3);
+ }
+
+ /**
+ * Tests that the request queue size limit is properly enforced.
+ * Verifies that:
+ * 1. Requests are queued until the size limit is reached
+ * 2. When limit is reached, first item removed
+ * 3. Different types of requests (sessions, attribution, location) are counted towards the limit
+ */
+ @Test
+ public void requestQueueSize() throws JSONException {
+ CountlyConfig countlyConfig = TestUtils.createBaseConfig().setLoggingEnabled(false).enableManualSessionControl();
+ countlyConfig.immediateRequestGenerator = createIRGForSpecificResponse(new ServerConfigBuilder().requestQueueSize(3).build());
+ Countly.sharedInstance().init(countlyConfig);
+
+ Countly.sharedInstance().sessions().beginSession();
+ ModuleSessionsTests.validateSessionBeginRequest(0, TestUtils.commonDeviceId);
+
+ Countly.sharedInstance().attribution().recordDirectAttribution("_special_test", "_special_test");
+ Assert.assertEquals(2, TestUtils.getCurrentRQ().length);
+
+ Countly.sharedInstance().location().setLocation("country", "city", "gps", "ip");
+ Assert.assertEquals(3, TestUtils.getCurrentRQ().length);
+
+ Map params = new ConcurrentHashMap<>();
+ params.put("key", "value");
+ Countly.sharedInstance().requestQueue().addDirectRequest(params);
+
+ boolean failed = false;
+ try {
+ ModuleSessionsTests.validateSessionBeginRequest(0, TestUtils.commonDeviceId); // this will be not true anymore
+ failed = true;
+ } catch (Throwable e) {
+ // do nothing
+ }
+
+ Assert.assertFalse(failed);
+ }
+
+ // ================ Helper Methods ================
+
+ private void assertDefaultConfigValues(Countly countly) {
+ Assert.assertTrue(countly.moduleConfiguration.getNetworkingEnabled());
+ Assert.assertTrue(countly.moduleConfiguration.getTrackingEnabled());
+ Assert.assertTrue(countly.moduleConfiguration.getCrashReportingEnabled());
+ }
+
+ private Countly initAndValidateConfigParsingResult(String targetResponse, boolean responseAccepted) {
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse(targetResponse));
+ config.enableServerConfiguration();
+ countly = new Countly().init(config);
if (!responseAccepted) {
Assert.assertNull(countlyStore.getServerConfig());
- assertConfigDefault(countly);
+ assertDefaultConfigValues(countly);
} else {
Assert.assertNotNull(countlyStore.getServerConfig());
}
@@ -236,48 +1061,212 @@ Countly initAndValidateConfigParsingResult(String targetResponse, boolean respon
return countly;
}
- void assertConfigDefault(Countly countly) {
- Assert.assertTrue(countly.moduleConfiguration.getNetworkingEnabled());
- Assert.assertTrue(countly.moduleConfiguration.getTrackingEnabled());
+ private String createStorageConfig(boolean tracking, boolean networking, boolean crashes) throws JSONException {
+ return new ServerConfigBuilder()
+ .tracking(tracking)
+ .networking(networking)
+ .crashReporting(crashes)
+ .build();
}
- ImmediateRequestGenerator createIRGForSpecificResponse(final String targetResponse) {
+ static ImmediateRequestGenerator createIRGForSpecificResponse(final String targetResponse) {
return new ImmediateRequestGenerator() {
- @Override public ImmediateRequestI CreateImmediateRequestMaker() {
+ @Override
+ public ImmediateRequestI CreateImmediateRequestMaker() {
return new ImmediateRequestI() {
- @Override public void doWork(String requestData, String customEndpoint, ConnectionProcessor cp, boolean requestShouldBeDelayed, boolean networkingIsEnabled, ImmediateRequestMaker.InternalImmediateRequestCallback callback, ModuleLog log) {
+ @Override
+ public void doWork(String requestData, String customEndpoint, ConnectionProcessor cp, boolean requestShouldBeDelayed, boolean networkingIsEnabled, ImmediateRequestMaker.InternalImmediateRequestCallback callback, ModuleLog log) {
if (targetResponse == null) {
callback.callback(null);
return;
}
- JSONObject jobj = null;
-
try {
- jobj = new JSONObject(targetResponse);
+ callback.callback(new JSONObject(targetResponse));
} catch (JSONException e) {
e.printStackTrace();
}
+ }
+ };
+ }
+ };
+ }
+
+ private void initServerConfigWithValues(BiConsumer configSetter) throws JSONException, InterruptedException {
+ ServerConfigBuilder builder = new ServerConfigBuilder()
+ // Feature flags
+ .tracking(false)
+ .networking(false)
+ .crashReporting(false)
+ .viewTracking(false)
+ .sessionTracking(false)
+ .customEventTracking(false)
+ .contentZone(true)
+ .locationTracking(false)
+ .refreshContentZone(false)
+
+ // Intervals and sizes
+ .serverConfigUpdateInterval(8)
+ .requestQueueSize(2000)
+ .eventQueueSize(200)
+ .logging(true)
+ .sessionUpdateInterval(120)
+ .contentZoneInterval(60)
+ .consentRequired(true)
+ .dropOldRequestTime(1)
+ .keyLengthLimit(89)
+ .valueSizeLimit(43)
+ .segmentationValuesLimit(25)
+ .breadcrumbLimit(90)
+ .traceLengthLimit(78)
+ .traceLinesLimit(89);
+
+ String serverConfig = builder.build();
+ CountlyConfig countlyConfig = TestUtils.createBaseConfig().setLoggingEnabled(false);
+ configSetter.accept(countlyConfig, serverConfig);
+
+ countly = new Countly().init(countlyConfig);
+ Thread.sleep(2000);
+
+ builder.validateAgainst(countly);
+ }
+
+ private int[] setupTest_allFeatures(JSONObject serverConfig) {
+ final int[] counts = { 0, 0, 0, 0, 0 };
+ CountlyConfig countlyConfig = TestUtils.createBaseConfig().setLoggingEnabled(false).enableManualSessionControl();
+ countlyConfig.immediateRequestGenerator = new ImmediateRequestGenerator() {
+ @Override public ImmediateRequestI CreateImmediateRequestMaker() {
+ return new ImmediateRequestI() {
+ @Override public void doWork(String requestData, String customEndpoint, ConnectionProcessor cp, boolean requestShouldBeDelayed, boolean networkingIsEnabled, ImmediateRequestMaker.InternalImmediateRequestCallback callback, ModuleLog log) {
+ if (networkingIsEnabled) {
+ if (requestData.contains("&hc=")) {
+ counts[0] += 1;
+ } else if (requestData.contains("&method=feedback")) {
+ counts[1] += 1;
+ } else if (requestData.contains("&method=rc")) {
+ counts[2] += 1;
+ } else if (requestData.contains("&method=queue")) {
+ counts[3] += 1;
+ } else if (requestData.contains("&method=sc")) {
+ // do nothing
+ } else {
+ Assert.fail("Unexpected request data: " + requestData);
+ }
+ }
- callback.callback(jobj);
+ if (requestData.contains("&method=sc")) {
+ counts[4] += 1;
+ Assert.assertTrue(networkingIsEnabled);
+ callback.callback(serverConfig);
+ }
}
};
}
};
+ countlyConfig.metricProviderOverride = new MockedMetricProvider();
+ Countly.sharedInstance().init(countlyConfig);
+ Countly.sharedInstance().moduleContent.CONTENT_START_DELAY_MS = 0; // make it zero to catch content immediate request
+ Countly.sharedInstance().moduleContent.REFRESH_CONTENT_ZONE_DELAY_MS = 0; // make it zero to catch content immediate request
+ return counts;
+ }
+
+ private void validateCounts(int[] counts, int hc, int fc, int rc, int cc, int sc) {
+ Assert.assertEquals(hc, counts[0]); // health check request
+ Assert.assertEquals(fc, counts[1]); // feedback request
+ Assert.assertEquals(rc, counts[2]); // remote config request
+ Assert.assertEquals(cc, counts[3]); // content request
+ Assert.assertEquals(sc, counts[4]); // server config request
}
- //creates the stringified storage object with all the required properties
- String getStorageString(boolean tracking, boolean networking) throws JSONException {
- JSONObject jsonObject = new JSONObject();
- JSONObject jsonObjectConfig = new JSONObject();
+ private String flow_allFeatures() throws InterruptedException {
+ Countly.sharedInstance().sessions().beginSession();
+
+ Countly.sharedInstance().events().recordEvent("test_event");
+
+ Countly.sharedInstance().views().startView("test_view");
+
+ Exception e = new Exception("test_exception");
+ Countly.sharedInstance().crashes().recordHandledException(e);
+
+ Countly.sharedInstance().userProfile().setProperty("test_property", "test_value");
+ Countly.sharedInstance().userProfile().save(); // events will be packed on this
+
+ Countly.sharedInstance().location().setLocation("country", "city", "gps", "ip");
+
+ Countly.sharedInstance().apm().recordNetworkTrace("test_trace", 400, 2000, 1111, 1111, 2222);
+
+ Countly.sharedInstance().attribution().recordDirectAttribution("_special_test", "test_data");
+
+ Map params = new ConcurrentHashMap<>();
+ params.put("key", "value");
+ Countly.sharedInstance().requestQueue().addDirectRequest(params);
+
+ return ModuleCrashTests.extractStackTrace(e);
+ }
+
+ private void immediateFlow_allFeatures() throws InterruptedException {
+ Countly.sharedInstance().remoteConfig().downloadAllKeys(null); // will add one rc immediate request
+ Countly.sharedInstance().feedback().getAvailableFeedbackWidgets(new ModuleFeedback.RetrieveFeedbackWidgets() {
+ @Override public void onFinished(List retrievedWidgets, String error) {
+
+ }
+ }); // will add one feedback immediate request
+ Countly.sharedInstance().contents().enterContentZone(); // will add one content immediate request
+
+ Thread.sleep(1000);
+
+ Countly.sharedInstance().contents().refreshContentZone(); // will add one more content immediate request
+ }
+
+ private void feedbackFlow_allFeatures() {
+ // could not mock ratings immediate, it requires a context
+ Countly.sharedInstance().ratings().recordRatingWidgetWithID("test", 5, "test", "test", true);
+ ModuleFeedback.CountlyFeedbackWidget widget = new ModuleFeedback.CountlyFeedbackWidget();
+ widget.name = "test";
+ widget.widgetId = "test";
+ widget.type = ModuleFeedback.FeedbackWidgetType.nps;
+ Countly.sharedInstance().feedback().reportFeedbackWidgetManually(widget, null, null);
+ }
+
+ private static void validateEventInRQ(String eventName, Map segmentation, int idx, int rqCount, int eventIdx, int eventCount) throws JSONException {
+ ModuleEventsTests.validateEventInRQ(TestUtils.commonDeviceId, eventName, segmentation, 1, 0.0, 0.0, "_CLY_", "_CLY_", "_CLY_", "_CLY_", idx, rqCount, eventIdx, eventCount);
+ }
+
+ private void base_allFeatures(Consumer consumer, int hc, int fc, int rc, int cc, int scc) throws JSONException, InterruptedException {
+ ServerConfigBuilder sc = new ServerConfigBuilder();
+ consumer.accept(sc);
+ int[] counts = setupTest_allFeatures(sc.buildJson());
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(0, TestUtils.getCountlyStore().getEventQueueSize());
+
+ String stackTrace = flow_allFeatures();
+
+ ModuleSessionsTests.validateSessionBeginRequest(0, TestUtils.commonDeviceId);
+ ModuleCrashTests.validateCrash(stackTrace, "", false, false, 8, 1, TestUtils.map(), 0, TestUtils.map(), new ArrayList<>());
+ validateEventInRQ("[CLY]_orientation", TestUtils.map("mode", "portrait"), 2, 8, 0, 3);
+ validateEventInRQ("test_event", TestUtils.map(), 2, 8, 1, 3);
+ validateEventInRQ("[CLY]_view", TestUtils.map("name", "test_view", "segment", "Android", "visit", "1", "start", "1"), 2, 8, 2, 3);
+ ModuleUserProfileTests.validateUserProfileRequest(3, 8, TestUtils.map(), TestUtils.map("test_property", "test_value"));
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", "gps"), 4);
+ ModuleAPMTests.validateNetworkRequest(5, 8, "test_trace", 1111, 400, 2000, 1111);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("attribution_data", "test_data"), 6);
+ TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("key", "value"), 7);
+
+ Assert.assertEquals(8, TestUtils.getCurrentRQ().length);
+
+ immediateFlow_allFeatures();
+
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
+
+ feedbackFlow_allFeatures();
+ Assert.assertEquals(0, countlyStore.getEventQueueSize());
- jsonObjectConfig.put("tracking", tracking);
- jsonObjectConfig.put("networking", networking);
+ validateEventInRQ("[CLY]_star_rating", TestUtils.map("platform", "android", "app_version", Countly.DEFAULT_APP_VERSION, "rating", "5", "widget_id", "test", "contactMe", true, "email", "test", "comment", "test"), 8, 9, 0, 2);
+ validateEventInRQ("[CLY]_nps", TestUtils.map("app_version", Countly.DEFAULT_APP_VERSION, "widget_id", "test", "closed", "1", "platform", "android"), 8, 9, 1, 2);
- jsonObject.put("v", 1);
- jsonObject.put("t", 1_681_808_287_464L);
- jsonObject.put("c", jsonObjectConfig);
+ Assert.assertEquals(9, TestUtils.getCurrentRQ().length);
- return jsonObject.toString();
+ validateCounts(counts, hc, fc, rc, cc, scc);
}
}
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConsentTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConsentTests.java
index d0c1de3b6..52f45fc6c 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConsentTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConsentTests.java
@@ -8,10 +8,10 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mockito;
+import org.mockito.exceptions.verification.NoInteractionsWanted;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.verifyNoInteractions;
@RunWith(AndroidJUnit4.class)
public class ModuleConsentTests {
@@ -176,11 +176,11 @@ public void validateFeatureNames() {
* No requests should be created.
* There should be no interactions with the mock
*/
- @Test
+ @Test(expected = NoInteractionsWanted.class)
public void initTimeNoConsentRequiredRQ() {
RequestQueueProvider rqp = mock(RequestQueueProvider.class);
Countly mCountly = new Countly().init(TestUtils.createConsentCountlyConfig(false, null, null, rqp));
- verifyZeroInteractions(rqp);
+ verifyNoInteractions(rqp); // This test is no longer valid because we fetch server config
}
/**
@@ -192,7 +192,8 @@ public void initTimeNoConsentRequiredRQ() {
public void initTimeNoConsentGivenRQ() throws JSONException {
RequestQueueProvider rqp = mock(RequestQueueProvider.class);
Countly mCountly = new Countly().init(TestUtils.createConsentCountlyConfig(true, null, null, rqp));
- Assert.assertEquals(2, Mockito.mockingDetails(rqp).getInvocations().size());
+ //Assert.assertEquals(2, Mockito.mockingDetails(rqp).getInvocations().size());
+ //above is not valid anymore because we fetch server config so this test is no longer valid
TestUtils.verifyLocationValuesInRQMock(1, true, null, null, null, null, rqp);
TestUtils.verifyConsentValuesInRQMock(1, new String[] {}, usedFeatureNames, rqp);
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleCrashTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleCrashTests.java
index 0f66577bb..88806761d 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleCrashTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleCrashTests.java
@@ -41,7 +41,7 @@ public class ModuleCrashTests {
@Before
public void setUp() {
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
mCountly = new Countly();
config = new CountlyConfig(TestUtils.getContext(), "appkey", "http://test.count.ly").setDeviceId("1234").setLoggingEnabled(true).enableCrashReporting();
@@ -709,7 +709,7 @@ public void recordException_crashFilter_globalCrashFilter() throws JSONException
countly.crashes().recordHandledException(exception, TestUtils.map("secret", "secret"));
validateCrash(extractStackTrace(exception), "", false, false, TestUtils.map("secret", "secret"), 0, new ConcurrentHashMap<>(), new ArrayList<>());
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
countly.crashes().recordUnhandledException(exception, TestUtils.map("secret", "secret"));
validateCrash(extractStackTrace(exception), "", true, false, TestUtils.map("secret", "secret"), 0, new ConcurrentHashMap<>(), new ArrayList<>());
}
@@ -836,7 +836,7 @@ public void recordException_crashFilter_nativeCrash() throws JSONException {
}
private void createNativeDumFiles() {
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
String finalPath = TestUtils.getContext().getCacheDir().getAbsolutePath() + File.separator + "Countly" + File.separator + "CrashDumps";
@@ -860,12 +860,12 @@ private void createFile(String filePath, String fileName, String data) {
}
}
- private void validateCrash(@NonNull String error, @NonNull String breadcrumbs, boolean fatal, boolean nativeCrash,
+ static void validateCrash(@NonNull String error, @NonNull String breadcrumbs, boolean fatal, boolean nativeCrash,
@NonNull Map customSegmentation, int changedBits, @NonNull Map customMetrics, @NonNull List baseMetricsExclude) throws JSONException {
validateCrash(error, breadcrumbs, fatal, nativeCrash, 1, 0, customSegmentation, changedBits, customMetrics, baseMetricsExclude);
}
- private void validateCrash(@NonNull String error, @NonNull String breadcrumbs, boolean fatal, boolean nativeCrash, final int rqSize, final int idx,
+ static void validateCrash(@NonNull String error, @NonNull String breadcrumbs, boolean fatal, boolean nativeCrash, final int rqSize, final int idx,
@NonNull Map customSegmentation, int changedBits, @NonNull Map customMetrics, @NonNull List baseMetricsExclude) throws JSONException {
Map[] RQ = TestUtils.getCurrentRQ();
Assert.assertEquals(rqSize, RQ.length);
@@ -905,7 +905,7 @@ private void validateCrash(@NonNull String error, @NonNull String breadcrumbs, b
Assert.assertEquals(paramCount, crash.length());
}
- private int validateCrashMetrics(@NonNull JSONObject crash, boolean nativeCrash, @NonNull Map customMetrics, @NonNull List metricsToExclude) throws JSONException {
+ private static int validateCrashMetrics(@NonNull JSONObject crash, boolean nativeCrash, @NonNull Map customMetrics, @NonNull List metricsToExclude) throws JSONException {
int metricCount = 12 - metricsToExclude.size();
assertMetricIfNotExcluded(metricsToExclude, "_device", "C", crash);
@@ -944,7 +944,7 @@ private int validateCrashMetrics(@NonNull JSONObject crash, boolean nativeCrash,
return metricCount;
}
- private void assertMetricIfNotExcluded(List metricsToExclude, String metric, Object value, JSONObject crash) throws JSONException {
+ private static void assertMetricIfNotExcluded(List metricsToExclude, String metric, Object value, JSONObject crash) throws JSONException {
if (metricsToExclude.contains(metric)) {
Assert.assertFalse(crash.has(metric));
} else {
@@ -952,11 +952,11 @@ private void assertMetricIfNotExcluded(List metricsToExclude, String met
}
}
- private String extractStackTrace(Throwable throwable) {
+ static String extractStackTrace(Throwable throwable) {
return extractStackTrace(throwable, 1000, -1);
}
- private String extractStackTrace(Throwable throwable, int lineLength, int maxLines) {
+ private static String extractStackTrace(Throwable throwable, int lineLength, int maxLines) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
@@ -1236,6 +1236,61 @@ public void internalLimits_globalCrashFilter_sdkInternalLimits_withPreValues() t
validateCrash(extractStackTrace(exception), "Scani\nMerce\n", true, false, segm, 12, new HashMap<>(), new ArrayList<>());
}
+ /**
+ * Custom crash segmentation and segmentation while recording the crash is provided serverConfig
+ * Also breadcrumbs are added before
+ * Validate all 4 limits are applied after crash filtering:
+ * - Max value size 5
+ * - Max key length 2
+ * - Max segmentation values 5
+ * - Max breadcrumb count 2
+ * Validate that all values are truncated to their limits
+ *
+ * @throws JSONException if JSON parsing fails
+ */
+ @Test
+ public void serverConfig_globalCrashFilter_sdkInternalLimits_withPreValues() throws JSONException {
+ CountlyConfig cConfig = TestUtils.createBaseConfig();
+ cConfig.metricProviderOverride = mmp;
+ cConfig.immediateRequestGenerator = ModuleConfigurationTests.createIRGForSpecificResponse(new ServerConfigBuilder().valueSizeLimit(5).keyLengthLimit(2).segmentationValuesLimit(5).breadcrumbLimit(2).build());
+
+ cConfig.sdkInternalLimits.setMaxValueSize(5).setMaxKeyLength(2).setMaxSegmentationValues(5).setMaxBreadcrumbCount(2);
+ cConfig.crashes.setCustomCrashSegmentation(TestUtils.map("arr", new int[] { 1, 2, 3, 4, 5 }, "double", Double.MAX_VALUE, "bool", true, "float", 1.1, "object", new Object(), "string", "string_to_become"));
+ cConfig.crashes.setGlobalCrashFilterCallback(crash -> {
+ if (Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT <= 25) {
+ TestUtils.assertEqualsMap(TestUtils.map("ar", new int[] { 1, 2, 3, 4, 5 }, "do", Double.MAX_VALUE, "de", "no", "fl", 1.1, "in", Integer.MIN_VALUE), crash.getCrashSegmentation());
+ } else {
+ TestUtils.assertEqualsMap(TestUtils.map("de", "no", "do", Double.MAX_VALUE, "bo", false, "in", Integer.MIN_VALUE, "fl", 1.1), crash.getCrashSegmentation());
+ }
+ Assert.assertEquals("Volvo\nScani\n", crash.getBreadcrumbsAsString());
+
+ crash.getCrashSegmentation().put("beforemath", "Mudrunner");
+ crash.getCrashSegmentation().put("arr", new int[] { 1, 2 });
+ crash.getCrashSegmentation().put("obj", new Object());
+ crash.getCrashSegmentation().put("double", Double.MIN_VALUE);
+
+ crash.getBreadcrumbs().add("MercedesActros");
+
+ return false;
+ });
+
+ Countly countly = new Countly().init(cConfig);
+ countly.crashes().addCrashBreadcrumb("VolvoFH750");
+ countly.crashes().addCrashBreadcrumb("ScaniaR730");
+
+ Exception exception = new Exception("Some message");
+ countly.crashes().recordUnhandledException(exception, TestUtils.map("boolean", false, "star", "boom_boom", "integer", Integer.MIN_VALUE, "desire", "no"));
+
+ Map segm;
+ if (Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT <= 25) {
+ segm = TestUtils.map("do", Double.MIN_VALUE, "de", "no", "fl", 1.1, "ar", new int[] { 1, 2 }, "in", Integer.MIN_VALUE);
+ } else {
+ segm = TestUtils.map("be", "Mudru", "do", Double.MIN_VALUE, "bo", false, "in", Integer.MIN_VALUE, "fl", 1.1);
+ }
+
+ validateCrash(extractStackTrace(exception), "Scani\nMerce\n", true, false, segm, 12, new HashMap<>(), new ArrayList<>());
+ }
+
/**
* Validate that the stack trace is truncated to the maximum allowed length of 2
* Adding all thread information is disabled
@@ -1310,6 +1365,25 @@ public void internalLimits_recordException_stackTraceLimits_lineLength_afterCras
validateCrash(expectedStackTrace.toString(), "", true, false, new HashMap<>(), 16, new HashMap<>(), new ArrayList<>());
}
+ /**
+ * Validate that the stack trace is truncated to the maximum allowed length of 2 with server config
+ * Adding all thread information is disabled
+ *
+ * @throws JSONException if JSON parsing fails
+ */
+ @Test
+ public void serverConfig_recordException_stackTraceLimits_lineLength() throws JSONException {
+ CountlyConfig cConfig = TestUtils.createBaseConfig();
+ cConfig.metricProviderOverride = mmp;
+ cConfig.immediateRequestGenerator = ModuleConfigurationTests.createIRGForSpecificResponse(new ServerConfigBuilder().traceLengthLimit(2).build());
+
+ Countly countly = new Countly().init(cConfig);
+
+ Exception exception = new Exception("Some message");
+ countly.crashes().recordUnhandledException(exception);
+ validateCrash(extractStackTrace(exception, 2, -1), "", true, false, new HashMap<>(), 0, new HashMap<>(), new ArrayList<>());
+ }
+
/**
* "recordHandledException" with Array segmentations
* Validate that all primitive types arrays are successfully recorded
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleDeviceIdTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleDeviceIdTests.java
index 70e1c1cdd..988629edc 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleDeviceIdTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleDeviceIdTests.java
@@ -13,12 +13,12 @@ public class ModuleDeviceIdTests {
@Before
public void setUp() {
Countly.sharedInstance().halt();
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
}
@After
public void tearDown() {
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
}
/**
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleEventsTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleEventsTests.java
index 0cd842301..a9c44065e 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleEventsTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleEventsTests.java
@@ -525,26 +525,7 @@ public void internalLimits_recordEventInternal_maxSegmentationValues() throws JS
countly.events().recordEvent("rnd_key", TestUtils.map("a", 1, "b", 2, "c", 3, "d", 4, "e", 5, "f", 6, "g", 7), 1, 1.1d, 1.1d);
validateEventInRQ("rnd_key", TestUtils.map("f", 6, "g", 7), 1, 1.1d, 1.1d, 0);
- countly.events().recordEvent(ModuleEvents.ACTION_EVENT_KEY, threeSegmentation);
- validateEventInRQ(ModuleEvents.ACTION_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 1);
-
- countly.events().recordEvent(ModuleFeedback.NPS_EVENT_KEY, threeSegmentation);
- validateEventInRQ(ModuleFeedback.NPS_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 2);
-
- countly.events().recordEvent(ModuleFeedback.SURVEY_EVENT_KEY, threeSegmentation);
- validateEventInRQ(ModuleFeedback.SURVEY_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 3);
-
- countly.events().recordEvent(ModuleFeedback.RATING_EVENT_KEY, threeSegmentation);
- validateEventInRQ(ModuleFeedback.RATING_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 4);
-
- countly.events().recordEvent(ModuleViews.VIEW_EVENT_KEY, threeSegmentation);
- validateEventInRQ(ModuleViews.VIEW_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 5);
-
- countly.events().recordEvent(ModuleViews.ORIENTATION_EVENT_KEY, threeSegmentation);
- validateEventInRQ(ModuleViews.ORIENTATION_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 6);
-
- countly.events().recordEvent(ModulePush.PUSH_EVENT_ACTION, threeSegmentation);
- validateEventInRQ(ModulePush.PUSH_EVENT_ACTION, threeSegmentation, 1, 0.0d, 0.0d, 7);
+ flow_internalEvents(countly, threeSegmentation);
}
/**
@@ -583,6 +564,62 @@ public void internalLimits_recordEventInternal_maxValueSizeKeyLength() throws JS
validateEventInRQ("rn", TestUtils.map("a", 1, "bb", "dd"), 1, 1.1d, 1.1d, 0);
}
+ /**
+ * Validate that only normal events' segmentation values are clipped to the maximum allowed values by given server config
+ * EQ size is 1 to trigger request generation
+ */
+ @Test
+ public void serverConfig_recordEventInternal_maxSegmentationValues() throws JSONException {
+ CountlyConfig config = TestUtils.createBaseConfig();
+ config.immediateRequestGenerator = ModuleConfigurationTests.createIRGForSpecificResponse(new ServerConfigBuilder().segmentationValuesLimit(2).build());
+ config.setEventQueueSizeToSend(1);
+ Countly countly = new Countly().init(config);
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+ Map threeSegmentation = TestUtils.map("a", 1, "b", 2, "c", 3);
+
+ countly.events().recordEvent("rnd_key", TestUtils.map("a", 1, "b", 2, "c", 3, "d", 4, "e", 5, "f", 6, "g", 7), 1, 1.1d, 1.1d);
+ validateEventInRQ("rnd_key", TestUtils.map("f", 6, "g", 7), 1, 1.1d, 1.1d, 0);
+
+ flow_internalEvents(countly, threeSegmentation);
+ }
+
+ /**
+ * "recordEvent" max value size limit
+ * Validate that all "String" values are clipped to the maximum allowed length by given server config
+ * EQ size is 1 to trigger request generation
+ */
+ @Test
+ public void serverConfig_recordEventInternal_maxValueSize() throws JSONException {
+ CountlyConfig config = TestUtils.createBaseConfig();
+ config.immediateRequestGenerator = ModuleConfigurationTests.createIRGForSpecificResponse(new ServerConfigBuilder().valueSizeLimit(2).build());
+ config.setEventQueueSizeToSend(1);
+ Countly countly = new Countly().init(config);
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+
+ countly.events().recordEvent("rnd_key", TestUtils.map("a", 1, "b", "bbb", "c", "ccc"), 1, 1.1d, 1.1d);
+ validateEventInRQ("rnd_key", TestUtils.map("a", 1, "b", "bb", "c", "cc"), 1, 1.1d, 1.1d, 0);
+ }
+
+ /**
+ * "recordEvent" max value size limit and key length
+ * Validate that clipped values clashes with same keys and overridden each other by given server config
+ * "bb" key should have value from the second of the last value which is "dd"
+ */
+ @Test
+ public void serverConfig_recordEventInternal_maxValueSizeKeyLength() throws JSONException {
+ CountlyConfig config = TestUtils.createBaseConfig();
+ config.immediateRequestGenerator = ModuleConfigurationTests.createIRGForSpecificResponse(new ServerConfigBuilder().valueSizeLimit(2).keyLengthLimit(2).build());
+ config.setEventQueueSizeToSend(1);
+ Countly countly = new Countly().init(config);
+
+ Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
+
+ countly.events().recordEvent("rnd_key", TestUtils.map("a", 1, "bbb", "bbb", "bbc", "ccc", "bbd", "ddd", "bbe", "eee"), 1, 1.1d, 1.1d);
+ validateEventInRQ("rn", TestUtils.map("a", 1, "bb", "dd"), 1, 1.1d, 1.1d, 0);
+ }
+
/**
* "recordEvent" with Array segmentations
* Validate that all primitive types arrays are successfully recorded
@@ -899,7 +936,8 @@ public void recordEventScenario_previous_current_ViewName_disabled() throws JSON
countly.views().startView("View1");
countly.events().recordEvent("TEST1");
- ModuleViewsTests.validateView("View1", 0.0, 1, 3, true, true, TestUtils.map(), "_CLY_", "_CLY_", null);
+ // start false because session did not start
+ ModuleViewsTests.validateView("View1", 0.0, 1, 3, false, true, TestUtils.map(), "_CLY_", "_CLY_", null);
validateEventInRQ("TEST1", 2, 3, "_CLY_", "_CLY_", null, null);
countly.views().startView("View2");
@@ -930,7 +968,8 @@ public void recordEventScenario_previous_current_ViewName() throws JSONException
countly.views().startView("View1");
countly.events().recordEvent("TEST1");
- ModuleViewsTests.validateView("View1", 0.0, 1, 3, true, true, TestUtils.map(), "_CLY_", "_CLY_", "");
+ // start false because session did not start
+ ModuleViewsTests.validateView("View1", 0.0, 1, 3, false, true, TestUtils.map(), "_CLY_", "_CLY_", "");
validateEventInRQ("TEST1", 2, 3, "_CLY_", "_CLY_", "TEST", "View1");
countly.views().startView("View2");
@@ -972,6 +1011,27 @@ protected static void validateEventInRQ(String deviceId, String eventName, Map expectedSegmentation, int count, Double sum, Double duration, String id, String pvid, String cvid, String peid, int idx, int eventCount) throws JSONException {
+ CountlyStore store = TestUtils.getCountlyStore();
+ Assert.assertEquals(eventCount, store.getEventQueueSize());
+ String eventStr = store.getEvents()[idx];
+
+ validateEvent(new JSONObject(eventStr), eventName, expectedSegmentation, count, sum, duration, id, pvid, cvid, peid);
+ }
+
+ protected static void validateEventInEQ(String eventName, Map expectedSegmentation, int count, Double sum, Double duration, int idx, int eventCount) throws JSONException {
+ CountlyStore store = TestUtils.getCountlyStore();
+ Assert.assertEquals(eventCount, store.getEventQueueSize());
+ String eventStr = store.getEvents()[idx];
+ System.err.println(eventStr);
+
+ validateEvent(new JSONObject(eventStr), eventName, expectedSegmentation, count, sum, duration, "_CLY_", "_CLY_", "_CLY_", "_CLY_");
+ }
+
+ private static void validateEvent(JSONObject event, String eventName, Map expectedSegmentation, int count, Double sum, Double duration, String id, String pvid, String cvid, String peid) throws JSONException {
Assert.assertEquals(eventName, event.get("key"));
Assert.assertEquals(count, event.getInt("count"));
Assert.assertEquals(sum, event.optDouble("sum", 0.0d), 0.0001);
@@ -1046,6 +1106,29 @@ protected static void validateEventInRQ(String deviceId, String eventName, int r
validateEventInRQ(deviceId, eventName, null, 1, 0.0d, 0.0d, "_CLY_", "_CLY_", "_CLY_", "_CLY_", rqIdx, -1, eventIdx, eventCount);
}
+ private void flow_internalEvents(Countly countly, Map threeSegmentation) throws JSONException {
+ countly.events().recordEvent(ModuleEvents.ACTION_EVENT_KEY, threeSegmentation);
+ validateEventInRQ(ModuleEvents.ACTION_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 1);
+
+ countly.events().recordEvent(ModuleFeedback.NPS_EVENT_KEY, threeSegmentation);
+ validateEventInRQ(ModuleFeedback.NPS_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 2);
+
+ countly.events().recordEvent(ModuleFeedback.SURVEY_EVENT_KEY, threeSegmentation);
+ validateEventInRQ(ModuleFeedback.SURVEY_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 3);
+
+ countly.events().recordEvent(ModuleFeedback.RATING_EVENT_KEY, threeSegmentation);
+ validateEventInRQ(ModuleFeedback.RATING_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 4);
+
+ countly.events().recordEvent(ModuleViews.VIEW_EVENT_KEY, threeSegmentation);
+ validateEventInRQ(ModuleViews.VIEW_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 5);
+
+ countly.events().recordEvent(ModuleViews.ORIENTATION_EVENT_KEY, threeSegmentation);
+ validateEventInRQ(ModuleViews.ORIENTATION_EVENT_KEY, threeSegmentation, 1, 0.0d, 0.0d, 6);
+
+ countly.events().recordEvent(ModulePush.PUSH_EVENT_ACTION, threeSegmentation);
+ validateEventInRQ(ModulePush.PUSH_EVENT_ACTION, threeSegmentation, 1, 0.0d, 0.0d, 7);
+ }
+
/*
//todo should be reworked
@Test
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleRatingsTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleRatingsTests.java
index 863c33afc..f216154dd 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleRatingsTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleRatingsTests.java
@@ -163,6 +163,29 @@ public void internalLimits_recordManualRating_maxValueSize() throws JSONExceptio
ModuleEventsTests.validateEventInRQ(ModuleFeedback.RATING_EVENT_KEY, ratingSegmentation, 1);
}
+ /**
+ * Value size limit is applied to the email and the comment of the manual rating with server config provided
+ * "recordManualRating" and "recordRatingWidgetWithID" methods are tested
+ * Validate that events exist and contains the truncated values of the email and the comment
+ */
+ @Test
+ public void serverConfig_recordManualRating_maxValueSize() throws JSONException {
+ CountlyConfig config = new CountlyConfig(ApplicationProvider.getApplicationContext(), "appkey", "http://test.count.ly").setDeviceId("1234").setLoggingEnabled(true);
+ config.immediateRequestGenerator = ModuleConfigurationTests.createIRGForSpecificResponse(new ServerConfigBuilder().valueSizeLimit(1).build());
+ config.setEventQueueSizeToSend(1);
+ Countly countly = new Countly().init(config);
+
+ countly.ratings().recordManualRating("A", 3, "email", "comment", true);
+
+ Map ratingSegmentation = prepareRatingSegmentation("3", "A", "e", "c", true);
+ ModuleEventsTests.validateEventInRQ(ModuleFeedback.RATING_EVENT_KEY, ratingSegmentation, 0);
+
+ countly.ratings().recordRatingWidgetWithID("B", 5, "aaa@bbb.com", "very_good", false);
+
+ ratingSegmentation = prepareRatingSegmentation("5", "B", "a", "v", false);
+ ModuleEventsTests.validateEventInRQ(ModuleFeedback.RATING_EVENT_KEY, ratingSegmentation, 1);
+ }
+
private Map prepareRatingSegmentation(String rating, String widgetId, String email, String comment, boolean userCanBeContacted) {
Map segm = new ConcurrentHashMap<>();
segm.put("platform", "android");
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleRemoteConfigTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleRemoteConfigTests.java
index 0d5101474..ca7f7bfba 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleRemoteConfigTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleRemoteConfigTests.java
@@ -64,7 +64,7 @@ public void valuesClearedOnConsentRemoval() {
public void automaticRCTriggers() {
for (int a = 0; a < 2; a++) {
countlyStore.clear();
- final int[] triggerCounter = { 0 };
+ final int[] triggerCounter = { 0 }; // because we now have server config fetch
int intendedCount = 0;
CountlyConfig config = new CountlyConfig(TestUtils.getContext(), "appkey", "http://test.count.ly").setDeviceId("1234").setLoggingEnabled(true).enableCrashReporting().disableHealthCheck();
@@ -75,7 +75,9 @@ public void automaticRCTriggers() {
config.setConsentEnabled(new String[] { Countly.CountlyFeatureNames.remoteConfig });
}
config.immediateRequestGenerator = () -> (ImmediateRequestI) (requestData, customEndpoint, cp, requestShouldBeDelayed, networkingIsEnabled, callback, log) -> {
- triggerCounter[0]++;
+ if (!requestData.endsWith("method=sc")) { // this is server config, disabling it for this test
+ triggerCounter[0]++;
+ }
};
Countly countly = (new Countly()).init(config);
Assert.assertEquals(++intendedCount, triggerCounter[0]);//init should create a request
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleSessionsTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleSessionsTests.java
index d74ead0b8..30574b6b8 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleSessionsTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleSessionsTests.java
@@ -13,7 +13,7 @@
public class ModuleSessionsTests {
@Before
public void setUp() {
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
}
@After
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleUserProfileTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleUserProfileTests.java
index 2756c882d..c9e6bd55f 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleUserProfileTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleUserProfileTests.java
@@ -25,12 +25,12 @@ public class ModuleUserProfileTests {
@Before
public void setUp() {
Countly.sharedInstance().halt();
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
}
@After
public void tearDown() {
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
}
/**
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleViewsTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleViewsTests.java
index 7e412a1e9..a7a184262 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleViewsTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleViewsTests.java
@@ -102,10 +102,11 @@ void activityStartedViewTrackingBase(boolean shortNames) {
mCountly.moduleViews.onActivityStarted(act, 1);
final Map segm = new HashMap<>();
+ // start false because session did not start
if (shortNames) {
- ClearFillSegmentationViewStart(segm, act.getClass().getSimpleName(), true);
+ ClearFillSegmentationViewStart(segm, act.getClass().getSimpleName(), false);
} else {
- ClearFillSegmentationViewStart(segm, act.getClass().getName(), true);
+ ClearFillSegmentationViewStart(segm, act.getClass().getName(), false);
}
TestUtils.validateRecordEventInternalMock(ep, ModuleViews.VIEW_EVENT_KEY, segm, vals[0], 0, 1);
@@ -143,10 +144,11 @@ void activityStartedViewTrackingExceptionBase(boolean shortNames) {
mCountly.moduleViews.onActivityStarted(act2, 2);
final Map segm = new HashMap<>();
+ // start false because session did not start
if (shortNames) {
- ClearFillSegmentationViewStart(segm, act2.getClass().getSimpleName(), true);
+ ClearFillSegmentationViewStart(segm, act2.getClass().getSimpleName(), false);
} else {
- ClearFillSegmentationViewStart(segm, act2.getClass().getName(), true);
+ ClearFillSegmentationViewStart(segm, act2.getClass().getName(), false);
}
TestUtils.validateRecordEventInternalMock(ep, ModuleViews.VIEW_EVENT_KEY, segm, vals[0], 0, 1);
@@ -276,7 +278,8 @@ public void onActivityStartedStopped() throws InterruptedException, JSONExceptio
Thread.sleep(1000);
mCountly.moduleViews.onActivityStopped(0);//activity count = 0
- validateView(act.getClass().getSimpleName(), 0.0, 0, 2, true, true, TestUtils.map("aa", "11", "aagfg", "1133", "1", 123, "2", 234, "3", true), "idv1", "");
+ // start false because session did not start
+ validateView(act.getClass().getSimpleName(), 0.0, 0, 2, false, true, TestUtils.map("aa", "11", "aagfg", "1133", "1", 123, "2", 234, "3", true), "idv1", "");
validateView(act.getClass().getSimpleName(), 1.0, 1, 2, false, false, TestUtils.map("aa", "11", "aagfg", "1133", "1", 123, "2", 234, "3", true), "idv1", "");
}
@@ -294,7 +297,8 @@ public void recordViewNoSegm() throws InterruptedException {
String[] viewNames = { "DSD", "32", "DSD" };
final Map segm = new HashMap<>();
- ClearFillSegmentationViewStart(segm, viewNames[0], true);
+ // start false because session did not start
+ ClearFillSegmentationViewStart(segm, viewNames[0], false);
mCountly.views().recordView(viewNames[0]);
@@ -364,7 +368,8 @@ public void recordViewWithSegm() throws InterruptedException {
mCountly.views().recordView(viewNames[0], cSegm1);
- ClearFillSegmentationViewStart(segm, viewNames[0], true, globalSegm);
+ // start false because session did not start
+ ClearFillSegmentationViewStart(segm, viewNames[0], false, globalSegm);
TestUtils.validateRecordEventInternalMock(ep, ModuleViews.VIEW_EVENT_KEY, segm, vals[0], 0, 1);
clearInvocations(ep);
@@ -448,7 +453,7 @@ public void addSegmentationToView() throws InterruptedException {
String viewID = mCountly.views().startAutoStoppedView(viewNames[0]);
//make sure the first view event is recorded correctly
- ClearFillSegmentationViewStart(segm, viewNames[0], true, globalSegm);
+ ClearFillSegmentationViewStart(segm, viewNames[0], false, globalSegm); // First view false because session did not started
TestUtils.validateRecordEventInternalMock(ep, ModuleViews.VIEW_EVENT_KEY, segm, vals[0], 0, 1);
clearInvocations(ep);
@@ -588,7 +593,7 @@ public void autoSessionFlow_1() throws InterruptedException, JSONException {
String[] viewNames = { act.getClass().getSimpleName(), act2.getClass().getSimpleName(), act3.getClass().getSimpleName() };
final Map segm = new HashMap<>();
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
TestUtils.assertRQSize(0);
//go from one activity to another in the expected way and then "go to background"
///////// 1
@@ -838,7 +843,8 @@ public void performFullViewFlowBase(boolean useID, boolean startAutoCloseView) t
}
Assert.assertEquals(viewId, vals[0]);
- ClearFillSegmentationViewStart(segm, viewNames[0], true);
+ // start false because session did not start
+ ClearFillSegmentationViewStart(segm, viewNames[0], false);
TestUtils.validateRecordEventInternalMock(ep, ModuleViews.VIEW_EVENT_KEY, segm, vals[0], 0, 1);
clearInvocations(ep);
@@ -902,7 +908,8 @@ public void tripleViewBase(boolean useID) throws InterruptedException {
TestUtils.validateRecordEventInternalMockInteractions(ep, 0);
- viewID[0] = startViewInFlow(viewNames[0], vals[0], null, null, true, mCountly, ep);
+ // start false because session did not start
+ viewID[0] = startViewInFlow(viewNames[0], vals[0], null, null, false, mCountly, ep);
Thread.sleep(1000);
@@ -1020,7 +1027,8 @@ public void stopPausedViewBase(boolean useID) throws InterruptedException {
TestUtils.validateRecordEventInternalMockInteractions(ep, 0);
- viewID[0] = startViewInFlow(viewNames[0], vals[0], null, null, true, mCountly, ep);
+ // start false because session did not start
+ viewID[0] = startViewInFlow(viewNames[0], vals[0], null, null, false, mCountly, ep);
Thread.sleep(1000);
@@ -1096,7 +1104,8 @@ public void recordViewsWithSegmentationBase(boolean useID, boolean setSegmentati
Map givenStartSegm = new HashMap<>();
givenStartSegm.put("2", "v2");
- viewID[0] = startViewInFlow(viewNames[0], vals[0], givenStartSegm, globalSegm, true, mCountly, ep);
+ // start false because session did not start
+ viewID[0] = startViewInFlow(viewNames[0], vals[0], givenStartSegm, globalSegm, false, mCountly, ep);
Thread.sleep(1000);
@@ -1221,7 +1230,8 @@ public void validateSegmentationPrecedence() {
mCountly.views().startView("a", viewSegm);
Map segm = new HashMap<>();
- ClearFillSegmentationViewStart(segm, "a", true);
+ // start false because session did not start
+ ClearFillSegmentationViewStart(segm, "a", false);
segm.put("xx", 1);
segm.put("yy", 3);
segm.put("zz", 4);
@@ -1253,7 +1263,8 @@ public void overridingViewProtectedSegmentation() {
mCountly.views().startView("a", globalSegm);
Map segm = new HashMap<>();
- ClearFillSegmentationViewStart(segm, "a", true);
+ // start false because session did not start
+ ClearFillSegmentationViewStart(segm, "a", false);
TestUtils.validateRecordEventInternalMock(ep, ModuleViews.VIEW_EVENT_KEY, segm, vals[0], 0, 1);
clearInvocations(ep);
@@ -1274,7 +1285,8 @@ public void clearFirstViewFlagSessionEndBase(boolean manualSessions) throws Inte
TestUtils.assertRQSize(0);
mCountly.views().startView("a", null);
- validateView("a", 0.0, 0, 1, true, true, null, vals[0], "");
+ // start false because session did not start
+ validateView("a", 0.0, 0, 1, false, true, null, vals[0], "");
if (manualSessions) {
mCountly.sessions().beginSession();
@@ -1285,7 +1297,7 @@ public void clearFirstViewFlagSessionEndBase(boolean manualSessions) throws Inte
ModuleSessionsTests.validateSessionBeginRequest(1, TestUtils.commonDeviceId);
mCountly.views().startView("b", null);
- validateView("b", 0.0, 2, 3, false, true, null, vals[1], vals[0]);
+ validateView("b", 0.0, 2, 3, true, true, null, vals[1], vals[0]);
Thread.sleep(1000);
int lastViewIdx = 4;
@@ -1306,7 +1318,7 @@ public void clearFirstViewFlagSessionEndBase(boolean manualSessions) throws Inte
ModuleSessionsTests.validateSessionEndRequest(3, 1, TestUtils.commonDeviceId);
mCountly.views().startView("c", null);
- validateView("c", 0.0, lastViewIdx, lastViewIdx + 1, true, true, null, vals[2], vals[1]);
+ validateView("c", 0.0, lastViewIdx, lastViewIdx + 1, false, true, null, vals[2], vals[1]);
}
@Test
@@ -1332,7 +1344,8 @@ public void clearFirstViewFlagSessionConsentRemoved() throws JSONException {
// 0 is consent request
ModuleConsentTests.validateConsentRequest(TestUtils.commonDeviceId, 0, new boolean[] { false, false, false, false, false, false, false, false, false, false, false, false, true, false, false });
TestUtils.validateRequest(TestUtils.commonDeviceId, TestUtils.map("location", ""), 1);
- validateView("a", 0.0, 2, 3, true, true, null, vals[0], "");
+ // start false because session did not start
+ validateView("a", 0.0, 2, 3, false, true, null, vals[0], "");
//nothing should happen when session consent is given
mCountly.consent().giveConsent(new String[] { Countly.CountlyFeatureNames.sessions });
@@ -1346,7 +1359,8 @@ public void clearFirstViewFlagSessionConsentRemoved() throws JSONException {
ModuleConsentTests.validateConsentRequest(TestUtils.commonDeviceId, 5, new boolean[] { false, false, false, false, false, false, false, false, false, false, false, false, true, false, false });
mCountly.views().startView("c", null);
- validateView("c", 0.0, 6, 7, true, true, null, vals[2], vals[1]);
+ // start false because session did not start
+ validateView("c", 0.0, 6, 7, false, true, null, vals[2], vals[1]);
}
/**
@@ -1365,7 +1379,8 @@ public void internalLimits_setGlobalSegmentation_maxSegmentationValues() throws
Countly countly = new Countly().init(config);
countly.views().startView("a");
Map viewStartSegm = TestUtils.map();
- ClearFillSegmentationViewStart(viewStartSegm, "a", true, TestUtils.map("d", 4, "e", 5));
+ // start false because session did not start
+ ClearFillSegmentationViewStart(viewStartSegm, "a", false, TestUtils.map("d", 4, "e", 5));
ModuleEventsTests.validateEventInRQ(ModuleViews.VIEW_EVENT_KEY, viewStartSegm, 1, 0.0d, 0.0d, 0);
}
@@ -1384,7 +1399,8 @@ public void internalLimits_startEvent_maxSegmentationValues() throws JSONExcepti
Countly countly = new Countly().init(config);
countly.views().startView("a", TestUtils.map("d", 4, "e", 5, "f", 6));
Map viewStartSegm = TestUtils.map();
- ClearFillSegmentationViewStart(viewStartSegm, "a", true, TestUtils.map("f", 6, "e", 5));
+ // start false because session did not start
+ ClearFillSegmentationViewStart(viewStartSegm, "a", false, TestUtils.map("f", 6, "e", 5));
ModuleEventsTests.validateEventInRQ(ModuleViews.VIEW_EVENT_KEY, viewStartSegm, 1, 0.0d, 0.0d, 0);
}
@@ -1406,7 +1422,37 @@ public void internalLimits_setGlobalSegmentation_maxSegmentationValues_interface
Countly countly = new Countly().init(config);
countly.views().startView("a");
Map viewStartSegm = TestUtils.map();
- ClearFillSegmentationViewStart(viewStartSegm, "a", true);
+ // start false because session did not start
+ ClearFillSegmentationViewStart(viewStartSegm, "a", false);
+ ModuleEventsTests.validateEventInRQ(ModuleViews.VIEW_EVENT_KEY, viewStartSegm, 1, 0.0d, 0.0d, 0);
+
+ countly.views().setGlobalViewSegmentation(TestUtils.map("a", 1, "b", 2, "c", 3, "d", 4, "e", 5));
+ countly.views().stopViewWithName("a");
+ Map viewEndSegm = TestUtils.map();
+ ClearFillSegmentationViewEnd(viewEndSegm, "a", TestUtils.map("d", 4, "e", 5));
+ ModuleEventsTests.validateEventInRQ(ModuleViews.VIEW_EVENT_KEY, viewEndSegm, 1, 0.0d, 0.0d, 1);
+ }
+
+ /**
+ * Validate that max segmentation values clips the last two values of the
+ * global segmentation with server config
+ * Also validate that the global segmentation is updated correctly
+ * when the view is stopped
+ * "setGlobalViewSegmentation" call from the views interface is used
+ *
+ * @throws JSONException if JSON parsing fails
+ */
+ @Test
+ public void serverConfig_setGlobalSegmentation_maxSegmentationValues_interface() throws JSONException {
+ CountlyConfig config = TestUtils.createBaseConfig();
+ config.immediateRequestGenerator = ModuleConfigurationTests.createIRGForSpecificResponse(new ServerConfigBuilder().segmentationValuesLimit(2).build());
+ config.setEventQueueSizeToSend(1);
+
+ Countly countly = new Countly().init(config);
+ countly.views().startView("a");
+ Map viewStartSegm = TestUtils.map();
+ // start false because session did not start
+ ClearFillSegmentationViewStart(viewStartSegm, "a", false);
ModuleEventsTests.validateEventInRQ(ModuleViews.VIEW_EVENT_KEY, viewStartSegm, 1, 0.0d, 0.0d, 0);
countly.views().setGlobalViewSegmentation(TestUtils.map("a", 1, "b", 2, "c", 3, "d", 4, "e", 5));
@@ -1442,7 +1488,8 @@ public void internalLimit_recordViewsWithSegmentation() throws JSONException {
givenStartSegm.put("sop", 4);
String viewID = mCountly.views().startView("VIEW", givenStartSegm);
- validateView("VI", 0.0, 0, 1, true, true, TestUtils.map("av", "v1", "so", 4), "idv1", "");
+ // start false because session did not start
+ validateView("VI", 0.0, 0, 1, false, true, TestUtils.map("av", "v1", "so", 4), "idv1", "");
mCountly.views().setGlobalViewSegmentation(TestUtils.map("sunburn", true, "sunflower", "huh"));
@@ -1491,7 +1538,8 @@ public void internalLimit_recordViewsWithSegmentation_maxValueSize() throws JSON
} else {
segm = TestUtils.map("yo", "wo", "so", "ma", "av", "v1", "i_", "i_");
}
- validateView("VI", 0.0, 0, 1, true, true, segm, "idv1", "");
+ // start false because session did not start
+ validateView("VI", 0.0, 0, 1, false, true, segm, "idv1", "");
mCountly.views().setGlobalViewSegmentation(TestUtils.map("go", 45, "gone", 567.78f));
@@ -1555,7 +1603,8 @@ public void startView_validateSupportedArrays() throws JSONException {
countly.views().startView("test", segmentation);
Map expectedSegmentation = TestUtils.map();
- ClearFillSegmentationViewStart(expectedSegmentation, "test", true);
+ // start false because session did not start
+ ClearFillSegmentationViewStart(expectedSegmentation, "test", false);
expectedSegmentation.putAll(TestUtils.map(
"arr", new JSONArray(arr),
"arrB", new JSONArray(arrB),
@@ -1611,7 +1660,8 @@ public void startView_validateSupportedLists() throws JSONException {
countly.views().startView("test", segmentation);
Map expectedSegmentation = TestUtils.map();
- ClearFillSegmentationViewStart(expectedSegmentation, "test", true);
+ // start false because session did not start
+ ClearFillSegmentationViewStart(expectedSegmentation, "test", false);
// Prepare expected segmentation with JSONArrays
expectedSegmentation.putAll(TestUtils.map(
@@ -1667,7 +1717,8 @@ public void startView_validateSupportedJSONArrays() throws JSONException {
countly.views().startView("test", segmentation);
Map expectedSegmentation = TestUtils.map();
- ClearFillSegmentationViewStart(expectedSegmentation, "test", true);
+ // start false because session did not start
+ ClearFillSegmentationViewStart(expectedSegmentation, "test", false);
// Prepare expected segmentation with JSONArrays
expectedSegmentation.putAll(TestUtils.map(
@@ -1708,9 +1759,11 @@ public void startView_unsupportedDataTypesSegmentation() throws JSONException {
countly.views().startView("test", segmentation);
Map expectedSegmentation = TestUtils.map();
- ClearFillSegmentationViewStart(expectedSegmentation, "test", true);
+ // start false because session did not start
+ ClearFillSegmentationViewStart(expectedSegmentation, "test", false);
- validateView("test", 0.0, 0, 1, true, false, expectedSegmentation, "idv1", "");
+ // start false because session did not start
+ validateView("test", 0.0, 0, 1, false, false, expectedSegmentation, "idv1", "");
}
/**
@@ -1849,7 +1902,8 @@ public void recordView_previousViewName() throws JSONException {
Countly countly = new Countly().init(countlyConfig);
countly.views().startView("test");
- validateView("test", 0.0, 0, 1, true, true, TestUtils.map(), "_CLY_", "_CLY_", "");
+ // start false because session did not start
+ validateView("test", 0.0, 0, 1, false, true, TestUtils.map(), "_CLY_", "_CLY_", "");
countly.views().startView("test2");
validateView("test2", 0.0, 1, 2, false, true, TestUtils.map(), "_CLY_", "_CLY_", "test");
@@ -1873,14 +1927,20 @@ public void startView_consentRemoval() throws JSONException {
countly.views().startView("test");
ModuleConsentTests.validateAllConsentRequest(TestUtils.commonDeviceId, 0);
- validateView("test", 0.0, 1, 2, true, true, TestUtils.map(), "_CLY_", "_CLY_", null);
+ // start false because session did not start
+ validateView("test", 0.0, 1, 2, false, true, TestUtils.map(), "_CLY_", "_CLY_", null);
countly.views().startView("test2");
validateView("test2", 0.0, 2, 3, false, true, TestUtils.map(), "_CLY_", "_CLY_", null);
countly.consent().removeConsent(new String[] { Countly.CountlyFeatureNames.views });
- validateView("test", 0.0, 3, 6, false, false, TestUtils.map(), "_CLY_", "_CLY_", null);
- validateView("test2", 0.0, 4, 6, false, false, TestUtils.map(), "_CLY_", "_CLY_", null);
+ try {
+ validateView("test", 0.0, 3, 6, false, false, TestUtils.map(), "_CLY_", "_CLY_", null);
+ validateView("test2", 0.0, 4, 6, false, false, TestUtils.map(), "_CLY_", "_CLY_", null);
+ } catch (Exception ignored) {
+ validateView("test", 0.0, 4, 6, false, false, TestUtils.map(), "_CLY_", "_CLY_", null);
+ validateView("test2", 0.0, 3, 6, false, false, TestUtils.map(), "_CLY_", "_CLY_", null);
+ }
ModuleConsentTests.validateConsentRequest(TestUtils.commonDeviceId, 5, new boolean[] { true, true, true, true, true, true, true, true, true, true, true, true, false, true, true });
countly.consent().giveConsent(new String[] { Countly.CountlyFeatureNames.views });
@@ -1906,7 +1966,8 @@ public void startAutoStoppedView_consentRemoval() throws JSONException {
countly.views().startAutoStoppedView("test");
ModuleConsentTests.validateAllConsentRequest(TestUtils.commonDeviceId, 0);
- validateView("test", 0.0, 1, 2, true, true, TestUtils.map(), "_CLY_", "_CLY_", null);
+ // start false because session did not start
+ validateView("test", 0.0, 1, 2, false, true, TestUtils.map(), "_CLY_", "_CLY_", null);
countly.views().startAutoStoppedView("test2");
validateView("test", 0.0, 2, 4, false, false, TestUtils.map(), "_CLY_", "_CLY_", null);
@@ -1921,6 +1982,45 @@ public void startAutoStoppedView_consentRemoval() throws JSONException {
Assert.assertEquals(7, TestUtils.getCurrentRQ().length);
}
+ /**
+ * "startView" and "startAutoStoppedView" with start parameter,
+ * start parameter is only added to the first view of a session
+ *
+ * @throws JSONException if the JSON is not valid
+ */
+ @Test
+ public void startView_firstViewsInSessions() throws JSONException, InterruptedException {
+ CountlyConfig countlyConfig = TestUtils.createBaseConfig();
+ countlyConfig.setTrackOrientationChanges(false);
+ countlyConfig.enableManualSessionControl();
+ countlyConfig.setEventQueueSizeToSend(1);
+
+ Countly countly = new Countly().init(countlyConfig);
+
+ countly.views().startAutoStoppedView("test");
+ // start false because session did not start
+ validateView("test", 0.0, 0, 1, false, true, TestUtils.map(), "_CLY_", "_CLY_", null);
+
+ countly.sessions().beginSession();
+ Thread.sleep(2000);
+
+ ModuleSessionsTests.validateSessionBeginRequest(1, TestUtils.commonDeviceId);
+ countly.views().startAutoStoppedView("test2");
+ validateView("test", 2.0, 2, 4, false, false, TestUtils.map(), "_CLY_", "_CLY_", null);
+ validateView("test2", 0.0, 3, 4, true, true, TestUtils.map(), "_CLY_", "_CLY_", null);
+
+ countly.sessions().endSession();
+ ModuleSessionsTests.validateSessionEndRequest(4, 2, TestUtils.commonDeviceId);
+
+ countly.views().startView("test3");
+ validateView("test2", 0.0, 5, 7, false, false, TestUtils.map(), "_CLY_", "_CLY_", null);
+ validateView("test3", 0.0, 6, 7, false, true, TestUtils.map(), "_CLY_", "_CLY_", null);
+ countly.sessions().beginSession();
+ ModuleSessionsTests.validateSessionBeginRequest(7, TestUtils.commonDeviceId);
+ countly.views().startAutoStoppedView("test4");
+ validateView("test4", 0.0, 8, 9, true, true, TestUtils.map(), "_CLY_", "_CLY_", null);
+ }
+
/**
* Auto view tracking with consent removal
* Validate that running view is stopped when the view consent is removed
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/RemoteConfigVariantControlTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/RemoteConfigVariantControlTests.java
index b6ea23f72..aca945ec8 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/RemoteConfigVariantControlTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/RemoteConfigVariantControlTests.java
@@ -181,7 +181,7 @@ public void testConvertVariantsJsonToMap_NullJsonKey() throws JSONException {
@Test
public void testNormalFlow() {
- CountlyConfig config = TestUtils.createVariantConfig(createIRGForSpecificResponse("{\"key\":[{\"name\":\"variant\"}]}"));
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse("{\"key\":[{\"name\":\"variant\"}]}"));
Countly countly = new Countly().init(config);
// Developer did not provide a callback
@@ -204,7 +204,7 @@ public void testNormalFlow() {
*/
@Test
public void testNullVariant() {
- CountlyConfig config = TestUtils.createVariantConfig(createIRGForSpecificResponse("{\"key\":[{\"name\":null}]}"));
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse("{\"key\":[{\"name\":null}]}"));
Countly countly = new Countly().init(config);
// Developer did not provide a callback
@@ -221,7 +221,7 @@ public void testNullVariant() {
*/
@Test
public void testFilteringWrongKeys() {
- CountlyConfig config = TestUtils.createVariantConfig(createIRGForSpecificResponse("{\"key\":[{\"noname\":\"variant1\"},{\"name\":\"variant2\"}]}"));
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(createIRGForSpecificResponse("{\"key\":[{\"noname\":\"variant1\"},{\"name\":\"variant2\"}]}"));
Countly countly = new Countly().init(config);
// Developer did not provide a callback
@@ -255,7 +255,7 @@ ImmediateRequestGenerator createIRGForSpecificResponse(final String targetRespon
@Test
public void variantGetters_preDownload() {
- CountlyConfig config = TestUtils.createVariantConfig(null);
+ CountlyConfig config = TestUtils.createIRGeneratorConfig(null);
Countly countly = new Countly().init(config);
//should return empty map of values
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ServerConfigBuilder.java b/sdk/src/androidTest/java/ly/count/android/sdk/ServerConfigBuilder.java
new file mode 100644
index 000000000..003c68726
--- /dev/null
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/ServerConfigBuilder.java
@@ -0,0 +1,264 @@
+package ly.count.android.sdk;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Assert;
+
+import static ly.count.android.sdk.ModuleConfiguration.keyRConfig;
+import static ly.count.android.sdk.ModuleConfiguration.keyRConsentRequired;
+import static ly.count.android.sdk.ModuleConfiguration.keyRContentZoneInterval;
+import static ly.count.android.sdk.ModuleConfiguration.keyRCrashReporting;
+import static ly.count.android.sdk.ModuleConfiguration.keyRCustomEventTracking;
+import static ly.count.android.sdk.ModuleConfiguration.keyRDropOldRequestTime;
+import static ly.count.android.sdk.ModuleConfiguration.keyREnterContentZone;
+import static ly.count.android.sdk.ModuleConfiguration.keyREventQueueSize;
+import static ly.count.android.sdk.ModuleConfiguration.keyRLimitBreadcrumb;
+import static ly.count.android.sdk.ModuleConfiguration.keyRLimitKeyLength;
+import static ly.count.android.sdk.ModuleConfiguration.keyRLimitSegValues;
+import static ly.count.android.sdk.ModuleConfiguration.keyRLimitTraceLength;
+import static ly.count.android.sdk.ModuleConfiguration.keyRLimitTraceLine;
+import static ly.count.android.sdk.ModuleConfiguration.keyRLimitValueSize;
+import static ly.count.android.sdk.ModuleConfiguration.keyRLocationTracking;
+import static ly.count.android.sdk.ModuleConfiguration.keyRLogging;
+import static ly.count.android.sdk.ModuleConfiguration.keyRNetworking;
+import static ly.count.android.sdk.ModuleConfiguration.keyRRefreshContentZone;
+import static ly.count.android.sdk.ModuleConfiguration.keyRReqQueueSize;
+import static ly.count.android.sdk.ModuleConfiguration.keyRServerConfigUpdateInterval;
+import static ly.count.android.sdk.ModuleConfiguration.keyRSessionTracking;
+import static ly.count.android.sdk.ModuleConfiguration.keyRSessionUpdateInterval;
+import static ly.count.android.sdk.ModuleConfiguration.keyRTimestamp;
+import static ly.count.android.sdk.ModuleConfiguration.keyRTracking;
+import static ly.count.android.sdk.ModuleConfiguration.keyRVersion;
+import static ly.count.android.sdk.ModuleConfiguration.keyRViewTracking;
+
+class ServerConfigBuilder {
+ final Map config;
+ private long timestamp;
+ private String version;
+
+ public ServerConfigBuilder() {
+ config = new HashMap<>();
+ timestamp = System.currentTimeMillis();
+ version = "1";
+ }
+
+ ServerConfigBuilder timestamp(long timestamp) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ ServerConfigBuilder version(String version) {
+ this.version = version;
+ return this;
+ }
+
+ ServerConfigBuilder tracking(boolean enabled) {
+ config.put(keyRTracking, enabled);
+ return this;
+ }
+
+ ServerConfigBuilder networking(boolean enabled) {
+ config.put(keyRNetworking, enabled);
+ return this;
+ }
+
+ ServerConfigBuilder crashReporting(boolean enabled) {
+ config.put(keyRCrashReporting, enabled);
+ return this;
+ }
+
+ ServerConfigBuilder viewTracking(boolean enabled) {
+ config.put(keyRViewTracking, enabled);
+ return this;
+ }
+
+ ServerConfigBuilder sessionTracking(boolean enabled) {
+ config.put(keyRSessionTracking, enabled);
+ return this;
+ }
+
+ ServerConfigBuilder customEventTracking(boolean enabled) {
+ config.put(keyRCustomEventTracking, enabled);
+ return this;
+ }
+
+ ServerConfigBuilder contentZone(boolean enabled) {
+ config.put(keyREnterContentZone, enabled);
+ return this;
+ }
+
+ ServerConfigBuilder locationTracking(boolean enabled) {
+ config.put(keyRLocationTracking, enabled);
+ return this;
+ }
+
+ ServerConfigBuilder refreshContentZone(boolean enabled) {
+ config.put(keyRRefreshContentZone, enabled);
+ return this;
+ }
+
+ ServerConfigBuilder serverConfigUpdateInterval(int interval) {
+ config.put(keyRServerConfigUpdateInterval, interval);
+ return this;
+ }
+
+ ServerConfigBuilder requestQueueSize(int size) {
+ config.put(keyRReqQueueSize, size);
+ return this;
+ }
+
+ ServerConfigBuilder eventQueueSize(int size) {
+ config.put(keyREventQueueSize, size);
+ return this;
+ }
+
+ ServerConfigBuilder logging(boolean enabled) {
+ config.put(keyRLogging, enabled);
+ return this;
+ }
+
+ ServerConfigBuilder sessionUpdateInterval(int interval) {
+ config.put(keyRSessionUpdateInterval, interval);
+ return this;
+ }
+
+ ServerConfigBuilder contentZoneInterval(int interval) {
+ config.put(keyRContentZoneInterval, interval);
+ return this;
+ }
+
+ ServerConfigBuilder consentRequired(boolean required) {
+ config.put(keyRConsentRequired, required);
+ return this;
+ }
+
+ ServerConfigBuilder dropOldRequestTime(int hours) {
+ config.put(keyRDropOldRequestTime, hours);
+ return this;
+ }
+
+ ServerConfigBuilder keyLengthLimit(int limit) {
+ config.put(keyRLimitKeyLength, limit);
+ return this;
+ }
+
+ ServerConfigBuilder valueSizeLimit(int limit) {
+ config.put(keyRLimitValueSize, limit);
+ return this;
+ }
+
+ ServerConfigBuilder segmentationValuesLimit(int limit) {
+ config.put(keyRLimitSegValues, limit);
+ return this;
+ }
+
+ ServerConfigBuilder breadcrumbLimit(int limit) {
+ config.put(keyRLimitBreadcrumb, limit);
+ return this;
+ }
+
+ ServerConfigBuilder traceLengthLimit(int limit) {
+ config.put(keyRLimitTraceLength, limit);
+ return this;
+ }
+
+ ServerConfigBuilder traceLinesLimit(int limit) {
+ config.put(keyRLimitTraceLine, limit);
+ return this;
+ }
+
+ ServerConfigBuilder defaults() {
+ // Feature flags
+ tracking(true);
+ networking(true);
+ crashReporting(true);
+ viewTracking(true);
+ sessionTracking(true);
+ customEventTracking(true);
+ contentZone(false);
+ locationTracking(true);
+ refreshContentZone(true);
+
+ // Intervals and sizes
+ serverConfigUpdateInterval(4);
+ requestQueueSize(1000);
+ eventQueueSize(100);
+ logging(false);
+ sessionUpdateInterval(60);
+ contentZoneInterval(30);
+ consentRequired(false);
+ dropOldRequestTime(0);
+
+ // Set default limits
+ keyLengthLimit(Countly.maxKeyLengthDefault);
+ valueSizeLimit(Countly.maxValueSizeDefault);
+ segmentationValuesLimit(Countly.maxSegmentationValuesDefault);
+ breadcrumbLimit(Countly.maxBreadcrumbCountDefault);
+ traceLengthLimit(Countly.maxStackTraceLineLengthDefault);
+ traceLinesLimit(Countly.maxStackTraceLinesPerThreadDefault);
+
+ return this;
+ }
+
+ String build() throws JSONException {
+ return buildJson().toString();
+ }
+
+ JSONObject buildJson() throws JSONException {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put(keyRTimestamp, timestamp);
+ jsonObject.put(keyRVersion, version);
+ jsonObject.put(keyRConfig, new JSONObject(config));
+ return jsonObject;
+ }
+
+ /**
+ * Validates the configuration values against the provided Countly instance
+ */
+ void validateAgainst(Countly countly) {
+ validateFeatureFlags(countly);
+ validateIntervalsAndSizes(countly);
+ validateLimits(countly);
+ }
+
+ private void validateFeatureFlags(Countly countly) {
+ Assert.assertEquals(config.get(keyRTracking), countly.config_.configProvider.getTrackingEnabled());
+ Assert.assertEquals(config.get(keyRNetworking), countly.config_.configProvider.getNetworkingEnabled());
+ Assert.assertEquals(config.get(keyRCrashReporting), countly.config_.configProvider.getCrashReportingEnabled());
+ Assert.assertEquals(config.get(keyRViewTracking), countly.config_.configProvider.getViewTrackingEnabled());
+ Assert.assertEquals(config.get(keyRSessionTracking), countly.config_.configProvider.getSessionTrackingEnabled());
+ Assert.assertEquals(config.get(keyRCustomEventTracking), countly.config_.configProvider.getCustomEventTrackingEnabled());
+ Assert.assertEquals(config.get(keyREnterContentZone), countly.config_.configProvider.getContentZoneEnabled());
+ Assert.assertEquals(config.get(keyRLocationTracking), countly.config_.configProvider.getLocationTrackingEnabled());
+ Assert.assertEquals(config.get(keyRRefreshContentZone), countly.config_.configProvider.getRefreshContentZoneEnabled());
+ }
+
+ private void validateIntervalsAndSizes(Countly countly) {
+ Assert.assertEquals(config.get(keyRServerConfigUpdateInterval), countly.moduleConfiguration.serverConfigUpdateInterval);
+ Assert.assertEquals(config.get(keyRReqQueueSize), countly.config_.maxRequestQueueSize);
+ Assert.assertEquals(config.get(keyREventQueueSize), countly.EVENT_QUEUE_SIZE_THRESHOLD);
+ Assert.assertEquals(config.get(keyRLogging), countly.config_.loggingEnabled);
+
+ try {
+ Assert.assertEquals(config.get(keyRSessionUpdateInterval), countly.config_.sessionUpdateTimerDelay);
+ } catch (AssertionError _ignored) {
+ // This is a workaround for the issue where sessionUpdateTimerDelay is null by default
+ Assert.assertNull(countly.config_.sessionUpdateTimerDelay);
+ }
+
+ Assert.assertEquals(config.get(keyRContentZoneInterval), countly.config_.content.zoneTimerInterval);
+ Assert.assertEquals(config.get(keyRConsentRequired), countly.config_.shouldRequireConsent);
+ Assert.assertEquals(config.get(keyRDropOldRequestTime), countly.config_.dropAgeHours);
+ }
+
+ private void validateLimits(Countly countly) {
+ Assert.assertEquals(config.get(keyRLimitKeyLength), countly.config_.sdkInternalLimits.maxKeyLength);
+ Assert.assertEquals(config.get(keyRLimitValueSize), countly.config_.sdkInternalLimits.maxValueSize);
+ Assert.assertEquals(config.get(keyRLimitSegValues), countly.config_.sdkInternalLimits.maxSegmentationValues);
+ Assert.assertEquals(config.get(keyRLimitBreadcrumb), countly.config_.sdkInternalLimits.maxBreadcrumbCount);
+ Assert.assertEquals(config.get(keyRLimitTraceLength), countly.config_.sdkInternalLimits.maxStackTraceLineLength);
+ Assert.assertEquals(config.get(keyRLimitTraceLine), countly.config_.sdkInternalLimits.maxStackTraceLinesPerThread);
+ }
+}
\ No newline at end of file
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java b/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java
index a3b5362db..59d030627 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java
@@ -44,7 +44,7 @@ public class TestUtils {
public final static String commonAppKey = "appkey";
public final static String commonDeviceId = "1234";
public final static String SDK_NAME = "java-native-android";
- public final static String SDK_VERSION = "24.7.5";
+ public final static String SDK_VERSION = "25.4.0";
public static final int MAX_THREAD_COUNT_PER_STACK_TRACE = 50;
public static class Activity2 extends Activity {
@@ -53,23 +53,9 @@ public static class Activity2 extends Activity {
public static class Activity3 extends Activity {
}
- public static CountlyConfig createConfigurationConfig(boolean enableServerConfig, ImmediateRequestGenerator irGen) {
+ static CountlyConfig createIRGeneratorConfig(ImmediateRequestGenerator irGen) {
CountlyConfig cc = createBaseConfig();
-
cc.immediateRequestGenerator = irGen;
-
- if (enableServerConfig) {
- cc.enableServerConfiguration();
- }
-
- return cc;
- }
-
- public static CountlyConfig createVariantConfig(ImmediateRequestGenerator irGen) {
- CountlyConfig cc = createBaseConfig();
-
- cc.immediateRequestGenerator = irGen;
-
return cc;
}
@@ -80,7 +66,8 @@ public static CountlyConfig createConsentCountlyConfig(boolean requiresConsent,
.disableHealthCheck();//mocked tests fail without disabling this
cc.testModuleListener = testModuleListener;
cc.requestQueueProvider = rqp;
-
+ cc.immediateRequestGenerator = () -> (ImmediateRequestI) (requestData, customEndpoint, cp, requestShouldBeDelayed, networkingIsEnabled, callback, log) -> {
+ };
return cc;
}
@@ -97,6 +84,9 @@ public static CountlyConfig createAttributionCountlyConfig(boolean requiresConse
.disableHealthCheck();//mocked tests fail without disabling this
cc.testModuleListener = testModuleListener;
cc.requestQueueProvider = rqp;
+ cc.immediateRequestGenerator = () -> (ImmediateRequestI) (requestData, customEndpoint, cp, requestShouldBeDelayed, networkingIsEnabled, callback, log) -> {
+
+ };
return cc;
}
@@ -484,7 +474,7 @@ public static void verifyCurrentPreviousViewID(ModuleViews mv, String current, S
Assert.assertEquals(previous, mv.getPreviousViewId());
}
- protected static CountlyStore getCountyStore() {
+ protected static CountlyStore getCountlyStore() {
return new CountlyStore(getContext(), mock(ModuleLog.class), false);
}
@@ -505,7 +495,7 @@ protected static CountlyStore getCountyStore() {
*/
protected static @NonNull Map[] getCurrentRQ(String filter) {
//get all request files from target folder
- String[] requests = getCountyStore().getRequests();
+ String[] requests = getCountlyStore().getRequests();
//create array of request params
Map[] resultMapArray = new ConcurrentHashMap[requests.length];
@@ -528,9 +518,9 @@ protected static CountlyStore getCountyStore() {
}
protected static void removeRequestContains(String search) {
- for (String request : getCountyStore().getRequests()) {
+ for (String request : getCountlyStore().getRequests()) {
if (request.contains(search)) {
- getCountyStore().removeRequest(request);
+ getCountlyStore().removeRequest(request);
}
}
}
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/TestUtilsTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/TestUtilsTests.java
index ecb4a1e57..b96fb2aef 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/TestUtilsTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/TestUtilsTests.java
@@ -12,7 +12,7 @@ public class TestUtilsTests {
@Before
public void setUp() {
- CountlyStore store = TestUtils.getCountyStore();
+ CountlyStore store = TestUtils.getCountlyStore();
store.clear(); // clear the store to make sure that there are no requests from previous tests
}
@@ -34,7 +34,7 @@ public void getCurrentRQ() {
public void getCurrentRQ_notEmpty() {
Assert.assertEquals(0, TestUtils.getCurrentRQ().length);
- TestUtils.getCountyStore().addRequest("a=b&c=d&hi=7628y9u0%C4%B1oh&fiyua=5765", true);
+ TestUtils.getCountlyStore().addRequest("a=b&c=d&hi=7628y9u0%C4%B1oh&fiyua=5765", true);
Assert.assertEquals(1, TestUtils.getCurrentRQ().length);
Map request = TestUtils.getCurrentRQ()[0];
@@ -54,7 +54,7 @@ public void getCurrentRQ_notEmpty() {
*/
@Test
public void getCurrentRQ_trashRequest() {
- CountlyStore store = TestUtils.getCountyStore();
+ CountlyStore store = TestUtils.getCountlyStore();
store.addRequest("This is not a request", true);
Assert.assertEquals(1, TestUtils.getCurrentRQ().length);
Assert.assertEquals("", TestUtils.getCurrentRQ()[0].get("This is not a request"));
@@ -68,7 +68,7 @@ public void getCurrentRQ_trashRequest() {
*/
@Test
public void getCurrentRQ_wrongStructure() {
- CountlyStore store = TestUtils.getCountyStore();
+ CountlyStore store = TestUtils.getCountlyStore();
store.addRequest("&s==1", true);
Assert.assertEquals(1, TestUtils.getCurrentRQ().length);
Assert.assertNull(TestUtils.getCurrentRQ()[0].get("="));
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/scSE_SessionsTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/scSE_SessionsTests.java
index e24eb24b6..f0b07ccec 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/scSE_SessionsTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/scSE_SessionsTests.java
@@ -23,13 +23,13 @@ public class scSE_SessionsTests {
@Before
public void setUp() {
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
Countly.sharedInstance().halt();
}
@After
public void tearDown() {
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
Countly.sharedInstance().halt();
}
@@ -312,7 +312,7 @@ public void SE_206_CR_CNG_A_id_change() throws InterruptedException {
flowAutomaticSessions(countly, new TestLifecycleObserver());
- Assert.assertEquals(6, TestUtils.getCurrentRQ().length);
+ Assert.assertEquals(5, TestUtils.getCurrentRQ().length);
validateSessionConsentRequest(0, false, TestUtils.commonDeviceId);
validateRequest(TestUtils.map("location", ""), 1);
TestUtils.validateRequest("newID", TestUtils.map("old_device_id", TestUtils.commonDeviceId), 2);
diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/scUP_UserProfileTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/scUP_UserProfileTests.java
index 745c425b8..e6f36624e 100644
--- a/sdk/src/androidTest/java/ly/count/android/sdk/scUP_UserProfileTests.java
+++ b/sdk/src/androidTest/java/ly/count/android/sdk/scUP_UserProfileTests.java
@@ -22,13 +22,13 @@ public class scUP_UserProfileTests {
@Before
public void setUp() {
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
Countly.sharedInstance().halt();
}
@After
public void tearDown() {
- TestUtils.getCountyStore().clear();
+ TestUtils.getCountlyStore().clear();
Countly.sharedInstance().halt();
}
diff --git a/sdk/src/main/AndroidManifest.xml b/sdk/src/main/AndroidManifest.xml
index 4a73b03db..bc4fb5552 100644
--- a/sdk/src/main/AndroidManifest.xml
+++ b/sdk/src/main/AndroidManifest.xml
@@ -10,9 +10,8 @@
android:taskAffinity=".CountlyPushActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false"/>
-
diff --git a/sdk/src/main/java/ly/count/android/sdk/CertificateTrustManager.java b/sdk/src/main/java/ly/count/android/sdk/CertificateTrustManager.java
index e1f3ee845..cd9cc9ecb 100644
--- a/sdk/src/main/java/ly/count/android/sdk/CertificateTrustManager.java
+++ b/sdk/src/main/java/ly/count/android/sdk/CertificateTrustManager.java
@@ -1,5 +1,6 @@
package ly.count.android.sdk;
+import android.net.http.X509TrustManagerExtensions;
import android.util.Base64;
import java.io.ByteArrayInputStream;
import java.security.KeyStore;
@@ -54,23 +55,30 @@ public CertificateTrustManager(String[] keys, String[] certs) throws Certificate
}
}
+ public void checkServerTrusted(X509Certificate[] chain, String authType, String host) throws CertificateException {
+ performCommonChecks(chain, authType, host);
+ }
+
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
- if (chain == null) {
- throw new IllegalArgumentException("PublicKeyManager: X509Certificate array is null");
- }
+ performCommonChecks(chain, authType, null);
+ }
- if (!(chain.length > 0)) {
- throw new IllegalArgumentException("PublicKeyManager: X509Certificate is empty");
+ private void performCommonChecks(X509Certificate[] chain, String authType, String host) throws CertificateException {
+ if (chain == null || chain.length == 0) {
+ throw new IllegalArgumentException("PublicKeyManager: X509Certificate array is null or empty");
}
- // Perform customary SSL/TLS checks
- TrustManagerFactory tmf;
try {
- tmf = TrustManagerFactory.getInstance("X509");
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init((KeyStore) null);
for (TrustManager trustManager : tmf.getTrustManagers()) {
- ((X509TrustManager) trustManager).checkServerTrusted(chain, authType);
+ if (host != null && trustManager instanceof X509TrustManager) {
+ X509TrustManagerExtensions x509TrustManagerExtensions = new X509TrustManagerExtensions((X509TrustManager) trustManager);
+ x509TrustManagerExtensions.checkServerTrusted(chain, authType, host);
+ } else {
+ ((X509TrustManager) trustManager).checkServerTrusted(chain, authType);
+ }
}
} catch (Exception e) {
throw new CertificateException(e);
diff --git a/sdk/src/main/java/ly/count/android/sdk/ConfigContent.java b/sdk/src/main/java/ly/count/android/sdk/ConfigContent.java
index c771abe50..870f0f3c1 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ConfigContent.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ConfigContent.java
@@ -2,19 +2,19 @@
public class ConfigContent {
- int contentUpdateInterval = 30;
+ int zoneTimerInterval = 30;
ContentCallback globalContentCallback = null;
/**
* Set the interval for the automatic content update calls
*
- * @param contentUpdateInterval in seconds
+ * @param zoneTimerIntervalSeconds in seconds
* @return config content to chain calls
* @apiNote This is an EXPERIMENTAL feature, and it can have breaking changes
*/
- private synchronized ConfigContent setContentUpdateInterval(int contentUpdateInterval) {
- if (contentUpdateInterval > 0) {
- this.contentUpdateInterval = contentUpdateInterval;
+ public synchronized ConfigContent setZoneTimerInterval(int zoneTimerIntervalSeconds) {
+ if (zoneTimerIntervalSeconds > 15) {
+ this.zoneTimerInterval = zoneTimerIntervalSeconds;
}
return this;
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java b/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java
index b0134fec9..524011bfb 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java
@@ -4,4 +4,18 @@ interface ConfigurationProvider {
boolean getNetworkingEnabled();
boolean getTrackingEnabled();
+
+ boolean getSessionTrackingEnabled();
+
+ boolean getViewTrackingEnabled();
+
+ boolean getCustomEventTrackingEnabled();
+
+ boolean getContentZoneEnabled();
+
+ boolean getCrashReportingEnabled();
+
+ boolean getLocationTrackingEnabled();
+
+ boolean getRefreshContentZoneEnabled();
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java b/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java
index e73018878..ca8b9dc31 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java
@@ -339,8 +339,7 @@ public void run() {
}
if (deviceIdProvider_.getDeviceId() == null) {
- // When device ID is supplied by OpenUDID or by Google Advertising ID.
- // In some cases it might take time for them to initialize. So, just wait for it.
+ // This might not be the case anymore, check it out TODO
L.i("[ConnectionProcessor] No Device ID available yet, skipping request " + storedRequests[0]);
break;
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java b/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java
index 3511df032..b7ea7c15c 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java
@@ -195,9 +195,11 @@ public void beginSession(boolean locationDisabled, @Nullable String locationCoun
data += "&begin_session=1"
+ "&metrics=" + preparedMetrics;//can be only sent with begin session
- String locationData = prepareLocationData(locationDisabled, locationCountryCode, locationCity, locationGpsCoordinates, locationIpAddress);
- if (!locationData.isEmpty()) {
- data += locationData;
+ if (configProvider.getLocationTrackingEnabled()) {
+ String locationData = prepareLocationData(locationDisabled, locationCountryCode, locationCity, locationGpsCoordinates, locationIpAddress);
+ if (!locationData.isEmpty()) {
+ data += locationData;
+ }
}
Countly.sharedInstance().isBeginSessionSent = true;
@@ -823,7 +825,7 @@ public String prepareHealthCheckRequest(String preparedMetrics) {
return prepareCommonRequestData() + "&metrics=" + preparedMetrics;
}
- public String prepareFetchContents(int portraitWidth, int portraitHeight, int landscapeWidth, int landscapeHeight, String[] categories) {
+ public String prepareFetchContents(int portraitWidth, int portraitHeight, int landscapeWidth, int landscapeHeight, String[] categories, String language, String deviceType) {
JSONObject json = new JSONObject();
try {
@@ -841,7 +843,7 @@ public String prepareFetchContents(int portraitWidth, int portraitHeight, int la
L.e("Error while preparing fetch contents request");
}
- return prepareCommonRequestData() + "&method=queue" + "&category=" + Arrays.asList(categories) + "&resolution=" + UtilsNetworking.urlEncodeString(json.toString());
+ return prepareCommonRequestData() + "&method=queue" + "&category=" + Arrays.asList(categories) + "&resolution=" + UtilsNetworking.urlEncodeString(json.toString()) + "&la=" + language + "&dt=" + deviceType;
}
@Override
diff --git a/sdk/src/main/java/ly/count/android/sdk/Countly.java b/sdk/src/main/java/ly/count/android/sdk/Countly.java
index c481e68bb..c85f84dd5 100644
--- a/sdk/src/main/java/ly/count/android/sdk/Countly.java
+++ b/sdk/src/main/java/ly/count/android/sdk/Countly.java
@@ -47,7 +47,7 @@ of this software and associated documentation files (the "Software"), to deal
*/
public class Countly {
- private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "24.7.5";
+ private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "25.4.0";
/**
* Used as request meta data on every request
@@ -97,7 +97,7 @@ public class Countly {
/**
* How often onTimer() is called. This is the default value.
*/
- private static final long TIMER_DELAY_IN_SECONDS = 60;
+ protected static final long TIMER_DELAY_IN_SECONDS = 60;
protected static String[] publicKeyPinCertificates;
protected static String[] certificatePinCertificates;
@@ -127,13 +127,13 @@ public enum CountlyMessagingProvider {
}
//SDK limit defaults
- final int maxKeyLengthDefault = 128;
- final int maxValueSizeDefault = 256;
- final int maxSegmentationValuesDefault = 100;
- final int maxBreadcrumbCountDefault = 100;
- final int maxStackTraceLinesPerThreadDefault = 30;
- final int maxStackTraceLineLengthDefault = 200;
- final int maxStackTraceThreadCountDefault = 50;
+ static final int maxKeyLengthDefault = 128;
+ static final int maxValueSizeDefault = 256;
+ static final int maxSegmentationValuesDefault = 100;
+ static final int maxBreadcrumbCountDefault = 100;
+ static final int maxStackTraceLinesPerThreadDefault = 30;
+ static final int maxStackTraceLineLengthDefault = 200;
+ static final int maxStackTraceThreadCountDefault = 50;
// see http://stackoverflow.com/questions/7048198/thread-safe-singletons-in-java
private static class SingletonHolder {
@@ -695,7 +695,6 @@ public synchronized Countly init(CountlyConfig config) {
sdkIsInitialised = true;
//AFTER THIS POINT THE SDK IS COUNTED AS INITIALISED
-
//set global application listeners
if (config.application != null) {
L.d("[Countly] Calling registerActivityLifecycleCallbacks");
@@ -846,6 +845,63 @@ private void stopTimer() {
}
}
+ void onSdkConfigurationChanged(@NonNull CountlyConfig config) {
+ L.i("[Countly] onSdkConfigurationChanged");
+
+ if (config_ == null) {
+ L.e("[Countly] onSdkConfigurationChanged, config is null");
+ return;
+ }
+
+ setLoggingEnabled(config.loggingEnabled);
+
+ long timerDelay = TIMER_DELAY_IN_SECONDS;
+ if (config.sessionUpdateTimerDelay != null) {
+ timerDelay = config.sessionUpdateTimerDelay;
+ }
+
+ startTimerService(timerService_, timerFuture, timerDelay);
+
+ config.maxRequestQueueSize = Math.max(config.maxRequestQueueSize, 1);
+ countlyStore.setLimits(config.maxRequestQueueSize);
+
+ config.dropAgeHours = Math.max(config.dropAgeHours, 0);
+ if (config.dropAgeHours > 0) {
+ countlyStore.setRequestAgeLimit(config.dropAgeHours);
+ }
+
+ config.eventQueueSizeThreshold = Math.max(config.eventQueueSizeThreshold, 1);
+ EVENT_QUEUE_SIZE_THRESHOLD = config.eventQueueSizeThreshold;
+
+ // Have a look at the SDK limit values
+ if (config.sdkInternalLimits.maxKeyLength != null) {
+ config.sdkInternalLimits.maxKeyLength = Math.max(config.sdkInternalLimits.maxKeyLength, 1);
+ }
+
+ if (config.sdkInternalLimits.maxValueSize != null) {
+ config.sdkInternalLimits.maxValueSize = Math.max(config.sdkInternalLimits.maxValueSize, 1);
+ }
+
+ if (config.sdkInternalLimits.maxSegmentationValues != null) {
+ config.sdkInternalLimits.maxSegmentationValues = Math.max(config.sdkInternalLimits.maxSegmentationValues, 1);
+ }
+
+ if (config.sdkInternalLimits.maxBreadcrumbCount != null) {
+ config.sdkInternalLimits.maxBreadcrumbCount = Math.max(config.sdkInternalLimits.maxBreadcrumbCount, 1);
+ }
+
+ if (config.sdkInternalLimits.maxStackTraceLinesPerThread != null) {
+ config.sdkInternalLimits.maxStackTraceLinesPerThread = Math.max(config.sdkInternalLimits.maxStackTraceLinesPerThread, 1);
+ }
+ if (config.sdkInternalLimits.maxStackTraceLineLength != null) {
+ config.sdkInternalLimits.maxStackTraceLineLength = Math.max(config.sdkInternalLimits.maxStackTraceLineLength, 1);
+ }
+
+ for (ModuleBase module : modules) {
+ module.onSdkConfigurationChanged(config);
+ }
+ }
+
/**
* Immediately disables session and event tracking and clears any stored session and event data.
* Testing Purposes Only!
@@ -920,6 +976,7 @@ void onStartInternal(Activity activity) {
//begin a session
moduleSessions.beginSessionInternal();
+ moduleConfiguration.fetchIfTimeIsUpForFetchingServerConfig();
}
config_.deviceInfo.inForeground();
diff --git a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java
index 58a7ea526..8aedaefa0 100644
--- a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java
+++ b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java
@@ -79,7 +79,7 @@ public class CountlyConfig {
protected String appKey = null;
/**
- * unique ID for the device the app is running on; note that null in deviceID means that Countly will fall back to OpenUDID, then, if it's not available, to Google Advertising ID.
+ * unique ID for the device the app is running on; note that null in deviceID means that Countly will fall back to UUID.
*/
protected String deviceID = null;
@@ -197,12 +197,11 @@ public class CountlyConfig {
boolean explicitStorageModeEnabled = false;
- boolean serverConfigurationEnabled = false;
-
boolean healthCheckEnabled = true;
// Requests older than this value in hours would be dropped (0 means this feature is disabled)
int dropAgeHours = 0;
+ String sdkBehaviorSettings;
/**
* THIS VARIABLE SHOULD NOT BE USED
@@ -291,7 +290,7 @@ public synchronized CountlyConfig setAppKey(String appKey) {
}
/**
- * unique ID for the device the app is running on; note that null in deviceID means that Countly will fall back to OpenUDID, then, if it's not available, to Google Advertising ID.
+ * unique ID for the device the app is running on; note that null in deviceID means that Countly will fall back to UUID.
*
* @return Returns the same config object for convenient linking
*/
@@ -300,16 +299,6 @@ public synchronized CountlyConfig setDeviceId(String deviceID) {
return this;
}
- /**
- * enum value specifying which device ID generation strategy Countly should use: OpenUDID or Google Advertising ID.
- *
- * @return Returns the same config object for convenient linking
- * @deprecated this call should not be used anymore as it does not have any purpose anymore
- */
- public synchronized CountlyConfig setIdMode(DeviceIdType idMode) {
- return this;
- }
-
/**
* sets the limit after how many sessions, for each apps version, the automatic star rating dialog is shown.
*
@@ -1001,9 +990,9 @@ public synchronized CountlyConfig enableExplicitStorageMode() {
*
* @return Returns the same config object for convenient linking
* @apiNote This is an EXPERIMENTAL feature, and it can have breaking changes
+ * @deprecated and will do nothing
*/
public synchronized CountlyConfig enableServerConfiguration() {
- serverConfigurationEnabled = true;
return this;
}
@@ -1012,6 +1001,17 @@ protected synchronized CountlyConfig disableHealthCheck() {
return this;
}
+ /**
+ * Set the server configuration to be set while initializing the SDK
+ *
+ * @param sdkBehaviorSettings The server configuration to be set
+ * @return Returns the same config object for convenient linking
+ */
+ public synchronized CountlyConfig setSDKBehaviorSettings(String sdkBehaviorSettings) {
+ this.sdkBehaviorSettings = sdkBehaviorSettings;
+ return this;
+ }
+
/**
* APM configuration interface to be used with CountlyConfig
*/
diff --git a/sdk/src/main/java/ly/count/android/sdk/CountlyTimer.java b/sdk/src/main/java/ly/count/android/sdk/CountlyTimer.java
index bbb482a6e..b3713dd47 100644
--- a/sdk/src/main/java/ly/count/android/sdk/CountlyTimer.java
+++ b/sdk/src/main/java/ly/count/android/sdk/CountlyTimer.java
@@ -30,7 +30,18 @@ protected void stopTimer(@NonNull ModuleLog L) {
}
}
+ /**
+ * Start a timer with the given delay
+ *
+ * @param timerDelay in seconds
+ * @param runnable to run
+ * @param L logger
+ */
protected void startTimer(long timerDelay, @NonNull Runnable runnable, @NonNull ModuleLog L) {
+ startTimer(timerDelay, 0, runnable, L);
+ }
+
+ protected void startTimer(long timerDelay, long initialDelayMS, @NonNull Runnable runnable, @NonNull ModuleLog L) {
long timerDelayInternal = timerDelay * 1000;
if (timerDelayInternal < UtilsTime.ONE_SECOND_IN_MS) {
@@ -41,7 +52,7 @@ protected void startTimer(long timerDelay, @NonNull Runnable runnable, @NonNull
timerDelayInternal = TIMER_DELAY_MS;
}
- L.i("[CountlyTimer] startTimer, Starting timer timerDelay: [" + timerDelayInternal + " ms]");
+ L.i("[CountlyTimer] startTimer, Starting timer timerDelay: [" + timerDelayInternal + " ms], initialDelay: [" + initialDelayMS + " ms]");
if (timerService != null) {
L.d("[CountlyTimer] startTimer, timer was running, stopping it");
@@ -49,6 +60,6 @@ protected void startTimer(long timerDelay, @NonNull Runnable runnable, @NonNull
}
timerService = Executors.newSingleThreadScheduledExecutor();
- timerService.scheduleWithFixedDelay(runnable, 0, timerDelayInternal, TimeUnit.MILLISECONDS);
+ timerService.scheduleWithFixedDelay(runnable, initialDelayMS, timerDelayInternal, TimeUnit.MILLISECONDS);
}
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/CountlyWebViewClient.java b/sdk/src/main/java/ly/count/android/sdk/CountlyWebViewClient.java
index 4b123d85f..708aff8e7 100644
--- a/sdk/src/main/java/ly/count/android/sdk/CountlyWebViewClient.java
+++ b/sdk/src/main/java/ly/count/android/sdk/CountlyWebViewClient.java
@@ -38,7 +38,7 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request
return false;
}
- public void registerWebViewUrlListeners(List listener) {
- this.listeners.addAll(listener);
+ public void registerWebViewUrlListener(WebViewUrlListener listener) {
+ this.listeners.add(listener);
}
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/DeviceId.java b/sdk/src/main/java/ly/count/android/sdk/DeviceId.java
index af8600bbd..af4c55129 100644
--- a/sdk/src/main/java/ly/count/android/sdk/DeviceId.java
+++ b/sdk/src/main/java/ly/count/android/sdk/DeviceId.java
@@ -62,8 +62,8 @@ protected DeviceId(@Nullable String providedId, @NonNull StorageProvider givenSt
if (providedId == null) {
//if the provided ID is 'null' then that means that a new ID must be generated
- L.i("[DeviceId-int] Using OpenUDID");
- setAndStoreId(DeviceIdType.OPEN_UDID, openUDIDProvider.getOpenUDID());
+ L.i("[DeviceId-int] Using UUID");
+ setAndStoreId(DeviceIdType.OPEN_UDID, openUDIDProvider.getUUID());
} else if (providedId.equals(temporaryCountlyDeviceId)) {
L.i("[DeviceId-int] Entering temp ID mode");
@@ -106,8 +106,8 @@ protected String getCurrentId() {
assert type != null;
if (id == null && type == DeviceIdType.OPEN_UDID) {
- //using openUDID as a fallback
- id = openUDIDProvider.getOpenUDID();
+ //using UUID as a fallback
+ id = openUDIDProvider.getUUID();
}
return id;
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java b/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java
index 65ef158f4..518b8afde 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java
@@ -99,7 +99,7 @@ void consentWillChange(@NonNull List consentThatWillChange, final boolea
}
//notify the SDK modules that internal configuration was updated
- void sdkConfigurationChanged() {
+ void onSdkConfigurationChanged(@NonNull CountlyConfig config) {
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java b/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java
index 331d7d51c..ca7ff943d 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java
@@ -1,32 +1,61 @@
package ly.count.android.sdk;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
class ModuleConfiguration extends ModuleBase implements ConfigurationProvider {
ImmediateRequestGenerator immediateRequestGenerator;
-
- boolean serverConfigEnabled = false;
+ CountlyTimer serverConfigUpdateTimer;
JSONObject latestRetrievedConfigurationFull = null;
JSONObject latestRetrievedConfiguration = null;
//config keys
- final static String keyTracking = "tracking";
- final static String keyNetworking = "networking";
+ final static String keyRTracking = "tracking";
+ final static String keyRNetworking = "networking";
//request keys
final static String keyRTimestamp = "t";
final static String keyRVersion = "v";
final static String keyRConfig = "c";
-
- final static boolean defaultVTracking = true;
- final static boolean defaultVNetworking = true;
+ final static String keyRReqQueueSize = "rqs";
+ final static String keyREventQueueSize = "eqs";
+ final static String keyRLogging = "log";
+ final static String keyRSessionUpdateInterval = "sui";
+ final static String keyRSessionTracking = "st";
+ final static String keyRViewTracking = "vt";
+ final static String keyRLocationTracking = "lt";
+ final static String keyRRefreshContentZone = "rcz";
+
+ final static String keyRLimitKeyLength = "lkl";
+ final static String keyRLimitValueSize = "lvs";
+ final static String keyRLimitSegValues = "lsv";
+ final static String keyRLimitBreadcrumb = "lbc";
+ final static String keyRLimitTraceLine = "ltlpt";
+ final static String keyRLimitTraceLength = "ltl";
+ final static String keyRCustomEventTracking = "cet";
+ final static String keyREnterContentZone = "ecz";
+ final static String keyRContentZoneInterval = "czi";
+ final static String keyRConsentRequired = "cr";
+ final static String keyRDropOldRequestTime = "dort";
+ final static String keyRCrashReporting = "crt";
+ final static String keyRServerConfigUpdateInterval = "scui";
boolean currentVTracking = true;
boolean currentVNetworking = true;
- boolean configurationFetched = false;
+ boolean currentVSessionTracking = true;
+ boolean currentVViewTracking = true;
+ boolean currentVCustomEventTracking = true;
+ boolean currentVContentZone = false;
+ boolean currentVCrashReporting = true;
+ boolean currentVLocationTracking = true;
+ boolean currentVRefreshContentZone = true;
+ // in hours
+ Integer serverConfigUpdateInterval;
+ int currentServerConfigUpdateInterval = 4;
+ long lastServerConfigFetchTimestamp = -1;
ModuleConfiguration(@NonNull Countly cly, @NonNull CountlyConfig config) {
super(cly, config);
@@ -34,39 +63,60 @@ class ModuleConfiguration extends ModuleBase implements ConfigurationProvider {
config.configProvider = this;
configProvider = this;
- serverConfigEnabled = config.serverConfigurationEnabled;
-
immediateRequestGenerator = config.immediateRequestGenerator;
+ serverConfigUpdateTimer = new CountlyTimer();
+ serverConfigUpdateInterval = currentServerConfigUpdateInterval;
config.countlyStore.setConfigurationProvider(this);
- if (serverConfigEnabled) {
- //load the previously saved configuration
- loadConfigFromStorage();
+ //load the previously saved configuration
+ loadConfigFromStorage(config.sdkBehaviorSettings);
- //update the config variables according to the new state
- updateConfigVariables();
- }
+ //update the config variables according to the new state
+ updateConfigVariables(config);
}
@Override
void initFinished(@NonNull final CountlyConfig config) {
- if (serverConfigEnabled) {
- //once the SDK has loaded, init fetching the server config
- fetchConfigFromServer();
- }
+ //once the SDK has loaded, init fetching the server config
+ L.d("[ModuleConfiguration] initFinished");
+ fetchConfigFromServer(config);
+ startServerConfigUpdateTimer();
}
@Override
void halt() {
+ serverConfigUpdateTimer.stopTimer(L);
+ }
+ @Override
+ void onSdkConfigurationChanged(@NonNull CountlyConfig config) {
+ if (currentServerConfigUpdateInterval != serverConfigUpdateInterval) {
+ currentServerConfigUpdateInterval = serverConfigUpdateInterval;
+ startServerConfigUpdateTimer();
+ }
+ }
+
+ private void startServerConfigUpdateTimer() {
+ serverConfigUpdateTimer.startTimer((long) currentServerConfigUpdateInterval * 60 * 60 * 1000, (long) currentServerConfigUpdateInterval * 60 * 60, new Runnable() {
+ @Override
+ public void run() {
+ fetchConfigFromServer(_cly.config_);
+ }
+ }, L);
}
/**
* Reads from storage to local json objects
*/
- void loadConfigFromStorage() {
+ void loadConfigFromStorage(@Nullable String sdkBehaviorSettings) {
+
String sConfig = storageProvider.getServerConfig();
+
+ if (Utils.isNullOrEmpty(sConfig)) {
+ sConfig = sdkBehaviorSettings;
+ }
+
L.v("[ModuleConfiguration] loadConfigFromStorage, [" + sConfig + "]");
if (sConfig == null || sConfig.isEmpty()) {
@@ -86,38 +136,69 @@ void loadConfigFromStorage() {
}
}
+ private T extractValue(String key, StringBuilder sb, T currentValue, T defaultValue, Class clazz) {
+ if (latestRetrievedConfiguration.has(key)) {
+ try {
+ Object value = latestRetrievedConfiguration.get(key);
+ if (!value.equals(currentValue)) {
+ sb.append(key).append(":[").append(value).append("], ");
+ return clazz.cast(value);
+ }
+ } catch (Exception e) {
+ L.w("[ModuleConfiguration] updateConfigs, failed to load '" + key + "', " + e.getMessage());
+ }
+ }
+
+ if (currentValue == null) {
+ return defaultValue;
+ }
+
+ return currentValue;
+ }
+
//update the config variables according to the current config obj state
- void updateConfigVariables() {
+ private void updateConfigVariables(@NonNull final CountlyConfig clyConfig) {
L.v("[ModuleConfiguration] updateConfigVariables");
- //set all to defaults
- currentVNetworking = defaultVNetworking;
- currentVTracking = defaultVTracking;
-
if (latestRetrievedConfiguration == null) {
//no config, don't continue
return;
}
- //networking
- if (latestRetrievedConfiguration.has(keyNetworking)) {
- try {
- currentVNetworking = latestRetrievedConfiguration.getBoolean(keyNetworking);
- } catch (JSONException e) {
- L.w("[ModuleConfiguration] updateConfigs, failed to load 'networking', " + e);
- }
- }
-
- //tracking
- if (latestRetrievedConfiguration.has(keyTracking)) {
- try {
- currentVTracking = latestRetrievedConfiguration.getBoolean(keyTracking);
- } catch (JSONException e) {
- L.w("[ModuleConfiguration] updateConfigs, failed to load 'tracking', " + e);
- }
+ StringBuilder sb = new StringBuilder();
+
+ currentVNetworking = extractValue(keyRNetworking, sb, currentVNetworking, currentVNetworking, Boolean.class);
+ currentVTracking = extractValue(keyRTracking, sb, currentVTracking, currentVTracking, Boolean.class);
+ currentVSessionTracking = extractValue(keyRSessionTracking, sb, currentVSessionTracking, currentVSessionTracking, Boolean.class);
+ currentVCrashReporting = extractValue(keyRCrashReporting, sb, currentVCrashReporting, currentVCrashReporting, Boolean.class);
+ currentVViewTracking = extractValue(keyRViewTracking, sb, currentVViewTracking, currentVViewTracking, Boolean.class);
+ currentVCustomEventTracking = extractValue(keyRCustomEventTracking, sb, currentVCustomEventTracking, currentVCustomEventTracking, Boolean.class);
+ currentVLocationTracking = extractValue(keyRLocationTracking, sb, currentVLocationTracking, currentVLocationTracking, Boolean.class);
+ currentVContentZone = extractValue(keyREnterContentZone, sb, currentVContentZone, currentVContentZone, Boolean.class);
+ serverConfigUpdateInterval = extractValue(keyRServerConfigUpdateInterval, sb, serverConfigUpdateInterval, currentServerConfigUpdateInterval, Integer.class);
+ currentVRefreshContentZone = extractValue(keyRRefreshContentZone, sb, currentVRefreshContentZone, currentVRefreshContentZone, Boolean.class);
+
+ clyConfig.setMaxRequestQueueSize(extractValue(keyRReqQueueSize, sb, clyConfig.maxRequestQueueSize, clyConfig.maxRequestQueueSize, Integer.class));
+ clyConfig.setEventQueueSizeToSend(extractValue(keyREventQueueSize, sb, clyConfig.eventQueueSizeThreshold, Countly.sharedInstance().EVENT_QUEUE_SIZE_THRESHOLD, Integer.class));
+ clyConfig.setLoggingEnabled(extractValue(keyRLogging, sb, clyConfig.loggingEnabled, clyConfig.loggingEnabled, Boolean.class));
+ clyConfig.setUpdateSessionTimerDelay(extractValue(keyRSessionUpdateInterval, sb, clyConfig.sessionUpdateTimerDelay, Long.valueOf(Countly.TIMER_DELAY_IN_SECONDS).intValue(), Integer.class));
+ clyConfig.sdkInternalLimits.setMaxKeyLength(extractValue(keyRLimitKeyLength, sb, clyConfig.sdkInternalLimits.maxKeyLength, Countly.maxKeyLengthDefault, Integer.class));
+ clyConfig.sdkInternalLimits.setMaxValueSize(extractValue(keyRLimitValueSize, sb, clyConfig.sdkInternalLimits.maxValueSize, Countly.maxValueSizeDefault, Integer.class));
+ clyConfig.sdkInternalLimits.setMaxSegmentationValues(extractValue(keyRLimitSegValues, sb, clyConfig.sdkInternalLimits.maxSegmentationValues, Countly.maxSegmentationValuesDefault, Integer.class));
+ clyConfig.sdkInternalLimits.setMaxBreadcrumbCount(extractValue(keyRLimitBreadcrumb, sb, clyConfig.sdkInternalLimits.maxBreadcrumbCount, Countly.maxBreadcrumbCountDefault, Integer.class));
+ clyConfig.sdkInternalLimits.setMaxStackTraceLinesPerThread(extractValue(keyRLimitTraceLine, sb, clyConfig.sdkInternalLimits.maxStackTraceLinesPerThread, Countly.maxStackTraceLinesPerThreadDefault, Integer.class));
+ clyConfig.sdkInternalLimits.setMaxStackTraceLineLength(extractValue(keyRLimitTraceLength, sb, clyConfig.sdkInternalLimits.maxStackTraceLineLength, Countly.maxStackTraceLineLengthDefault, Integer.class));
+ clyConfig.content.setZoneTimerInterval(extractValue(keyRContentZoneInterval, sb, clyConfig.content.zoneTimerInterval, clyConfig.content.zoneTimerInterval, Integer.class));
+ clyConfig.setRequiresConsent(extractValue(keyRConsentRequired, sb, clyConfig.shouldRequireConsent, clyConfig.shouldRequireConsent, Boolean.class));
+ clyConfig.setRequestDropAgeHours(extractValue(keyRDropOldRequestTime, sb, clyConfig.dropAgeHours, clyConfig.dropAgeHours, Integer.class));
+
+ String updatedValues = sb.toString();
+ if (!updatedValues.isEmpty()) {
+ L.i("[ModuleConfiguration] updateConfigVariables, SDK configuration has changed, notifying the SDK, new values: [" + updatedValues + "]");
+ _cly.onSdkConfigurationChanged(clyConfig);
}
}
- void saveAndStoreDownloadedConfig(@NonNull JSONObject config) {
+ void saveAndStoreDownloadedConfig(@NonNull JSONObject config, @NonNull CountlyConfig clyConfig) {
L.v("[ModuleConfiguration] saveAndStoreDownloadedConfig");
if (!config.has(keyRVersion)) {
L.w("[ModuleConfiguration] saveAndStoreDownloadedConfig, Retrieved configuration does not has a 'version' field. Config will be ignored.");
@@ -136,7 +217,7 @@ void saveAndStoreDownloadedConfig(@NonNull JSONObject config) {
//at this point it is a valid response
latestRetrievedConfigurationFull = config;
- String configAsString = null;
+ String configAsString;
try {
latestRetrievedConfiguration = config.getJSONObject(keyRConfig);
@@ -153,13 +234,12 @@ void saveAndStoreDownloadedConfig(@NonNull JSONObject config) {
storageProvider.setServerConfig(configAsString);
//update config variables
- updateConfigVariables();
+ updateConfigVariables(clyConfig);
}
/**
* Perform network request for retrieving latest config
* If valid config is downloaded, save it, and update the values
- *
* Example response:
* {
* "v":1,
@@ -175,14 +255,9 @@ void saveAndStoreDownloadedConfig(@NonNull JSONObject config) {
* }
* }
*/
- void fetchConfigFromServer() {
+ void fetchConfigFromServer(@NonNull CountlyConfig config) {
L.v("[ModuleConfiguration] fetchConfigFromServer");
- if (!serverConfigEnabled) {
- L.d("[ModuleConfiguration] fetchConfigFromServer, fetch config from the server is aborted, server config is disabled");
- return;
- }
-
// why _cly? because module configuration is created before module device id, so we need to access it like this
// call order to module device id is after module configuration and device id provider is module device id
if (_cly.config_.deviceIdProvider.isTemporaryIdEnabled()) {
@@ -191,13 +266,7 @@ void fetchConfigFromServer() {
return;
}
- if (configurationFetched) {
- L.d("[ModuleConfiguration] fetchConfigFromServer, fetch config from the server is aborted, config already fetched");
- return;
- }
-
- configurationFetched = true;
-
+ lastServerConfigFetchTimestamp = UtilsTime.currentTimestampMs();
String requestData = requestQueueProvider.prepareServerConfigRequest();
ConnectionProcessor cp = requestQueueProvider.createConnectionProcessor();
@@ -207,28 +276,60 @@ void fetchConfigFromServer() {
return;
}
- L.d("[ModuleConfiguration] Retrieved configuration response: [" + checkResponse.toString() + "]");
+ L.d("[ModuleConfiguration] Retrieved configuration response: [" + checkResponse + "]");
- saveAndStoreDownloadedConfig(checkResponse);
+ saveAndStoreDownloadedConfig(checkResponse, config);
}, L);
}
+ void fetchIfTimeIsUpForFetchingServerConfig() {
+ if (lastServerConfigFetchTimestamp > 0) {
+ long currentTime = UtilsTime.currentTimestampMs();
+ long timePassed = currentTime - lastServerConfigFetchTimestamp;
+
+ if (timePassed > (long) currentServerConfigUpdateInterval * 60 * 60 * 1000) {
+ fetchConfigFromServer(_cly.config_);
+ }
+ }
+ }
+
// configuration getters
@Override
public boolean getNetworkingEnabled() {
- if (!serverConfigEnabled) {
- return defaultVNetworking;
- }
-
return currentVNetworking;
}
@Override
public boolean getTrackingEnabled() {
- if (!serverConfigEnabled) {
- return defaultVTracking;
- }
return currentVTracking;
}
+
+ @Override public boolean getSessionTrackingEnabled() {
+ return currentVSessionTracking;
+ }
+
+ @Override public boolean getViewTrackingEnabled() {
+ return currentVViewTracking;
+ }
+
+ @Override public boolean getCustomEventTrackingEnabled() {
+ return currentVCustomEventTracking;
+ }
+
+ @Override public boolean getContentZoneEnabled() {
+ return currentVContentZone;
+ }
+
+ @Override public boolean getCrashReportingEnabled() {
+ return currentVCrashReporting;
+ }
+
+ @Override public boolean getLocationTrackingEnabled() {
+ return currentVLocationTracking;
+ }
+
+ @Override public boolean getRefreshContentZoneEnabled() {
+ return currentVRefreshContentZone;
+ }
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleConsent.java b/sdk/src/main/java/ly/count/android/sdk/ModuleConsent.java
index 68f410463..c6793c253 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ModuleConsent.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ModuleConsent.java
@@ -286,6 +286,11 @@ void initFinished(@NonNull final CountlyConfig config) {
}
}
+ @Override
+ void onSdkConfigurationChanged(@NonNull CountlyConfig config) {
+ requiresConsent = config.shouldRequireConsent;
+ }
+
@Override
void onConsentChanged(@NonNull final List consentChangeDelta, final boolean newConsent, @NonNull final ModuleConsent.ConsentChangeSource changeSource) {
L.d("[ModuleConsent] onConsentChanged, consentChangeDelta: [" + consentChangeDelta + "], newConsent: [" + newConsent + "], changeSource: [" + changeSource + "]");
diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java
index ffbf7c673..a5b581ade 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java
@@ -8,9 +8,10 @@
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-import org.json.JSONArray;
+import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
public class ModuleContent extends ModuleBase {
@@ -18,21 +19,45 @@ public class ModuleContent extends ModuleBase {
Content contentInterface;
CountlyTimer countlyTimer;
private boolean shouldFetchContents = false;
- private final int contentUpdateInterval;
+ private boolean isCurrentlyInContentZone = false;
+ private int zoneTimerInterval;
private final ContentCallback globalContentCallback;
- static int waitForDelay = 0;
+ private int waitForDelay = 0;
+ int CONTENT_START_DELAY_MS = 4000; // 4 seconds
+ int REFRESH_CONTENT_ZONE_DELAY_MS = 2500; // 2.5 seconds
ModuleContent(@NonNull Countly cly, @NonNull CountlyConfig config) {
super(cly, config);
- L.v("[ModuleContent] Initialising");
+ L.v("[ModuleContent] Initialising, zoneTimerInterval: [" + config.content.zoneTimerInterval + "], globalContentCallback: [" + config.content.globalContentCallback + "]");
iRGenerator = config.immediateRequestGenerator;
contentInterface = new Content();
countlyTimer = new CountlyTimer();
- contentUpdateInterval = config.content.contentUpdateInterval;
+ zoneTimerInterval = config.content.zoneTimerInterval;
globalContentCallback = config.content.globalContentCallback;
}
+ @Override
+ void onSdkConfigurationChanged(@NonNull CountlyConfig config) {
+ zoneTimerInterval = config.content.zoneTimerInterval;
+ if (!configProvider.getContentZoneEnabled()) {
+ exitContentZoneInternal();
+ } else {
+ if (!shouldFetchContents) {
+ exitContentZoneInternal();
+ }
+ waitForDelay = 0;
+ enterContentZoneInternal(null, 0);
+ }
+ }
+
+ @Override
+ void initFinished(@NotNull CountlyConfig config) {
+ if (configProvider.getContentZoneEnabled()) {
+ enterContentZoneInternal(null, 0);
+ }
+ }
+
void fetchContentsInternal(@NonNull String[] categories) {
L.d("[ModuleContent] fetchContentsInternal, shouldFetchContents: [" + shouldFetchContents + "], categories: [" + Arrays.toString(categories) + "]");
@@ -65,6 +90,7 @@ void fetchContentsInternal(@NonNull String[] categories) {
_cly.context_.startActivity(intent);
shouldFetchContents = false; // disable fetching contents until the next time, this will disable the timer fetching
+ isCurrentlyInContentZone = true;
} else {
L.w("[ModuleContent] fetchContentsInternal, response is not valid, skipping");
}
@@ -74,38 +100,66 @@ void fetchContentsInternal(@NonNull String[] categories) {
}, L);
}
- void registerForContentUpdates(@Nullable String[] categories) {
+ private void enterContentZoneInternal(@Nullable String[] categories, final int initialDelayMS) {
+ if (!consentProvider.getConsent(Countly.CountlyFeatureNames.content)) {
+ L.w("[ModuleContent] enterContentZoneInternal, Consent is not granted, skipping");
+ return;
+ }
+
if (deviceIdProvider.isTemporaryIdEnabled()) {
- L.w("[ModuleContent] registerForContentUpdates, temporary device ID is enabled, skipping");
+ L.w("[ModuleContent] enterContentZoneInternal, temporary device ID is enabled, skipping");
return;
}
+ if (isCurrentlyInContentZone) {
+ L.w("[ModuleContent] enterContentZoneInternal, already in content zone, skipping");
+ return;
+ }
+
+ shouldFetchContents = true;
+
String[] validCategories;
if (categories == null) {
- L.w("[ModuleContent] registerForContentUpdates, categories is null, providing empty array");
+ L.w("[ModuleContent] enterContentZoneInternal, categories is null, providing empty array");
validCategories = new String[] {};
} else {
validCategories = categories;
}
- countlyTimer.startTimer(contentUpdateInterval, () -> {
- L.d("[ModuleContent] registerForContentUpdates, waitForDelay: [" + waitForDelay + "], shouldFetchContents: [" + shouldFetchContents + "], categories: [" + Arrays.toString(validCategories) + "]");
+ L.d("[ModuleContent] enterContentZoneInternal, categories: [" + Arrays.toString(validCategories) + "]");
- if (waitForDelay > 0) {
- waitForDelay--;
- return;
- }
+ int contentInitialDelay = initialDelayMS;
+ long sdkStartTime = UtilsTime.currentTimestampMs() - Countly.applicationStart;
+ if (sdkStartTime < CONTENT_START_DELAY_MS) {
+ contentInitialDelay += CONTENT_START_DELAY_MS;
+ }
- if (!shouldFetchContents) {
- L.w("[ModuleContent] registerForContentUpdates, shouldFetchContents is false, skipping");
- return;
- }
+ countlyTimer.startTimer(zoneTimerInterval, contentInitialDelay, new Runnable() {
+ @Override public void run() {
+ L.d("[ModuleContent] enterContentZoneInternal, waitForDelay: [" + waitForDelay + "], shouldFetchContents: [" + shouldFetchContents + "], categories: [" + Arrays.toString(validCategories) + "]");
+ if (waitForDelay > 0) {
+ waitForDelay--;
+ return;
+ }
- fetchContentsInternal(validCategories);
+ if (!shouldFetchContents) {
+ L.w("[ModuleContent] enterContentZoneInternal, shouldFetchContents is false, skipping");
+ return;
+ }
+
+ fetchContentsInternal(validCategories);
+ }
}, L);
}
+ void notifyAfterContentIsClosed() {
+ L.v("[ModuleContent] notifyAfterContentIsClosed, setting waitForDelay to 2 and shouldFetchContents to true");
+ waitForDelay = 2; // this is indicating that we will wait 1 min after closing the content and before fetching the next one
+ shouldFetchContents = true;
+ isCurrentlyInContentZone = false;
+ }
+
@NonNull
private String prepareContentFetchRequest(@NonNull DisplayMetrics displayMetrics, @NonNull String[] categories) {
Resources resources = _cly.context_.getResources();
@@ -121,27 +175,24 @@ private String prepareContentFetchRequest(@NonNull DisplayMetrics displayMetrics
int landscapeWidth = portrait ? scaledHeight : scaledWidth;
int landscapeHeight = portrait ? scaledWidth : scaledHeight;
- return requestQueueProvider.prepareFetchContents(portraitWidth, portraitHeight, landscapeWidth, landscapeHeight, categories);
+ String language = Locale.getDefault().getLanguage().toLowerCase();
+ String deviceType = deviceInfo.mp.getDeviceType(_cly.context_);
+
+ return requestQueueProvider.prepareFetchContents(portraitWidth, portraitHeight, landscapeWidth, landscapeHeight, categories, language, deviceType);
}
boolean validateResponse(@NonNull JSONObject response) {
- return response.has("geo");
- //boolean success = response.optString("result", "error").equals("success");
- //JSONArray content = response.optJSONArray("content");
- //return success && content != null && content.length() > 0;
+ return response.has("geo") && response.has("html");
}
@NonNull
Map parseContent(@NonNull JSONObject response, @NonNull DisplayMetrics displayMetrics) {
Map placementCoordinates = new ConcurrentHashMap<>();
- JSONArray contents = response.optJSONArray("content");
- //assert contents != null; TODO enable later
- JSONObject contentObj = response; //contents.optJSONObject(0); TODO this will be changed
- assert contentObj != null;
+ assert response != null;
- String content = contentObj.optString("html");
- JSONObject coordinates = contentObj.optJSONObject("geo");
+ String content = response.optString("html");
+ JSONObject coordinates = response.optJSONObject("geo");
assert coordinates != null;
placementCoordinates.put(Configuration.ORIENTATION_PORTRAIT, extractOrientationPlacements(coordinates, displayMetrics.density, "p", content));
@@ -162,7 +213,9 @@ private TransparentActivityConfig extractOrientationPlacements(@NonNull JSONObje
TransparentActivityConfig config = new TransparentActivityConfig((int) Math.ceil(x * density), (int) Math.ceil(y * density), (int) Math.ceil(w * density), (int) Math.ceil(h * density));
config.url = content;
- config.globalContentCallback = globalContentCallback;
+ // TODO, passing callback with an intent is impossible, need to find a way to pass it
+ // Currently, the callback is set as a static variable in TransparentActivity
+ TransparentActivity.globalContentCallback = globalContentCallback;
return config;
}
@@ -176,16 +229,11 @@ void halt() {
countlyTimer = null;
}
- private void optOutFromContent() {
- exitContentZoneInternal();
- shouldFetchContents = false;
- }
-
@Override
void onConsentChanged(@NonNull final List consentChangeDelta, final boolean newConsent, @NonNull final ModuleConsent.ConsentChangeSource changeSource) {
L.d("[ModuleContent] onConsentChanged, consentChangeDelta: [" + consentChangeDelta + "], newConsent: [" + newConsent + "], changeSource: [" + changeSource + "]");
if (consentChangeDelta.contains(Countly.CountlyFeatureNames.content) && !newConsent) {
- optOutFromContent();
+ exitContentZoneInternal();
}
}
@@ -193,48 +241,55 @@ void onConsentChanged(@NonNull final List consentChangeDelta, final bool
void deviceIdChanged(boolean withoutMerge) {
L.d("[ModuleContent] deviceIdChanged, withoutMerge: [" + withoutMerge + "]");
if (withoutMerge) {
- optOutFromContent();
+ exitContentZoneInternal();
}
}
- protected void exitContentZoneInternal() {
+ private void exitContentZoneInternal() {
shouldFetchContents = false;
countlyTimer.stopTimer(L);
+ waitForDelay = 0;
+ }
+
+ private void refreshContentZoneInternal() {
+ if (!configProvider.getRefreshContentZoneEnabled()) {
+ return;
+ }
+
+ if (isCurrentlyInContentZone) {
+ L.w("[ModuleContent] refreshContentZone, already in content zone, skipping");
+ return;
+ }
+
+ if (!shouldFetchContents) {
+ exitContentZoneInternal();
+ }
+
+ _cly.moduleRequestQueue.attemptToSendStoredRequestsInternal();
+
+ enterContentZoneInternal(null, REFRESH_CONTENT_ZONE_DELAY_MS);
}
public class Content {
/**
- * Opt in user for the content fetching and updates
- *
- * @param categories categories for the content
- * @apiNote This is an EXPERIMENTAL feature, and it can have breaking changes
+ * Enables content fetching and updates for the user.
+ * This method opts the user into receiving content updates
+ * and ensures that relevant data is fetched accordingly.
*/
- private void enterContentZone(@Nullable String... categories) {
- L.d("[ModuleContent] openForContent, categories: [" + Arrays.toString(categories) + "]");
-
+ public void enterContentZone() {
if (!consentProvider.getConsent(Countly.CountlyFeatureNames.content)) {
- L.w("[ModuleContent] openForContent, Consent is not granted, skipping");
+ L.w("[ModuleContent] enterContentZone, Consent is not granted, skipping");
return;
}
- shouldFetchContents = true;
- registerForContentUpdates(categories);
+ enterContentZoneInternal(null, 0);
}
/**
- * Opt in user for the content fetching and updates
- *
- * @apiNote This is an EXPERIMENTAL feature, and it can have breaking changes
- */
- public void enterContentZone() {
- enterContentZone(new String[] {});
- }
-
- /**
- * Opt out user from the content fetching and updates
- *
- * @apiNote This is an EXPERIMENTAL feature, and it can have breaking changes
+ * Disables content fetching and updates for the user.
+ * This method opts the user out of receiving content updates
+ * and stops any ongoing content retrieval processes.
*/
public void exitContentZone() {
if (!consentProvider.getConsent(Countly.CountlyFeatureNames.content)) {
@@ -246,20 +301,17 @@ public void exitContentZone() {
}
/**
- * Change the content that is being shown
- *
- * @param categories categories for the content
- * @apiNote This is an EXPERIMENTAL feature, and it can have breaking changes
+ * Triggers a manual refresh of the content zone.
+ * This method forces an update by fetching the latest content,
+ * ensuring the user receives the most up-to-date information.
*/
- private void changeContent(@Nullable String... categories) {
- L.d("[ModuleContent] changeContent, categories: [" + Arrays.toString(categories) + "]");
-
+ public void refreshContentZone() {
if (!consentProvider.getConsent(Countly.CountlyFeatureNames.content)) {
- L.w("[ModuleContent] changeContent, Consent is not granted, skipping");
+ L.w("[ModuleContent] refreshContentZone, Consent is not granted, skipping");
return;
}
- registerForContentUpdates(categories);
+ refreshContentZoneInternal();
}
}
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleCrash.java b/sdk/src/main/java/ly/count/android/sdk/ModuleCrash.java
index 8985e33fa..cdf5c85be 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ModuleCrash.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ModuleCrash.java
@@ -305,6 +305,11 @@ Countly recordExceptionInternal(@Nullable final Throwable exception, final boole
return _cly;
}
+ if (!configProvider.getCrashReportingEnabled()) {
+ L.d("[ModuleCrash] recordExceptionInternal, Crash reporting is disabled in the server configuration");
+ return _cly;
+ }
+
if (exception == null) {
L.d("[ModuleCrash] recordException, provided exception was null, returning");
return _cly;
@@ -337,7 +342,17 @@ Countly addBreadcrumbInternal(@Nullable String breadcrumb) {
@Override
void initFinished(@NonNull CountlyConfig config) {
+ if (!configProvider.getCrashReportingEnabled()) {
+ L.d("[ModuleCrash] initFinished, Crash reporting is disabled in the server configuration");
+ return;
+ }
+
//enable unhandled crash reporting
+ if (!configProvider.getCrashReportingEnabled()) {
+ L.w("[ModuleCrash] initFinished, Crash reporting is disabled in the server configuration");
+ return;
+ }
+
if (config.crashes.enableUnhandledCrashReporting) {
enableCrashReporting();
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleDeviceId.java b/sdk/src/main/java/ly/count/android/sdk/ModuleDeviceId.java
index d3b3e7013..6ecce4618 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ModuleDeviceId.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ModuleDeviceId.java
@@ -1,9 +1,7 @@
package ly.count.android.sdk;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
-import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.UUID;
@@ -83,7 +81,7 @@ void exitTemporaryIdMode(@NonNull String deviceId) {
deviceIdInstance.changeToCustomId(deviceId);
// trigger fetching if the temp id given on init
- _cly.moduleConfiguration.fetchConfigFromServer();
+ _cly.moduleConfiguration.fetchConfigFromServer(_cly.config_);
//update stored request for ID change to use this new ID
replaceTempIDWithRealIDinRQ(deviceId);
@@ -252,33 +250,26 @@ void halt() {
}
- public final static String PREF_KEY = "openudid";
+ public final static String PREF_KEY = "openudid"; // key of old impl, keeping because needs migration
public final static String PREFS_NAME = "openudid_prefs";
- @SuppressLint("HardwareIds")
- @Override @NonNull public String getOpenUDID() {
+ @Override @NonNull public String getUUID() {
String retrievedID;
SharedPreferences mPreferences = _cly.context_.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
- //Try to get the openudid from local preferences
+ //Try to get the stored UUID from local preferences
retrievedID = mPreferences.getString(PREF_KEY, null);
if (retrievedID == null) //Not found if temp storage
{
- Countly.sharedInstance().L.d("[OpenUDID] Generating openUDID");
- //Try to get the ANDROID_ID
- retrievedID = Settings.Secure.getString(_cly.context_.getContentResolver(), Settings.Secure.ANDROID_ID);
- if (retrievedID == null || retrievedID.equals("9774d56d682e549c") || retrievedID.length() < 15) {
- //if ANDROID_ID is null, or it's equals to the GalaxyTab generic ANDROID_ID or is too short bad, generates a new one
- //the new one would be random
- retrievedID = UUID.randomUUID().toString();
- }
+ Countly.sharedInstance().L.d("[ModuleDeviceId] getUUID, Generating UUID");
+ retrievedID = UUID.randomUUID().toString();
final SharedPreferences.Editor e = mPreferences.edit();
e.putString(PREF_KEY, retrievedID);
e.apply();
}
- Countly.sharedInstance().L.d("[OpenUDID] ID: " + retrievedID);
+ Countly.sharedInstance().L.d("[ModuleDeviceId] getUUID, retrievedID:[" + retrievedID + "]");
return retrievedID;
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleEvents.java b/sdk/src/main/java/ly/count/android/sdk/ModuleEvents.java
index 0261b1761..c0b73246d 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ModuleEvents.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ModuleEvents.java
@@ -225,6 +225,10 @@ public void recordEventInternal(@Nullable final String key, @Nullable Map consentChangeDelta, final boolean newConsent, @NonNull final ModuleConsent.ConsentChangeSource changeSource) {
if (consentChangeDelta.contains(Countly.CountlyFeatureNames.location)) {
diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java b/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java
index 414398324..6af16f3a1 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java
@@ -27,6 +27,7 @@ public class ModuleRatings extends ModuleBase {
//star rating
StarRatingCallback starRatingCallback_;// saved callback that is used for automatic star rating
boolean showStarRatingDialogOnFirstActivity = false;
+ ImmediateRequestGenerator iRGenerator;
final Ratings ratingsInterface;
@@ -34,6 +35,7 @@ public class ModuleRatings extends ModuleBase {
super(cly, config);
L.v("[ModuleRatings] Initialising");
+ iRGenerator = config.immediateRequestGenerator;
starRatingCallback_ = config.starRatingCallback;
setStarRatingInitConfig(config.starRatingSessionLimit, config.starRatingTextTitle, config.starRatingTextMessage, config.starRatingTextDismiss);
setIfRatingDialogIsCancellableInternal(config.starRatingDialogIsCancellable);
@@ -480,7 +482,7 @@ synchronized void showFeedbackPopupInternal(@Nullable final String widgetId, @Nu
ConnectionProcessor cp = requestQueueProvider.createConnectionProcessor();
final boolean networkingIsEnabled = cp.configProvider_.getNetworkingEnabled();
- (new ImmediateRequestMaker()).doWork(requestData, "/o/feedback/widget", cp, false, networkingIsEnabled, new ImmediateRequestMaker.InternalImmediateRequestCallback() {
+ iRGenerator.CreateImmediateRequestMaker().doWork(requestData, "/o/feedback/widget", cp, false, networkingIsEnabled, new ImmediateRequestMaker.InternalImmediateRequestCallback() {
@Override
public void callback(JSONObject checkResponse) {
if (checkResponse == null) {
diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleSessions.java b/sdk/src/main/java/ly/count/android/sdk/ModuleSessions.java
index 0219e4c1b..24ddb01f4 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ModuleSessions.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ModuleSessions.java
@@ -46,6 +46,10 @@ void beginSessionInternal() {
return;
}
+ if (!configProvider.getSessionTrackingEnabled()) {
+ return;
+ }
+
if (sessionIsRunning()) {
L.w("[ModuleSessions] A session is already running, this 'beginSessionInternal' will be ignored");
healthTracker.logSessionStartedWhileRunning();
@@ -70,6 +74,10 @@ void updateSessionInternal() {
return;
}
+ if (!configProvider.getSessionTrackingEnabled()) {
+ return;
+ }
+
if (!sessionIsRunning()) {
L.w("[ModuleSessions] No session is running, this 'updateSessionInternal' will be ignored");
healthTracker.logSessionUpdatedWhileNotRunning();
@@ -88,6 +96,10 @@ void endSessionInternal(boolean checkConsent) {
return;
}
+ if (!configProvider.getSessionTrackingEnabled()) {
+ return;
+ }
+
if (!sessionIsRunning()) {
L.w("[ModuleSessions] No session is running, this 'endSessionInternal' will be ignored");
healthTracker.logSessionEndedWhileNotRunning();
diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleViews.java b/sdk/src/main/java/ly/count/android/sdk/ModuleViews.java
index 4ca8cb331..05da30220 100644
--- a/sdk/src/main/java/ly/count/android/sdk/ModuleViews.java
+++ b/sdk/src/main/java/ly/count/android/sdk/ModuleViews.java
@@ -18,9 +18,7 @@ public class ModuleViews extends ModuleBase implements ViewIdProvider {
String previousViewName = "";
String currentViewName = "";
-
private boolean firstView = true;
-
boolean autoViewTracker = false;
boolean automaticTrackingShouldUseShortName = false;
@@ -150,12 +148,12 @@ Map CreateViewEventSegmentation(@NonNull ViewData vd, boolean fi
void autoCloseRequiredViews(boolean closeAllViews, @Nullable Map customViewSegmentation) {
L.d("[ModuleViews] autoCloseRequiredViews");
- List viewsToRemove = new ArrayList<>(1);
+ List viewsToRemove = new ArrayList<>(1);
for (Map.Entry entry : viewDataMap.entrySet()) {
ViewData vd = entry.getValue();
- if (closeAllViews || vd.isAutoStoppedView) {
- viewsToRemove.add(vd.viewID);
+ if (closeAllViews || (!vd.willStartAgain && vd.isAutoStoppedView)) {
+ viewsToRemove.add(vd);
}
}
@@ -164,7 +162,13 @@ void autoCloseRequiredViews(boolean closeAllViews, @Nullable Map
}
for (int a = 0; a < viewsToRemove.size(); a++) {
- stopViewWithIDInternal(viewsToRemove.get(a), customViewSegmentation);
+ ViewData vd = viewsToRemove.get(a);
+ if (!vd.willStartAgain) {
+ stopViewWithIDInternal(vd.viewID, customViewSegmentation);
+ } else if (closeAllViews) {
+ //if we are closing all views, we should remove the view from the cache
+ viewDataMap.remove(vd.viewID);
+ }
}
}
@@ -184,6 +188,11 @@ void autoCloseRequiredViews(boolean closeAllViews, @Nullable Map
return null;
}
+ if (!configProvider.getViewTrackingEnabled()) {
+ L.d("[ModuleViews] startViewInternal, View tracking is disabled, ignoring call, view will not be started view name:[" + viewName + "]");
+ return null;
+ }
+
if (viewName == null || viewName.isEmpty()) {
L.e("[ModuleViews] startViewInternal, Trying to record view with null or empty view name, ignoring request");
return null;
@@ -217,9 +226,11 @@ void autoCloseRequiredViews(boolean closeAllViews, @Nullable Map
applyLimitsToViewSegmentation(customViewSegmentation, "startViewInternal", accumulatedEventSegm);
- Map viewSegmentation = CreateViewEventSegmentation(currentViewData, firstView, true, accumulatedEventSegm);
+ boolean firstViewInSession = firstView && _cly.moduleSessions.sessionIsRunning();
- if (firstView) {
+ Map viewSegmentation = CreateViewEventSegmentation(currentViewData, firstViewInSession, true, accumulatedEventSegm);
+
+ if (firstViewInSession) {
L.d("[ModuleViews] Recording view as the first one in the session. [" + viewName + "]");
firstView = false;
}
@@ -276,6 +287,12 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map 0) {
diff --git a/sdk/src/main/java/ly/count/android/sdk/OpenUDIDProvider.java b/sdk/src/main/java/ly/count/android/sdk/OpenUDIDProvider.java
index 04806bcb8..cb45d7211 100644
--- a/sdk/src/main/java/ly/count/android/sdk/OpenUDIDProvider.java
+++ b/sdk/src/main/java/ly/count/android/sdk/OpenUDIDProvider.java
@@ -1,5 +1,5 @@
package ly.count.android.sdk;
interface OpenUDIDProvider {
- String getOpenUDID();
+ String getUUID();
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java b/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java
index 25407bcaa..50cebbee9 100644
--- a/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java
+++ b/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java
@@ -70,5 +70,5 @@ interface RequestQueueProvider {
String prepareHealthCheckRequest(String preparedMetrics);
- String prepareFetchContents(int portraitWidth, int portraitHeight, int landscapeWidth, int landscapeHeight, String[] categories);
+ String prepareFetchContents(int portraitWidth, int portraitHeight, int landscapeWidth, int landscapeHeight, String[] categories, String language, String deviceType);
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java b/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java
index 4dcb87591..e3995d0a6 100644
--- a/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java
+++ b/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java
@@ -12,6 +12,7 @@
import android.util.Log;
import android.view.Display;
import android.view.Gravity;
+import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.webkit.WebSettings;
@@ -35,10 +36,15 @@ public class TransparentActivity extends Activity {
TransparentActivityConfig configPortrait = null;
WebView webView;
RelativeLayout relativeLayout;
+ static ContentCallback globalContentCallback;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(Countly.TAG, "[TransparentActivity] onCreate, content received, showing it");
+
+ // there is a stripe at the top of the screen for contents
+ // we eliminate it with hiding the system ui
+ hideSystemUI();
super.onCreate(savedInstanceState);
overridePendingTransition(0, 0);
@@ -60,37 +66,29 @@ protected void onCreate(Bundle savedInstanceState) {
config = setupConfig(config);
- int width = config.width;
- int height = config.height;
-
- configLandscape.listeners.add((url, webView) -> {
- if (url.startsWith(URL_START)) {
- return contentUrlAction(url, configLandscape, webView);
- }
- return false;
- });
-
- configPortrait.listeners.add((url, webView) -> {
- if (url.startsWith(URL_START)) {
- return contentUrlAction(url, configPortrait, webView);
+ WebViewUrlListener listener = new WebViewUrlListener() {
+ @Override public boolean onUrl(String url, WebView webView) {
+ if (url.startsWith(URL_START)) {
+ return contentUrlAction(url, webView);
+ }
+ return false;
}
- return false;
- });
+ };
// Configure window layout parameters
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.gravity = Gravity.TOP | Gravity.LEFT; // try out START
params.x = config.x;
params.y = config.y;
- params.height = height;
- params.width = width;
+ params.height = config.height;
+ params.width = config.width;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
getWindow().setAttributes(params);
getWindow().setBackgroundDrawableResource(android.R.color.transparent);
// Create and configure the layout
relativeLayout = new RelativeLayout(this);
- RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(config.width, config.height);
relativeLayout.setLayoutParams(layoutParams);
webView = createWebView(config);
@@ -99,6 +97,18 @@ protected void onCreate(Bundle savedInstanceState) {
setContentView(relativeLayout);
}
+ private void hideSystemUI() {
+ // Enables regular immersive mode
+ getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ );
+ }
+
private TransparentActivityConfig setupConfig(@Nullable TransparentActivityConfig config) {
final WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
final Display display = wm.getDefaultDisplay();
@@ -125,8 +135,8 @@ private TransparentActivityConfig setupConfig(@Nullable TransparentActivityConfi
return config;
}
- private void changeOrientation(TransparentActivityConfig config) {
- Log.d(Countly.TAG, "[TransparentActivity] changeOrientation, config x: [" + config.x + "] y: [" + config.y + "] width: [" + config.width + "] height: [" + config.height + "]");
+ private void resizeContent(TransparentActivityConfig config) {
+ Log.d(Countly.TAG, "[TransparentActivity] resizeContent, config x: [" + config.x + "] y: [" + config.y + "] width: [" + config.width + "] height: [" + config.height + "]");
WindowManager.LayoutParams params = getWindow().getAttributes();
params.x = config.x;
params.y = config.y;
@@ -149,26 +159,36 @@ private void changeOrientation(TransparentActivityConfig config) {
public void onConfigurationChanged(android.content.res.Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d(Countly.TAG, "[TransparentActivity] onConfigurationChanged orientation: [" + newConfig.orientation + "], currentOrientation: [" + currentOrientation + "]");
- Log.v(Countly.TAG, "[TransparentActivity] onConfigurationChanged, Landscape: [" + Configuration.ORIENTATION_LANDSCAPE + "] Portrait: [" + Configuration.ORIENTATION_PORTRAIT + "]");
+
if (currentOrientation != newConfig.orientation) {
currentOrientation = newConfig.orientation;
- Log.i(Countly.TAG, "[TransparentActivity] onConfigurationChanged, orientation changed to currentOrientation: [" + currentOrientation + "]");
- changeOrientationInternal();
}
+
+ // CHANGE SCREEN SIZE
+ final WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
+ final Display display = wm.getDefaultDisplay();
+ final DisplayMetrics metrics = new DisplayMetrics();
+ display.getMetrics(metrics);
+
+ int scaledWidth = (int) Math.ceil(metrics.widthPixels / metrics.density);
+ int scaledHeight = (int) Math.ceil(metrics.heightPixels / metrics.density);
+
+ // refactor in the future to use the resize_me action
+ webView.loadUrl("javascript:window.postMessage({type: 'resize', width: " + scaledWidth + ", height: " + scaledHeight + "}, '*');");
}
- private void changeOrientationInternal() {
+ private void resizeContentInternal() {
switch (currentOrientation) {
case Configuration.ORIENTATION_LANDSCAPE:
if (configLandscape != null) {
configLandscape = setupConfig(configLandscape);
- changeOrientation(configLandscape);
+ resizeContent(configLandscape);
}
break;
case Configuration.ORIENTATION_PORTRAIT:
if (configPortrait != null) {
configPortrait = setupConfig(configPortrait);
- changeOrientation(configPortrait);
+ resizeContent(configPortrait);
}
break;
default:
@@ -176,7 +196,7 @@ private void changeOrientationInternal() {
}
}
- private boolean contentUrlAction(String url, TransparentActivityConfig config, WebView view) {
+ private boolean contentUrlAction(String url, WebView view) {
Log.d(Countly.TAG, "[TransparentActivity] contentUrlAction, url: [" + url + "]");
Map query = splitQuery(url);
Log.v(Countly.TAG, "[TransparentActivity] contentUrlAction, query: [" + query + "]");
@@ -189,7 +209,6 @@ private boolean contentUrlAction(String url, TransparentActivityConfig config, W
}
Object clyAction = query.get("action");
- boolean result = false;
if (clyAction instanceof String) {
Log.d(Countly.TAG, "[TransparentActivity] contentUrlAction, action string:[" + clyAction + "]");
String action = (String) clyAction;
@@ -211,15 +230,17 @@ private boolean contentUrlAction(String url, TransparentActivityConfig config, W
}
if (query.containsKey("close") && Objects.equals(query.get("close"), "1")) {
- if (config.globalContentCallback != null) { // TODO: verify this later
- config.globalContentCallback.onContentCallback(ContentStatus.CLOSED, query);
+ if (globalContentCallback != null) { // TODO: verify this later
+ globalContentCallback.onContentCallback(ContentStatus.CLOSED, query);
+ }
+
+ if (Countly.sharedInstance().isInitialized()) {
+ Countly.sharedInstance().moduleContent.notifyAfterContentIsClosed();
}
- ModuleContent.waitForDelay = 2; // this is indicating that we will wait 1 min after closing the content and before fetching the next one
finish();
- return true;
}
- return result;
+ return true;
}
private boolean linkAction(Map query, WebView view) {
@@ -272,7 +293,7 @@ private void resizeMeAction(Map query) {
configLandscape.width = (int) Math.ceil(landscape.getInt("w") * density);
configLandscape.height = (int) Math.ceil(landscape.getInt("h") * density);
- changeOrientationInternal();
+ resizeContentInternal();
} catch (JSONException e) {
Log.e(Countly.TAG, "[TransparentActivity] resizeMeAction, Failed to parse resize JSON", e);
}
@@ -288,15 +309,19 @@ private void eventAction(Map query) {
JSONObject eventJson = event.getJSONObject(i);
Log.v(Countly.TAG, "[TransparentActivity] eventAction, event JSON: [" + eventJson.toString() + "]");
- if (!eventJson.has("sg")) {
+ Map segmentation = new ConcurrentHashMap<>();
+ JSONObject sgJson = eventJson.optJSONObject("sg");
+ JSONObject segmentationJson = eventJson.optJSONObject("segmentation");
+
+ if (sgJson != null) {
+ segmentationJson = sgJson;
+ }
+
+ if (segmentationJson == null) {
Log.w(Countly.TAG, "[TransparentActivity] eventAction, event JSON is missing segmentation data event: [" + eventJson + "]");
continue;
}
- Map segmentation = new ConcurrentHashMap<>();
- JSONObject segmentationJson = eventJson.getJSONObject("sg");
- assert segmentationJson != null; // this is already checked above
-
for (int j = 0; j < segmentationJson.names().length(); j++) {
String key = segmentationJson.names().getString(j);
Object value = segmentationJson.get(key);
@@ -359,7 +384,14 @@ private WebView createWebView(TransparentActivityConfig config) {
webView.clearHistory();
CountlyWebViewClient client = new CountlyWebViewClient();
- client.registerWebViewUrlListeners(config.listeners);
+ client.registerWebViewUrlListener(new WebViewUrlListener() {
+ @Override public boolean onUrl(String url, WebView webView) {
+ if (url.startsWith(URL_START)) {
+ return contentUrlAction(url, webView);
+ }
+ return false;
+ }
+ });
webView.setWebViewClient(client);
webView.loadUrl(config.url);
diff --git a/sdk/src/main/java/ly/count/android/sdk/TransparentActivityConfig.java b/sdk/src/main/java/ly/count/android/sdk/TransparentActivityConfig.java
index cba8e52b4..5a764d66f 100644
--- a/sdk/src/main/java/ly/count/android/sdk/TransparentActivityConfig.java
+++ b/sdk/src/main/java/ly/count/android/sdk/TransparentActivityConfig.java
@@ -1,8 +1,6 @@
package ly.count.android.sdk;
import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.List;
class TransparentActivityConfig implements Serializable {
Integer x;
@@ -10,15 +8,11 @@ class TransparentActivityConfig implements Serializable {
Integer width;
Integer height;
String url;
- List listeners;
- ContentCallback globalContentCallback;
TransparentActivityConfig(Integer x, Integer y, Integer width, Integer height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
- this.listeners = new ArrayList<>();
- this.globalContentCallback = null;
}
}
\ No newline at end of file
diff --git a/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyConfigPush.java b/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyConfigPush.java
index defbed790..29134f2fb 100644
--- a/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyConfigPush.java
+++ b/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyConfigPush.java
@@ -13,6 +13,8 @@ public class CountlyConfigPush {
Set allowedIntentClassNames = new HashSet<>();
Set allowedIntentPackageNames = new HashSet<>();
+ CountlyNotificationButtonURLHandler notificationButtonURLHandler;
+
/**
* @param application
* @param mode
@@ -59,4 +61,15 @@ public synchronized CountlyConfigPush setAllowedIntentPackageNames(@NonNull List
this.allowedIntentPackageNames = new HashSet<>(allowedIntentPackageNames);
return this;
}
+
+ /**
+ * set notification button URL handler
+ *
+ * @param notificationButtonURLHandler for handling notification button click
+ * @return Returns the same push config object for convenient linking
+ */
+ public synchronized CountlyConfigPush setNotificationButtonURLHandler(CountlyNotificationButtonURLHandler notificationButtonURLHandler) {
+ this.notificationButtonURLHandler = notificationButtonURLHandler;
+ return this;
+ }
}
diff --git a/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyNotificationButtonURLHandler.java b/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyNotificationButtonURLHandler.java
new file mode 100644
index 000000000..d476de66d
--- /dev/null
+++ b/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyNotificationButtonURLHandler.java
@@ -0,0 +1,11 @@
+package ly.count.android.sdk.messaging;
+
+public interface CountlyNotificationButtonURLHandler {
+ /**
+ * Called when a notification button is clicked.
+ *
+ * @param url The URL associated with the button.
+ * @return true if the URL was handled, false otherwise.
+ */
+ boolean onClick(String url);
+}
diff --git a/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyPush.java b/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyPush.java
index fa843aad4..65b3f994b 100644
--- a/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyPush.java
+++ b/sdk/src/main/java/ly/count/android/sdk/messaging/CountlyPush.java
@@ -417,7 +417,6 @@ public static Boolean displayNotification(@Nullable final Context context, @Null
Button button = msg.buttons().get(i);
pushActivityIntent = createPushActivityIntent(context, msg, notificationIntent, i + 1, allowedIntentClassNames, allowedIntentPackageNames);
-
builder.addAction(button.icon(), button.title(), PendingIntent.getActivity(context, msg.hashCode() + i + 1, pushActivityIntent, Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
@@ -439,11 +438,11 @@ public void call(Bitmap bitmap) {
.setBigContentTitle(msg.title())
.setSummaryText(msg.message()));
}
- manager.notify(msg.hashCode(), builder.build());
+ manager.notify(msg.id().hashCode(), builder.build());
}
}, 1);
} else {
- manager.notify(msg.hashCode(), builder.build());
+ manager.notify(msg.id().hashCode(), builder.build());
}
return Boolean.TRUE;
@@ -555,6 +554,11 @@ public void onClick(DialogInterface dialog, int which) {
msg.recordAction(activity, 0);
dialog.dismiss();
+ if (countlyConfigPush.notificationButtonURLHandler != null && countlyConfigPush.notificationButtonURLHandler.onClick(msg.link().toString())) {
+ Countly.sharedInstance().L.d("[CountlyPush, displayDialog] Link handled by custom URL handler, skipping default link opening.");
+ return;
+ }
+
try {
Intent i = new Intent(Intent.ACTION_VIEW, msg.link());
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -596,18 +600,25 @@ private static void addButtons(final Context context, final AlertDialog.Builder
DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+
+ boolean isPositiveButtonPressed = (which == DialogInterface.BUTTON_POSITIVE);
+ if (countlyConfigPush.notificationButtonURLHandler != null && countlyConfigPush.notificationButtonURLHandler.onClick(msg.buttons().get(isPositiveButtonPressed ? 1 : 0).link().toString())) {
+ Countly.sharedInstance().L.d("[CountlyPush, dialog button onClick] Link handled by custom URL handler, skipping default link opening.");
+ return;
+ }
+
try {
- msg.recordAction(context, which == DialogInterface.BUTTON_POSITIVE ? 2 : 1);
- Intent intent = new Intent(Intent.ACTION_VIEW, msg.buttons().get(which == DialogInterface.BUTTON_POSITIVE ? 1 : 0).link());
+ msg.recordAction(context, isPositiveButtonPressed ? 2 : 1);
+ Intent intent = new Intent(Intent.ACTION_VIEW, msg.buttons().get(isPositiveButtonPressed ? 1 : 0).link());
Bundle bundle = new Bundle();
bundle.putParcelable(EXTRA_MESSAGE, msg);
intent.putExtra(EXTRA_MESSAGE, bundle);
- intent.putExtra(EXTRA_ACTION_INDEX, which == DialogInterface.BUTTON_POSITIVE ? 2 : 1);
+ intent.putExtra(EXTRA_ACTION_INDEX, isPositiveButtonPressed ? 2 : 1);
context.startActivity(intent);
} catch (Exception ex) {
- Countly.sharedInstance().L.e("[CountlyPush, dialog button onClick] Encountered issue while clicking on button #[" + which + "] [" + ex.toString() + "]");
+ Countly.sharedInstance().L.e("[CountlyPush, dialog button onClick] Encountered issue while clicking on button #[" + which + "] [" + ex + "]");
}
- dialog.dismiss();
}
};
builder.setNeutralButton(msg.buttons().get(0).title(), listener);
diff --git a/sdk/src/main/java/ly/count/android/sdk/messaging/ModulePush.java b/sdk/src/main/java/ly/count/android/sdk/messaging/ModulePush.java
index 0112d62d1..690155bb3 100644
--- a/sdk/src/main/java/ly/count/android/sdk/messaging/ModulePush.java
+++ b/sdk/src/main/java/ly/count/android/sdk/messaging/ModulePush.java
@@ -241,12 +241,16 @@ public void recordAction(Context context, int buttonIndex) {
@Override
public int hashCode() {
- return id.hashCode();
+ int result = 17;
+ result = 31 * result + (id != null ? id.hashCode() : 0);
+ result = 31 * result + (title != null ? title.hashCode() : 0);
+ result = 31 * result + (message != null ? message.hashCode() : 0);
+ return result;
}
@Override
public int describeContents() {
- return id.hashCode();
+ return hashCode();
}
@Override