Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
6fdf2a4
feat: 26.1.3
arifBurakDemiray May 12, 2026
7574a09
Merge pull request #564 from Countly/26_1_3
arifBurakDemiray May 12, 2026
d521ace
app fix
turtledreams May 13, 2026
4a78040
feat: remove build config to inside if
arifBurakDemiray May 14, 2026
fef389a
Merge pull request #566 from Countly/app-fix
arifBurakDemiray May 14, 2026
c17e043
Native crash fix
turtledreams Jun 5, 2026
fb63476
Merge pull request #567 from Countly/native-crash-fix
arifBurakDemiray Jun 8, 2026
63d5594
feat: automatic sbs settings and jtv
arifBurakDemiray Jun 10, 2026
ad598c1
Merge pull request #568 from Countly/sbs-automatic-tracking-jtv
turtledreams Jun 10, 2026
0ef457e
fix: temp id leak on ratings
arifBurakDemiray Jun 23, 2026
8a85584
feat: test for re-fetch contents after temp id
arifBurakDemiray Jun 23, 2026
1bb7dbe
Merge pull request #569 from Countly/fix_temp_id
arifBurakDemiray Jun 23, 2026
c6b91ad
feat: a config switch to disable all webview based UI
arifBurakDemiray Jun 23, 2026
5755804
feat: more checks
arifBurakDemiray Jun 23, 2026
6c5e047
feat: rename config
arifBurakDemiray Jun 24, 2026
2c73010
feat: config for disabling logging in production builds
arifBurakDemiray Jun 25, 2026
30053ed
Merge pull request #571 from Countly/disable-sdk-logging-in-production
arifBurakDemiray Jun 25, 2026
35f2303
Merge branch 'staging' into disable_webview_ui
arifBurakDemiray Jun 29, 2026
d8402c0
feat: pn and content security
arifBurakDemiray Jun 29, 2026
d17dbc6
Merge branch 'staging' into pn_security
arifBurakDemiray Jun 29, 2026
e198907
fix: changes after review
arifBurakDemiray Jun 29, 2026
c8f77cf
fix: ratings too
arifBurakDemiray Jun 30, 2026
c8f340d
feat: more tests
arifBurakDemiray Jun 30, 2026
20b53ac
Merge pull request #570 from Countly/disable_webview_ui
arifBurakDemiray Jun 30, 2026
b519550
Merge branch 'staging' into pn_security
arifBurakDemiray Jun 30, 2026
82c093a
fix: null check
arifBurakDemiray Jun 30, 2026
1b343e1
fix: add null check
arifBurakDemiray Jun 30, 2026
8356fa8
feat: last test additions
arifBurakDemiray Jun 30, 2026
9b1d810
fix: HealthCheck NPE
Jun 30, 2026
9c066fc
feat: final review changes
arifBurakDemiray Jul 1, 2026
0d45fbf
fix: curly brackets tabbing
arifBurakDemiray Jul 1, 2026
f3cefdb
Update CHANGELOG.md
arifBurakDemiray Jul 1, 2026
ffe22e6
feat: more tests
arifBurakDemiray Jul 1, 2026
19f9a50
Merge pull request #574 from dunkpi/health_counter_npe_fix
arifBurakDemiray Jul 1, 2026
d94395e
Merge branch 'staging' into pn_security
arifBurakDemiray Jul 1, 2026
46e2b57
chore: make changelog basic
arifBurakDemiray Jul 1, 2026
5f7a54a
Merge branch 'pn_security' of https://github.com/Countly/countly-sdk-…
arifBurakDemiray Jul 1, 2026
87ea0c1
Merge pull request #572 from Countly/pn_security
arifBurakDemiray Jul 1, 2026
ccb5ae5
feat: 26.1.4
arifBurakDemiray Jul 1, 2026
6c93af7
Merge pull request #575 from Countly/26_1_4
arifBurakDemiray Jul 1, 2026
6567274
feat: report app theme for UI
arifBurakDemiray Jul 2, 2026
a0f9936
refactor: improve link handling for contents and widgets
arifBurakDemiray Jul 2, 2026
f24f26c
feat: new tests
arifBurakDemiray Jul 2, 2026
77020ea
fix: + to space issue
arifBurakDemiray Jul 2, 2026
b1d8a5d
feat: content url handler
arifBurakDemiray Jul 13, 2026
fcd7570
feat: content url handler: changelog
arifBurakDemiray Jul 13, 2026
e272d0f
feat: custom ssl factory
arifBurakDemiray Jul 13, 2026
67729a0
Merge pull request #579 from Countly/content_url_handler
arifBurakDemiray Jul 13, 2026
fb63820
Merge branch 'staging' into custom-ssl-socket-factory
arifBurakDemiray Jul 13, 2026
c550487
Merge branch 'staging' into improve_content_comm
arifBurakDemiray Jul 13, 2026
1a06f56
Merge branch 'staging' into journey_respect_app_theme
arifBurakDemiray Jul 13, 2026
ee6b79f
fix: event class check
arifBurakDemiray Jul 14, 2026
f47f0d3
feat: tests about urls
arifBurakDemiray Jul 14, 2026
a28ed1d
Merge pull request #578 from Countly/improve_content_comm
arifBurakDemiray Jul 14, 2026
a403b24
Merge branch 'staging' into journey_respect_app_theme
arifBurakDemiray Jul 14, 2026
7cf396d
Merge pull request #577 from Countly/journey_respect_app_theme
arifBurakDemiray Jul 14, 2026
6b6a010
Merge branch 'staging' into custom-ssl-socket-factory
arifBurakDemiray Jul 14, 2026
1be9e40
fix: nomodule entries break await resources in contents
arifBurakDemiray Jul 16, 2026
a0a6f0a
Merge pull request #581 from Countly/fix_nomodule_script_load_await
arifBurakDemiray Jul 16, 2026
ed1a387
Merge pull request #580 from Countly/custom-ssl-socket-factory
arifBurakDemiray Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
## XX.XX.XX
* Added a new configuration option `setCustomSSLSocketFactory(SSLSocketFactory)` to send the SDK's HTTPS requests through a custom SSLSocketFactory.
* Added support for reporting the app's current theme (light or dark) when presenting feedback widgets, rating widgets, and content, so they are displayed in matching conditions.
* Improved link handling for content and feedback widgets, so links that carry their own query parameters, such as deep links, are parsed correctly.
* Added a content configuration option to provide a handler for links opened from the content web view, so the app can route its own deep links instead of the SDK opening the system browser, set via `setContentUrlHandler(ContentUrlHandler)`.

* Mitigated an issue where content could fail to be displayed on some devices, as the content web view could stay hidden even after its resources had finished loading.

## 26.1.4
* ! Minor breaking change ! Deprecated the static field "CountlyPush.useAdditionalIntentRedirectionChecks". It is now a no-op; use "CountlyConfigPush.enableAdditionalIntentRedirectionChecks()" instead, otherwise the stricter push intent redirection checks stay disabled.

* Added support for SDK behavior settings that control the SDK's automatic session tracking, automatic view tracking, automatic crash reporting, and Journey Trigger Views.
* Added a new push configuration option "enableAdditionalIntentRedirectionChecks()" to enable stricter validation of the notification intent's target package and class.
* Added a new content configuration option "setAllowedIntentSchemes(List)" to restrict which URI schemes content and feedback widget links may open.
* Added a new push configuration option "setAllowedIntentSchemes(List)" to restrict which URI schemes notification links may open.
* Added a new configuration option "disableWebView()" to disable all WebView-based UI in the SDK.
* Added a new config option "disableSDKLoggingInProduction()" that keeps the SDK's console logging disabled in production (non-debuggable) builds, even when logging is enabled.
* Improved the security of the content, feedback widget, rating widget and push notifications.

* Mitigated an issue where a native crash dump was truncated by the stack trace line length limit when a global crash filter was set.
* Mitigated an issue where the rating feedback popup request could be sent while in temporary device ID mode, creating a `CLYTemporaryDeviceID` user on the server.
* Mitigated an issue where the content zone did not resume after exiting temporary device ID mode even when it was enabled by the server configuration.
* Mitigated an issue while sending health checks after SDK is halted.

## 26.1.3
* Added gradle configuration cache support to upload symbols plugin.
* Improved user properties auto-save conditions to flush event queue with every user property call.

Expand Down
17 changes: 6 additions & 11 deletions app/src/main/java/ly/count/android/demo/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,13 @@ public void onCreate() {
WebView.setWebContentsDebuggingEnabled(true);
}

COUNTLY_SERVER_URL =
DEFAULT_URL.equals(BuildConfig.COUNTLY_SERVER_URL)
? DEFAULT_URL
: BuildConfig.COUNTLY_SERVER_URL;
COUNTLY_APP_KEY =
DEFAULT_APP_KEY.equals(BuildConfig.COUNTLY_APP_KEY)
? DEFAULT_APP_KEY
: BuildConfig.COUNTLY_APP_KEY;

if (DEFAULT_URL.equals(COUNTLY_SERVER_URL) || DEFAULT_APP_KEY.equals(COUNTLY_APP_KEY)) {
Log.e("CountlyDemo", "Please provide correct COUNTLY_SERVER_URL and COUNTLY_APP_KEY");
return;
COUNTLY_SERVER_URL = BuildConfig.COUNTLY_SERVER_URL;
COUNTLY_APP_KEY = BuildConfig.COUNTLY_APP_KEY;
if (DEFAULT_URL.equals(COUNTLY_SERVER_URL) || DEFAULT_APP_KEY.equals(COUNTLY_APP_KEY)) {
Log.e("CountlyDemo", "Please provide correct COUNTLY_SERVER_URL and COUNTLY_APP_KEY");
return;
}
}

if (false) {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ org.gradle.configureondemand=true
android.useAndroidX=true
android.enableJetifier=true
# RELEASE FIELD SECTION
VERSION_NAME=26.1.2
VERSION_NAME=26.1.4
GROUP=ly.count.android
POM_URL=https://github.com/Countly/countly-sdk-android
POM_SCM_URL=https://github.com/Countly/countly-sdk-android
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ of this software and associated documentation files (the "Software"), to deal
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -43,6 +45,8 @@ of this software and associated documentation files (the "Software"), to deal
import static ly.count.android.sdk.UtilsNetworking.sha256Hash;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
Expand Down Expand Up @@ -103,6 +107,18 @@ public void setUp() {
return true;
}

@Override public boolean getAutomaticSessionTrackingEnabled() {
return true;
}

@Override public boolean getAutomaticViewTrackingEnabled() {
return true;
}

@Override public boolean getAutomaticCrashReportingEnabled() {
return true;
}

@Override public boolean getLocationTrackingEnabled() {
return true;
}
Expand Down Expand Up @@ -158,6 +174,10 @@ public void setUp() {
@Override public Set<String> getJourneyTriggerEvents() {
return Collections.emptySet();
}

@Override public Set<String> getJourneyTriggerViews() {
return Collections.emptySet();
}
};

Countly.sharedInstance().setLoggingEnabled(true);
Expand Down Expand Up @@ -276,6 +296,71 @@ public void urlConnectionCustomHeaderValues() throws IOException {
assertNull(urlConnection.getRequestProperty("33"));
}

/**
* A custom SSL socket factory is applied to an https server request, even when no
* certificate/public-key pinning is configured. This is the key behavior the old pin-gated
* code lacked: the factory used to be applied only when the pinning statics were set.
*/
@Test
public void urlConnectionForServerRequest_appliesCustomSSLSocketFactoryOnHttps() throws IOException {
SSLSocketFactory customFactory = mock(SSLSocketFactory.class);
ConnectionProcessor cp = new ConnectionProcessor("https://secureserver", mockStore, mockDeviceId, configurationProviderFake, rip, customFactory, null, moduleLog, healthTrackerMock, Mockito.mock(Runnable.class), new ConcurrentHashMap<>());

final URLConnection urlConnection = cp.urlConnectionForServerRequest("eventData", null);

assertTrue(urlConnection instanceof HttpsURLConnection);
assertSame(customFactory, ((HttpsURLConnection) urlConnection).getSSLSocketFactory());
assertEquals(30_000, urlConnection.getConnectTimeout());
assertFalse(urlConnection.getDoOutput());
}

/**
* The custom SSL socket factory is also applied to the preflight (HEAD) request path.
*/
@Test
public void urlConnectionForPreflightRequest_appliesCustomSSLSocketFactory() throws IOException {
SSLSocketFactory customFactory = mock(SSLSocketFactory.class);
ConnectionProcessor cp = new ConnectionProcessor("https://secureserver", mockStore, mockDeviceId, configurationProviderFake, rip, customFactory, null, moduleLog, healthTrackerMock, Mockito.mock(Runnable.class), new ConcurrentHashMap<>());

final HttpURLConnection conn = (HttpURLConnection) cp.urlConnectionForPreflightRequest("https://secureserver/o/sdk?method=fetch");

assertTrue(conn instanceof HttpsURLConnection);
assertSame(customFactory, ((HttpsURLConnection) conn).getSSLSocketFactory());
assertEquals("HEAD", conn.getRequestMethod());
}

/**
* A plain http server URL has no TLS layer, so the custom factory cannot be applied. The
* request must still be built without throwing.
*/
@Test
public void urlConnectionForServerRequest_customFactoryNotAppliedOnHttp() throws IOException {
SSLSocketFactory customFactory = mock(SSLSocketFactory.class);
ConnectionProcessor cp = new ConnectionProcessor("http://server", mockStore, mockDeviceId, configurationProviderFake, rip, customFactory, null, moduleLog, healthTrackerMock, Mockito.mock(Runnable.class), new ConcurrentHashMap<>());

final URLConnection urlConnection = cp.urlConnectionForServerRequest("eventData", null);

assertFalse(urlConnection instanceof HttpsURLConnection);
assertEquals("http", urlConnection.getURL().getProtocol());
}

/**
* With no custom factory (and no pinning), an https request falls back to the platform default
* socket factory, never to a Countly-injected one.
*/
@Test
public void urlConnectionForServerRequest_noFactoryUsesPlatformDefaultOnHttps() throws IOException {
SSLSocketFactory unusedFactory = mock(SSLSocketFactory.class);
ConnectionProcessor cp = new ConnectionProcessor("https://secureserver", mockStore, mockDeviceId, configurationProviderFake, rip, null, null, moduleLog, healthTrackerMock, Mockito.mock(Runnable.class), new ConcurrentHashMap<>());

final URLConnection urlConnection = cp.urlConnectionForServerRequest("eventData", null);

assertTrue(urlConnection instanceof HttpsURLConnection);
SSLSocketFactory used = ((HttpsURLConnection) urlConnection).getSSLSocketFactory();
assertNotNull(used);
assertNotSame(unusedFactory, used);
}

@Test
public void testRun_storeReturnsNullConnections() throws IOException {
connectionProcessor = spy(connectionProcessor);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package ly.count.android.sdk;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
Expand All @@ -27,6 +32,45 @@ public class ConnectionQueueIntegrationTests {
private final String appKey = "testAppKey123";
private final String serverUrl = "https://test.server.com";

// A valid X.509 certificate (Sectigo, *.count.ly) used only to exercise the pinning code path;
// CertificateFactory parses it regardless of expiry, so the pinning SSLContext can be built.
private static final String PINNING_CERT =
"MIIGnjCCBYagAwIBAgIRAN73cVA7Y1nD+S8rToAqBpQwDQYJKoZIhvcNAQELBQAwgY8xCzAJ"
+ "BgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1"
+ "NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UEAxMuU2VjdGln"
+ "byBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTAeFw0yMDA2MD"
+ "EwMDAwMDBaFw0yMjA5MDMwMDAwMDBaMBUxEzARBgNVBAMMCiouY291bnQubHkwggEi"
+ "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCl9zmATVRwrGRtRQJcmBmA+zc/ZL"
+ "io3YfkwXO2w8u9lnw60J4JpPNn9OnGcxdM+sqbXKU3jTdjY4j3yaA6NlWibq2jU2x6"
+ "HT2sS+I5gFFE/6tO53WqjoMk48i3FkyoJDittwtQrVaRGcP8RjJH0pfXaP+JLrLAgg"
+ "HuW3tCFqYzkWi3uLGVjQbSIRNiXsM3FI0UMEa/x1I3U4hLjMjH28KagZbZLWnHOvks"
+ "AvGLg3xQkS+GSQ+6ARZ2/bGh5O9q4hCCCk0/PpwAXmrOnWtwrNuwHcCDOvuB22JxLd"
+ "t8jQDYrjwtJIvq4Yut8FQPv/75SKoETWWHyxe0x5NsB34UwA/BAgMBAAGjggNsMIID"
+ "aDAfBgNVHSMEGDAWgBSNjF7EVK2K4Xfpm/mbBeG4AY1h4TAdBgNVHQ4EFgQU8uf/ND"
+ "Rt8cu+AwARVIGXPMfxGbQwDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYD"
+ "VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEkGA1UdIARCMEAwNAYLKwYBBAGyMQ"
+ "ECAgcwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EM"
+ "AQIBMIGEBggrBgEFBQcBAQR4MHYwTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jcnQuc2VjdG"
+ "lnby5jb20vU2VjdGlnb1JTQURvbWFpblZhbGlkYXRpb25TZWN1cmVTZXJ2ZXJDQS5j"
+ "cnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMB8GA1UdEQQYMB"
+ "aCCiouY291bnQubHmCCGNvdW50Lmx5MIIB9AYKKwYBBAHWeQIEAgSCAeQEggHgAd4A"
+ "dQBGpVXrdfqRIDC1oolp9PN9ESxBdL79SbiFq/L8cP5tRwAAAXJwTJ0kAAAEAwBGME"
+ "QCIEErTN/aGJ8LV9brGklKeGAXMg1EN/FUxXDu13kNfXhcAiBrKMYe+W4flPyuLNm5"
+ "jp6FJwtUTZPNpZ+TmM40dRdwjQB0AN+lXqtogk8fbK3uuF9OPlrqzaISpGpejjsSwC"
+ "BEXCpzAAABcnBMncsAAAQDAEUwQwIfEYSpsSDtKpmj9ZmRWsx73G622N74v09JDjzP"
+ "bkg9RQIgUelIqSwqu69JanH7losrqTTsjwNv+3QJBNJ6GxJKkh0AdgBvU3asMfAxGd"
+ "iZAKRRFf93FRwR2QLBACkGjbIImjfZEwAAAXJwTJ0YAAAEAwBHMEUCIQCMBaaQAoua"
+ "97R+z2zONMUq1XsDP5aoAiutZG4XxuQ6wAIgW1p6XS3az4CCqjwbDKxL9qEnw8fWd+"
+ "yLx2skviSsTS0AdwApeb7wnjk5IfBWc59jpXflvld9nGAK+PlNXSZcJV3HhAAAAXJw"
+ "TJ1PAAAEAwBIMEYCIQDg1YFbJPPKDIyrFZJ9rtrUklkh2k/wpgwjDuIp7tPtOgIhAL"
+ "dZl9s/qISsFm2E64ruYbdE4HKR1ZJ0zbIXOZcds7XXMA0GCSqGSIb3DQEBCwUAA4IB"
+ "AQB2Ar1h2X/S4qsVlw0gEbXO//6Rj8mTB4BFW6c5r84n0vTwvA78h003eX00y0ymxO"
+ "i5hkqB8gd1IUSWP1R1ijYtBVPdFi+SsMjUsB5NKquQNlWpo0GlFjRlcXnDC6R6toN2"
+ "QixJb47VM40Vmn2g0ZuMGfy1XoQKvIyRosT92jGm1YcF+nLEHBDr+89apZ8sUpFfWo"
+ "AnCom+8sBGwje6zP10eBbprHyzM8snvdwo/QNLAzLcvVNKP+Sr4H7HKzec3g1+THI0"
+ "M72TzoguJcOZQEI6Pd+FIP5Xad53rq4jCtRGwYrsieH49a3orBnkkJvUKni+mtkxMb"
+ "PTJ7eeMmX9g/0h";

@Before
public void setUp() {
Countly.sharedInstance().halt();
Expand Down Expand Up @@ -302,6 +346,93 @@ public void integration_sdkOverride_reflectedInCommonRequest() {
commonRequest.contains("sdk_version=" + customSdkVersion));
}

// ==========================================
// Integration Tests - Custom SSL socket factory
// ==========================================

/**
* Integration test: a custom SSLSocketFactory set on CountlyConfig is resolved by
* ConnectionQueue and applied to both the server request and the preflight request that every
* ConnectionProcessor produces.
*/
@Test
public void integration_customSSLSocketFactory_appliedToServerAndPreflightRequests() throws Exception {
SSLSocketFactory customFactory = mock(SSLSocketFactory.class);
CountlyConfig config = new CountlyConfig(TestUtils.getContext(), appKey, serverUrl)
.setCustomSSLSocketFactory(customFactory);
Countly.sharedInstance().init(config);
ConnectionQueue cq = Countly.sharedInstance().connectionQueue_;

URLConnection serverConn = cq.createConnectionProcessor().urlConnectionForServerRequest("app_key=" + appKey, null);
HttpURLConnection preflightConn = (HttpURLConnection) cq.createConnectionProcessor().urlConnectionForPreflightRequest(serverUrl + "/o/sdk?method=fetch");

Assert.assertTrue(serverConn instanceof HttpsURLConnection);
Assert.assertSame(customFactory, ((HttpsURLConnection) serverConn).getSSLSocketFactory());
Assert.assertTrue(preflightConn instanceof HttpsURLConnection);
Assert.assertSame(customFactory, ((HttpsURLConnection) preflightConn).getSSLSocketFactory());
}

/**
* Integration test: when both a custom SSLSocketFactory and public-key pinning are configured,
* the custom factory wins and the pinning certificates are never parsed (so intentionally
* invalid pinning certs do not break initialization).
*/
@Test
public void integration_customSSLSocketFactory_takesPrecedenceOverPinning() throws Exception {
SSLSocketFactory customFactory = mock(SSLSocketFactory.class);
try {
CountlyConfig config = new CountlyConfig(TestUtils.getContext(), appKey, serverUrl)
.enablePublicKeyPinning(new String[] { "not-a-real-certificate" })
.setCustomSSLSocketFactory(customFactory);
Countly.sharedInstance().init(config);
ConnectionQueue cq = Countly.sharedInstance().connectionQueue_;

URLConnection serverConn = cq.createConnectionProcessor().urlConnectionForServerRequest("app_key=" + appKey, null);

Assert.assertTrue(serverConn instanceof HttpsURLConnection);
Assert.assertSame("custom factory must win over pinning", customFactory, ((HttpsURLConnection) serverConn).getSSLSocketFactory());
} finally {
Countly.publicKeyPinCertificates = null;
}
}

/**
* Integration test: public-key pinning and certificate pinning both remain functional after the
* SSL socket factory refactor. Each installs its own (non-default) socket factory on the SDK's
* HTTPS connections, built from the CertificateTrustManager.
*/
@Test
public void integration_pinning_installsDistinctSocketFactory() throws Exception {
String[] certs = { PINNING_CERT };
SSLSocketFactory platformDefault = HttpsURLConnection.getDefaultSSLSocketFactory();
try {
// public key pinning
Countly.sharedInstance().init(new CountlyConfig(TestUtils.getContext(), appKey, serverUrl).enablePublicKeyPinning(certs));
SSLSocketFactory publicKeyPinningFactory = appliedServerRequestFactory();
Assert.assertNotNull(publicKeyPinningFactory);
Assert.assertNotSame("public key pinning must install its own socket factory", platformDefault, publicKeyPinningFactory);

Countly.sharedInstance().halt();
Countly.publicKeyPinCertificates = null;

// certificate pinning
Countly.sharedInstance().init(new CountlyConfig(TestUtils.getContext(), appKey, serverUrl).enableCertificatePinning(certs));
SSLSocketFactory certificatePinningFactory = appliedServerRequestFactory();
Assert.assertNotNull(certificatePinningFactory);
Assert.assertNotSame("certificate pinning must install its own socket factory", platformDefault, certificatePinningFactory);
} finally {
Countly.publicKeyPinCertificates = null;
Countly.certificatePinCertificates = null;
}
}

private SSLSocketFactory appliedServerRequestFactory() throws IOException {
ConnectionQueue cq = Countly.sharedInstance().connectionQueue_;
URLConnection conn = cq.createConnectionProcessor().urlConnectionForServerRequest("app_key=" + appKey, null);
Assert.assertTrue(conn instanceof HttpsURLConnection);
return ((HttpsURLConnection) conn).getSSLSocketFactory();
}

// ==========================================
// Integration Tests - Update Session
// ==========================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,4 +526,19 @@ public void testPrepareCommonRequest() {
}
}
}

/**
* The theme ("th") parameter is reported on the URLs loaded into the WebView (feedback/rating
* widget and content URLs), not on the feedback-list or content-fetch data requests. These
* requests must therefore never carry "th" regardless of the device theme. The actual "th"
* append logic is validated in UtilsDeviceTests, its wiring into content in ModuleContentTests.
*/
@Test
public void testThemeParam_notOnFeedbackListNorFetchContents() {
final String feedbackRequest = connQ.prepareFeedbackListRequest();
final String contentRequest = connQ.prepareFetchContents(100, 200, 200, 100, new String[] {}, "en", "mobile", null);

Assert.assertFalse(feedbackRequest.contains("th="));
Assert.assertFalse(contentRequest.contains("th="));
}
}
Loading
Loading