From 021951f1567ccf30f2d58d76b6cf39f587f4a40f Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Mon, 9 Feb 2026 17:18:35 -0300 Subject: [PATCH 1/6] Polishing --- http-domain/README.md | 99 ++++++++++++++++++- .../android/client/network/Algorithm.java | 2 +- .../android/client/network/Base64Decoder.java | 2 +- .../network/CertificateCheckerHelper.java | 2 +- .../client/network/DefaultBase64Decoder.java | 2 +- .../android/client/network/PinEncoder.java | 2 +- .../client/network/PinEncoderImpl.java | 2 +- http/README.md | 92 ++++++++++++++--- .../client/network/HttpClientImpl.java | 32 ++++++ .../client/network/HttpRequestImpl.java | 2 +- .../client/network/HttpStreamRequestImpl.java | 2 +- .../network/HttpStreamResponseImpl.java | 2 +- .../android/client/SplitFactoryImpl.java | 26 ++--- 13 files changed, 230 insertions(+), 37 deletions(-) diff --git a/http-domain/README.md b/http-domain/README.md index 362956fbb..bad8159a8 100644 --- a/http-domain/README.md +++ b/http-domain/README.md @@ -1,5 +1,100 @@ # HTTP Domain module -This module contains public HTTP configuration contracts exposed to consumers of the Split SDK. +Public contracts and configuration types for the HTTP client. +These types are exposed to SDK consumers through the `:main` module's `api` dependency. -Includes certificate pinning configuration, proxy settings, SSL configuration, and authenticator interfaces. +## `HttpClientConfiguration` + +Bundles all HTTP client settings into a single object: + +```java +HttpClientConfiguration config = HttpClientConfiguration.builder() + .connectionTimeout(15_000) + .readTimeout(15_000) + .proxy(proxy) + .proxyAuthenticator(authenticator) + .certificatePinningConfiguration(pinConfig) + .developmentSslConfig(devSsl) + .build(); +``` + +## Proxy configuration + +### Basic auth + +```java +HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080) + .basicAuth("user", "pass") + .build(); +``` + +### mTLS with custom CA + +```java +HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8443) + .proxyCacert(caCertInputStream) + .mtls(clientCertInputStream, clientKeyInputStream) + .build(); +``` + +### Custom credentials provider + +```java +// Bearer token +HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080) + .credentialsProvider(() -> fetchBearerToken()) + .build(); + +// Basic credentials +HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080) + .credentialsProvider(new BasicCredentialsProvider() { + public String getUsername() { return "user"; } + public String getPassword() { return "pass"; } + }) + .build(); +``` + +## Custom proxy authenticator + +Implement `SplitAuthenticator` to handle proxy challenge/response flows: + +```java +SplitAuthenticator authenticator = new SplitAuthenticator() { + @Override + public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) { + request.setHeader("Proxy-Authorization", "Bearer " + getToken()); + return request; + } +}; +``` + +The `AuthenticatedRequest` gives access to existing headers and the request URL, so the authenticator can make decisions based on context. + +## Certificate pinning + +```java +CertificatePinningConfiguration pinConfig = CertificatePinningConfiguration.builder() + // Pin by hash (sha256 or sha1) + .addPin("sdk.split.io", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") + // Pin from a certificate file (derives hashes automatically) + .addPin("*.split.io", certInputStream) + // Optional: get notified on pin failures + .failureListener((host, certificateChain) -> { + Log.w("Split", "Pin failed for " + host + + ", chain size: " + certificateChain.size()); + }) + .build(); +``` + +Wildcard hosts are supported: `*.example.com` matches one subdomain, `**.example.com` matches any depth. + +## Development SSL overrides + +For test environments with self-signed certificates: + +```java +DevelopmentSslConfig devSsl = new DevelopmentSslConfig(trustManager, hostnameVerifier); + +// Or, if you already have an SSLSocketFactory: +DevelopmentSslConfig devSsl = new DevelopmentSslConfig(sslSocketFactory, trustManager, hostnameVerifier); +``` diff --git a/http-domain/src/main/java/io/split/android/client/network/Algorithm.java b/http-domain/src/main/java/io/split/android/client/network/Algorithm.java index bd3784efe..e0d669e05 100644 --- a/http-domain/src/main/java/io/split/android/client/network/Algorithm.java +++ b/http-domain/src/main/java/io/split/android/client/network/Algorithm.java @@ -1,6 +1,6 @@ package io.split.android.client.network; -public class Algorithm { +class Algorithm { public static final String SHA256 = "sha256"; public static final String SHA1 = "sha1"; diff --git a/http-domain/src/main/java/io/split/android/client/network/Base64Decoder.java b/http-domain/src/main/java/io/split/android/client/network/Base64Decoder.java index f31358046..387a900f0 100644 --- a/http-domain/src/main/java/io/split/android/client/network/Base64Decoder.java +++ b/http-domain/src/main/java/io/split/android/client/network/Base64Decoder.java @@ -1,6 +1,6 @@ package io.split.android.client.network; -public interface Base64Decoder { +interface Base64Decoder { byte[] decode(String base64); } diff --git a/http-domain/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java b/http-domain/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java index 45245ed0d..09504f9e2 100644 --- a/http-domain/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java +++ b/http-domain/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java @@ -15,7 +15,7 @@ import io.split.android.client.utils.logger.Logger; -public class CertificateCheckerHelper { +class CertificateCheckerHelper { @Nullable public static Set getPinsForHost(String pattern, Map> configuredPins) { diff --git a/http-domain/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java b/http-domain/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java index 486eb5be2..b46f38309 100644 --- a/http-domain/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java +++ b/http-domain/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java @@ -4,7 +4,7 @@ import io.split.android.client.utils.logger.Logger; -public class DefaultBase64Decoder implements Base64Decoder { +class DefaultBase64Decoder implements Base64Decoder { @Override public byte[] decode(String base64) { diff --git a/http-domain/src/main/java/io/split/android/client/network/PinEncoder.java b/http-domain/src/main/java/io/split/android/client/network/PinEncoder.java index d34212ca8..3de8beecf 100644 --- a/http-domain/src/main/java/io/split/android/client/network/PinEncoder.java +++ b/http-domain/src/main/java/io/split/android/client/network/PinEncoder.java @@ -2,7 +2,7 @@ import androidx.annotation.NonNull; -public interface PinEncoder { +interface PinEncoder { @NonNull byte[] encodeCertPin(String algorithm, byte[] encodedPublicKey); diff --git a/http-domain/src/main/java/io/split/android/client/network/PinEncoderImpl.java b/http-domain/src/main/java/io/split/android/client/network/PinEncoderImpl.java index f1e010d51..7132b1828 100644 --- a/http-domain/src/main/java/io/split/android/client/network/PinEncoderImpl.java +++ b/http-domain/src/main/java/io/split/android/client/network/PinEncoderImpl.java @@ -7,7 +7,7 @@ import io.split.android.client.utils.logger.Logger; -public class PinEncoderImpl implements PinEncoder { +class PinEncoderImpl implements PinEncoder { @Override @NonNull diff --git a/http/README.md b/http/README.md index 8ece8d648..19f3a3374 100644 --- a/http/README.md +++ b/http/README.md @@ -1,23 +1,89 @@ # HTTP module -Internal HTTP client for the Split SDK. Not exposed to SDK consumers. +HTTP client for the Split SDK. ## Building an `HttpClient` -Use `HttpClientImpl.Builder` to create an instance: +### Minimal ```java HttpClient client = new HttpClientImpl.Builder() .setConnectionTimeout(15_000) .setReadTimeout(15_000) - .setTlsUpdater(tlsUpdater) // optional – TlsUpdater SPI - .setProxy(httpProxy) // optional – proxy config from :http-domain - .setProxyAuthenticator(authenticator) // optional – SplitAuthenticator from :http-domain - .setCertificatePinningConfiguration(pinConfig) // optional – cert pins from :http-domain - .setDevelopmentSslConfig(devSslConfig) // optional – dev/test SSL overrides .build(); ``` +### With `HttpClientConfiguration` (preferred) + +Bundle all settings into a single config object from `:http-domain`: + +```java +HttpClientConfiguration config = HttpClientConfiguration.builder() + .connectionTimeout(15_000) + .readTimeout(15_000) + .proxy(proxy) // optional + .proxyAuthenticator(authenticator) // optional + .certificatePinningConfiguration(pinConfig) // optional + .developmentSslConfig(devSsl) // optional + .build(); + +HttpClient client = new HttpClientImpl.Builder() + .setConfiguration(config) + .setTlsUpdater(tlsUpdater) // optional – TlsUpdater + .build(); +``` + +Individual setter calls on the builder take precedence over the configuration object. + +### Proxy + +```java +// Basic auth proxy +HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080) + .basicAuth("user", "pass") + .build(); + +// mTLS proxy with custom CA +HttpProxy mtlsProxy = HttpProxy.newBuilder("proxy.example.com", 8443) + .proxyCacert(caCertInputStream) + .mtls(clientCertInputStream, clientKeyInputStream) + .build(); + +// With a credentials provider (e.g. bearer token) +HttpProxy bearerProxy = HttpProxy.newBuilder("proxy.example.com", 8080) + .credentialsProvider(new BearerCredentialsProvider(tokenSupplier)) + .build(); +``` + +### Certificate pinning + +```java +CertificatePinningConfiguration pinConfig = CertificatePinningConfiguration.builder() + .addPin("sdk.split.io", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") + .addPin("*.split.io", certInputStream) // derive pins from a certificate file + .failureListener(failedHost -> { + Log.w("Split", "Certificate pinning failed for " + failedHost); + }) + .build(); +``` + +### Development SSL overrides + +For test environments where the server uses a self-signed certificate: + +```java +DevelopmentSslConfig devSsl = new DevelopmentSslConfig(trustManager, hostnameVerifier); +``` + +### TLS on older devices + +Implement the `TlsUpdater` SPI and pass it to the builder. +The client calls `couldBeOld()` to decide whether to force TLS 1.2 via `Tls12OnlySocketFactory`. + +```java +TlsUpdater tlsUpdater = new LegacyTlsUpdaterAdapter(context); // provided by :main +``` + ## Making requests ```java @@ -25,6 +91,10 @@ HttpClient client = new HttpClientImpl.Builder() HttpRequest req = client.request(uri, HttpMethod.GET); HttpResponse resp = req.execute(); +// POST with body +HttpRequest post = client.request(uri, HttpMethod.POST, jsonBody); +HttpResponse resp = post.execute(); + // POST with body and extra headers HttpRequest post = client.request(uri, HttpMethod.POST, jsonBody, extraHeaders); HttpResponse resp = post.execute(); @@ -42,17 +112,13 @@ client.addHeaders(commonHeaders); // Streaming-specific headers (only applied to streamRequest calls) client.setStreamingHeader("SplitSDKClientKey", clientKey); +client.addStreamingHeaders(streamingHeaders); ``` -## TLS on older devices - -Implement the `TlsUpdater` SPI and pass it to the builder. The client calls `couldBeOld()` to decide whether to force TLS 1.2 via `Tls12OnlySocketFactory`. - ## URI building ```java -URI uri = new URIBuilder("https://sdk.split.io/api") - .addPath("splitChanges") +URI uri = new URIBuilder(new URI("https://sdk.split.io/api"), "splitChanges") .addParameter("since", "-1") .build(); ``` diff --git a/http/src/main/java/io/split/android/client/network/HttpClientImpl.java b/http/src/main/java/io/split/android/client/network/HttpClientImpl.java index d846462b7..dde0450e4 100644 --- a/http/src/main/java/io/split/android/client/network/HttpClientImpl.java +++ b/http/src/main/java/io/split/android/client/network/HttpClientImpl.java @@ -202,6 +202,8 @@ public static class Builder { private CertificatePinningConfiguration mCertificatePinningConfiguration; private CertificateChecker mCertificateChecker; private Base64Decoder mBase64Decoder = new DefaultBase64Decoder(); + @Nullable + private HttpClientConfiguration mConfiguration; public Builder setTlsUpdater(@Nullable TlsUpdater tlsUpdater) { mTlsUpdater = tlsUpdater; @@ -263,7 +265,16 @@ Builder setBase64Decoder(Base64Decoder base64Decoder) { return this; } + public Builder setConfiguration(@NonNull HttpClientConfiguration configuration) { + mConfiguration = configuration; + return this; + } + public HttpClient build() { + if (mConfiguration != null) { + applyConfiguration(mConfiguration); + } + if (mDevelopmentSslConfig == null) { if (mTlsUpdater != null && mTlsUpdater.couldBeOld()) { mTlsUpdater.update(); @@ -310,6 +321,27 @@ public HttpClient build() { certificateChecker); } + private void applyConfiguration(@NonNull HttpClientConfiguration configuration) { + if (mConnectionTimeout == -1) { + setConnectionTimeout(configuration.getConnectionTimeout()); + } + if (mReadTimeout == -1) { + setReadTimeout(configuration.getReadTimeout()); + } + if (mProxy == null && configuration.getProxy() != null) { + setProxy(configuration.getProxy()); + } + if (mCertificatePinningConfiguration == null && configuration.getCertificatePinningConfiguration() != null) { + setCertificatePinningConfiguration(configuration.getCertificatePinningConfiguration()); + } + if (mDevelopmentSslConfig == null) { + setDevelopmentSslConfig(configuration.getDevelopmentSslConfig()); + } + if (mProxyAuthenticator == null) { + setProxyAuthenticator(configuration.getProxyAuthenticator()); + } + } + private SSLSocketFactory createSslSocketFactoryFromProxy(HttpProxy proxyParams) { ProxySslSocketFactoryProviderImpl factoryProvider = new ProxySslSocketFactoryProviderImpl(mBase64Decoder); try { diff --git a/http/src/main/java/io/split/android/client/network/HttpRequestImpl.java b/http/src/main/java/io/split/android/client/network/HttpRequestImpl.java index 669dd8598..864a9836f 100644 --- a/http/src/main/java/io/split/android/client/network/HttpRequestImpl.java +++ b/http/src/main/java/io/split/android/client/network/HttpRequestImpl.java @@ -31,7 +31,7 @@ import io.split.android.client.utils.logger.Logger; -public class HttpRequestImpl implements HttpRequest { +class HttpRequestImpl implements HttpRequest { public static final String CONTENT_TYPE = "Content-Type"; public static final String APPLICATION_JSON_CHARSET_UTF_8 = "application/json; charset=utf-8"; diff --git a/http/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java b/http/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java index c8573e235..08d4f5376 100644 --- a/http/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java +++ b/http/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java @@ -30,7 +30,7 @@ import io.split.android.client.utils.logger.Logger; -public class HttpStreamRequestImpl implements HttpStreamRequest { +class HttpStreamRequestImpl implements HttpStreamRequest { private static final int STREAMING_READ_TIMEOUT_IN_MILLISECONDS = 80000; diff --git a/http/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java b/http/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java index bf24d0e74..bae64f68d 100644 --- a/http/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java +++ b/http/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java @@ -8,7 +8,7 @@ import io.split.android.client.utils.logger.Logger; -public class HttpStreamResponseImpl extends BaseHttpResponseImpl implements HttpStreamResponse { +class HttpStreamResponseImpl extends BaseHttpResponseImpl implements HttpStreamResponse { private final BufferedReader mData; diff --git a/main/src/main/java/io/split/android/client/SplitFactoryImpl.java b/main/src/main/java/io/split/android/client/SplitFactoryImpl.java index 0016b6a76..898a7c37d 100644 --- a/main/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/main/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -32,6 +32,7 @@ import io.split.android.client.lifecycle.SplitLifecycleManager; import io.split.android.client.lifecycle.SplitLifecycleManagerImpl; import io.split.android.client.network.HttpClient; +import io.split.android.client.network.HttpClientConfiguration; import io.split.android.client.network.HttpClientImpl; import io.split.android.client.network.LegacyTlsUpdaterAdapter; import io.split.android.client.service.CleanUpDatabaseTask; @@ -382,20 +383,19 @@ private static HttpClient getHttpClient(@NonNull String apiToken, @Nullable GeneralInfoStorage generalInfoStorage) { HttpClient defaultHttpClient; if (httpClient == null) { - HttpClientImpl.Builder builder = new HttpClientImpl.Builder() - .setConnectionTimeout(config.connectionTimeout()) - .setReadTimeout(config.readTimeout()) - .setDevelopmentSslConfig(config.developmentSslConfig()) + HttpClientConfiguration httpConfig = HttpClientConfiguration.builder() + .connectionTimeout(config.connectionTimeout()) + .readTimeout(config.readTimeout()) + .developmentSslConfig(config.developmentSslConfig()) + .proxy(config.proxy()) + .certificatePinningConfiguration(config.certificatePinningConfiguration()) + .proxyAuthenticator(config.authenticator()) + .build(); + + defaultHttpClient = new HttpClientImpl.Builder() + .setConfiguration(httpConfig) .setTlsUpdater(new LegacyTlsUpdaterAdapter(context)) - .setProxyAuthenticator(config.authenticator()); - if (config.proxy() != null) { - builder.setProxy(config.proxy()); - } - if (config.certificatePinningConfiguration() != null) { - builder.setCertificatePinningConfiguration(config.certificatePinningConfiguration()); - } - - defaultHttpClient = builder.build(); + .build(); // This should be extracted; has nothing to do with the method. if (config.proxy() != null && generalInfoStorage != null) { From 491deb3c379b3a12c686ab57622f37f3a7a6e534 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Tue, 10 Feb 2026 09:43:06 -0300 Subject: [PATCH 2/6] Rename http-domain to http-api --- build.gradle | 2 +- {http-domain => http-api}/.gitignore | 0 {http-domain => http-api}/README.md | 2 +- {http-domain => http-api}/build.gradle | 2 +- {http-domain => http-api}/consumer-rules.pro | 0 {http-domain => http-api}/proguard-rules.pro | 0 .../src/main/AndroidManifest.xml | 0 .../android/client/network/Algorithm.java | 0 .../client/network/AuthenticatedRequest.java | 0 .../android/client/network/Authenticator.java | 0 .../android/client/network/Base64Decoder.java | 0 .../network/BasicCredentialsProvider.java | 0 .../network/BearerCredentialsProvider.java | 0 .../network/CertificateCheckerHelper.java | 0 .../android/client/network/CertificatePin.java | 0 .../CertificatePinningConfiguration.java | 0 .../CertificatePinningFailureListener.java | 0 .../client/network/DefaultBase64Decoder.java | 0 .../client/network/DevelopmentSslConfig.java | 0 .../network/HttpClientConfiguration.java | 0 .../android/client/network/HttpProxy.java | 0 .../android/client/network/PinEncoder.java | 0 .../android/client/network/PinEncoderImpl.java | 0 .../client/network/ProxyConfiguration.java | 0 .../network/ProxyCredentialsProvider.java | 0 .../client/network/SplitAuthenticator.java | 0 .../network/CertificateCheckerHelperTest.java | 0 .../CertificatePinningConfigurationTest.java | 0 .../network/HttpClientConfigurationTest.java | 0 .../client/network/PinEncoderImplTest.java | 0 http/README.md | 2 +- http/build.gradle | 2 +- main/build.gradle | 2 +- settings.gradle | 2 +- sonar-project.properties | 18 +++++++++--------- 35 files changed, 16 insertions(+), 16 deletions(-) rename {http-domain => http-api}/.gitignore (100%) rename {http-domain => http-api}/README.md (99%) rename {http-domain => http-api}/build.gradle (89%) rename {http-domain => http-api}/consumer-rules.pro (100%) rename {http-domain => http-api}/proguard-rules.pro (100%) rename {http-domain => http-api}/src/main/AndroidManifest.xml (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/Algorithm.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/AuthenticatedRequest.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/Authenticator.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/Base64Decoder.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/BasicCredentialsProvider.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/BearerCredentialsProvider.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/CertificatePin.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/CertificatePinningConfiguration.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/CertificatePinningFailureListener.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/DevelopmentSslConfig.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/HttpClientConfiguration.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/HttpProxy.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/PinEncoder.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/PinEncoderImpl.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/ProxyConfiguration.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/ProxyCredentialsProvider.java (100%) rename {http-domain => http-api}/src/main/java/io/split/android/client/network/SplitAuthenticator.java (100%) rename {http-domain => http-api}/src/test/java/io/split/android/client/network/CertificateCheckerHelperTest.java (100%) rename {http-domain => http-api}/src/test/java/io/split/android/client/network/CertificatePinningConfigurationTest.java (100%) rename {http-domain => http-api}/src/test/java/io/split/android/client/network/HttpClientConfigurationTest.java (100%) rename {http-domain => http-api}/src/test/java/io/split/android/client/network/PinEncoderImplTest.java (100%) diff --git a/build.gradle b/build.gradle index 64f9cdfbc..a947d08b6 100644 --- a/build.gradle +++ b/build.gradle @@ -140,7 +140,7 @@ dependencies { include project(':events') include project(':events-domain') include project(':api') - include project(':http-domain') + include project(':http-api') include project(':http') } diff --git a/http-domain/.gitignore b/http-api/.gitignore similarity index 100% rename from http-domain/.gitignore rename to http-api/.gitignore diff --git a/http-domain/README.md b/http-api/README.md similarity index 99% rename from http-domain/README.md rename to http-api/README.md index bad8159a8..1d3cc5caf 100644 --- a/http-domain/README.md +++ b/http-api/README.md @@ -1,4 +1,4 @@ -# HTTP Domain module +# HTTP API module Public contracts and configuration types for the HTTP client. These types are exposed to SDK consumers through the `:main` module's `api` dependency. diff --git a/http-domain/build.gradle b/http-api/build.gradle similarity index 89% rename from http-domain/build.gradle rename to http-api/build.gradle index b15da12d4..7e915b5f3 100644 --- a/http-domain/build.gradle +++ b/http-api/build.gradle @@ -5,7 +5,7 @@ plugins { apply from: "$rootDir/gradle/common-android-library.gradle" android { - namespace 'io.split.android.client.network.domain' + namespace 'io.split.android.client.network.api' compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/http-domain/consumer-rules.pro b/http-api/consumer-rules.pro similarity index 100% rename from http-domain/consumer-rules.pro rename to http-api/consumer-rules.pro diff --git a/http-domain/proguard-rules.pro b/http-api/proguard-rules.pro similarity index 100% rename from http-domain/proguard-rules.pro rename to http-api/proguard-rules.pro diff --git a/http-domain/src/main/AndroidManifest.xml b/http-api/src/main/AndroidManifest.xml similarity index 100% rename from http-domain/src/main/AndroidManifest.xml rename to http-api/src/main/AndroidManifest.xml diff --git a/http-domain/src/main/java/io/split/android/client/network/Algorithm.java b/http-api/src/main/java/io/split/android/client/network/Algorithm.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/Algorithm.java rename to http-api/src/main/java/io/split/android/client/network/Algorithm.java diff --git a/http-domain/src/main/java/io/split/android/client/network/AuthenticatedRequest.java b/http-api/src/main/java/io/split/android/client/network/AuthenticatedRequest.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/AuthenticatedRequest.java rename to http-api/src/main/java/io/split/android/client/network/AuthenticatedRequest.java diff --git a/http-domain/src/main/java/io/split/android/client/network/Authenticator.java b/http-api/src/main/java/io/split/android/client/network/Authenticator.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/Authenticator.java rename to http-api/src/main/java/io/split/android/client/network/Authenticator.java diff --git a/http-domain/src/main/java/io/split/android/client/network/Base64Decoder.java b/http-api/src/main/java/io/split/android/client/network/Base64Decoder.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/Base64Decoder.java rename to http-api/src/main/java/io/split/android/client/network/Base64Decoder.java diff --git a/http-domain/src/main/java/io/split/android/client/network/BasicCredentialsProvider.java b/http-api/src/main/java/io/split/android/client/network/BasicCredentialsProvider.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/BasicCredentialsProvider.java rename to http-api/src/main/java/io/split/android/client/network/BasicCredentialsProvider.java diff --git a/http-domain/src/main/java/io/split/android/client/network/BearerCredentialsProvider.java b/http-api/src/main/java/io/split/android/client/network/BearerCredentialsProvider.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/BearerCredentialsProvider.java rename to http-api/src/main/java/io/split/android/client/network/BearerCredentialsProvider.java diff --git a/http-domain/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java b/http-api/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java rename to http-api/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java diff --git a/http-domain/src/main/java/io/split/android/client/network/CertificatePin.java b/http-api/src/main/java/io/split/android/client/network/CertificatePin.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/CertificatePin.java rename to http-api/src/main/java/io/split/android/client/network/CertificatePin.java diff --git a/http-domain/src/main/java/io/split/android/client/network/CertificatePinningConfiguration.java b/http-api/src/main/java/io/split/android/client/network/CertificatePinningConfiguration.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/CertificatePinningConfiguration.java rename to http-api/src/main/java/io/split/android/client/network/CertificatePinningConfiguration.java diff --git a/http-domain/src/main/java/io/split/android/client/network/CertificatePinningFailureListener.java b/http-api/src/main/java/io/split/android/client/network/CertificatePinningFailureListener.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/CertificatePinningFailureListener.java rename to http-api/src/main/java/io/split/android/client/network/CertificatePinningFailureListener.java diff --git a/http-domain/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java b/http-api/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java rename to http-api/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java diff --git a/http-domain/src/main/java/io/split/android/client/network/DevelopmentSslConfig.java b/http-api/src/main/java/io/split/android/client/network/DevelopmentSslConfig.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/DevelopmentSslConfig.java rename to http-api/src/main/java/io/split/android/client/network/DevelopmentSslConfig.java diff --git a/http-domain/src/main/java/io/split/android/client/network/HttpClientConfiguration.java b/http-api/src/main/java/io/split/android/client/network/HttpClientConfiguration.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/HttpClientConfiguration.java rename to http-api/src/main/java/io/split/android/client/network/HttpClientConfiguration.java diff --git a/http-domain/src/main/java/io/split/android/client/network/HttpProxy.java b/http-api/src/main/java/io/split/android/client/network/HttpProxy.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/HttpProxy.java rename to http-api/src/main/java/io/split/android/client/network/HttpProxy.java diff --git a/http-domain/src/main/java/io/split/android/client/network/PinEncoder.java b/http-api/src/main/java/io/split/android/client/network/PinEncoder.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/PinEncoder.java rename to http-api/src/main/java/io/split/android/client/network/PinEncoder.java diff --git a/http-domain/src/main/java/io/split/android/client/network/PinEncoderImpl.java b/http-api/src/main/java/io/split/android/client/network/PinEncoderImpl.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/PinEncoderImpl.java rename to http-api/src/main/java/io/split/android/client/network/PinEncoderImpl.java diff --git a/http-domain/src/main/java/io/split/android/client/network/ProxyConfiguration.java b/http-api/src/main/java/io/split/android/client/network/ProxyConfiguration.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/ProxyConfiguration.java rename to http-api/src/main/java/io/split/android/client/network/ProxyConfiguration.java diff --git a/http-domain/src/main/java/io/split/android/client/network/ProxyCredentialsProvider.java b/http-api/src/main/java/io/split/android/client/network/ProxyCredentialsProvider.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/ProxyCredentialsProvider.java rename to http-api/src/main/java/io/split/android/client/network/ProxyCredentialsProvider.java diff --git a/http-domain/src/main/java/io/split/android/client/network/SplitAuthenticator.java b/http-api/src/main/java/io/split/android/client/network/SplitAuthenticator.java similarity index 100% rename from http-domain/src/main/java/io/split/android/client/network/SplitAuthenticator.java rename to http-api/src/main/java/io/split/android/client/network/SplitAuthenticator.java diff --git a/http-domain/src/test/java/io/split/android/client/network/CertificateCheckerHelperTest.java b/http-api/src/test/java/io/split/android/client/network/CertificateCheckerHelperTest.java similarity index 100% rename from http-domain/src/test/java/io/split/android/client/network/CertificateCheckerHelperTest.java rename to http-api/src/test/java/io/split/android/client/network/CertificateCheckerHelperTest.java diff --git a/http-domain/src/test/java/io/split/android/client/network/CertificatePinningConfigurationTest.java b/http-api/src/test/java/io/split/android/client/network/CertificatePinningConfigurationTest.java similarity index 100% rename from http-domain/src/test/java/io/split/android/client/network/CertificatePinningConfigurationTest.java rename to http-api/src/test/java/io/split/android/client/network/CertificatePinningConfigurationTest.java diff --git a/http-domain/src/test/java/io/split/android/client/network/HttpClientConfigurationTest.java b/http-api/src/test/java/io/split/android/client/network/HttpClientConfigurationTest.java similarity index 100% rename from http-domain/src/test/java/io/split/android/client/network/HttpClientConfigurationTest.java rename to http-api/src/test/java/io/split/android/client/network/HttpClientConfigurationTest.java diff --git a/http-domain/src/test/java/io/split/android/client/network/PinEncoderImplTest.java b/http-api/src/test/java/io/split/android/client/network/PinEncoderImplTest.java similarity index 100% rename from http-domain/src/test/java/io/split/android/client/network/PinEncoderImplTest.java rename to http-api/src/test/java/io/split/android/client/network/PinEncoderImplTest.java diff --git a/http/README.md b/http/README.md index 19f3a3374..12e59f39f 100644 --- a/http/README.md +++ b/http/README.md @@ -15,7 +15,7 @@ HttpClient client = new HttpClientImpl.Builder() ### With `HttpClientConfiguration` (preferred) -Bundle all settings into a single config object from `:http-domain`: +Bundle all settings into a single config object from `:http-api`: ```java HttpClientConfiguration config = HttpClientConfiguration.builder() diff --git a/http/build.gradle b/http/build.gradle index f613652b7..a7367b06e 100644 --- a/http/build.gradle +++ b/http/build.gradle @@ -16,7 +16,7 @@ android { dependencies { implementation libs.annotation implementation project(':logger') - api project(':http-domain') + api project(':http-api') testImplementation libs.junit4 testImplementation libs.mockitoCore diff --git a/main/build.gradle b/main/build.gradle index 70d932be8..63f91db29 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -52,7 +52,7 @@ dependencies { // Public api modules api project(':logger') api project(':api') - api project(':http-domain') + api project(':http-api') // Internal module dependencies implementation project(':http') implementation project(':events-domain') diff --git a/settings.gradle b/settings.gradle index 7eaafe2d3..9ddd50403 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,7 +2,7 @@ rootProject.name = 'android-client' include ':api' include ':logger' -include ':http-domain' +include ':http-api' include ':http' include ':main' include ':events' diff --git a/sonar-project.properties b/sonar-project.properties index a3cc5e80d..a8542a151 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,16 +3,16 @@ sonar.projectKey=splitio_android-client sonar.projectName=android-client # Path to source directories (multi-module) -# Root project contains modules: main, events, logger, http-domain, http -sonar.sources=main/src/main/java,events/src/main/java,logger/src/main/java,http-domain/src/main/java,http/src/main/java +# Root project contains modules: main, events, logger, http-api, http +sonar.sources=main/src/main/java,events/src/main/java,logger/src/main/java,http-api/src/main/java,http/src/main/java # Path to compiled classes (multi-module) -# Include binary paths for all modules: main, events, logger, http-domain, http +# Include binary paths for all modules: main, events, logger, http-api, http sonar.java.binaries=\ main/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ events/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ logger/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ - http-domain/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ + http-api/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ http/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes # Path to dependency/libraries jars (multi-module) @@ -29,10 +29,10 @@ sonar.java.libraries=\ logger/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ logger/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ logger/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ - http-domain/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ - http-domain/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ - http-domain/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ - http-domain/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + http-api/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + http-api/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + http-api/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + http-api/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ http/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ http/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ http/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ @@ -40,7 +40,7 @@ sonar.java.libraries=\ # Path to test directories (multi-module) # Only include test source folders that are guaranteed to exist in all environments -sonar.tests=main/src/test/java,main/src/androidTest/java,main/src/sharedTest/java,events/src/test/java,logger/src/test/java,http-domain/src/test/java,http/src/test/java +sonar.tests=main/src/test/java,main/src/androidTest/java,main/src/sharedTest/java,events/src/test/java,logger/src/test/java,http-api/src/test/java,http/src/test/java # Encoding of the source code sonar.sourceEncoding=UTF-8 From 17cde55d4d4b35e8c5b12242de49acd54f5581a0 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Tue, 10 Feb 2026 10:18:35 -0300 Subject: [PATCH 3/6] Fix Sonar scan --- .github/workflows/sonarqube.yml | 3 --- sonar-project.properties | 18 ++++++++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index c2f306a64..94a0bf3c3 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -9,9 +9,6 @@ on: pull_request: branches: - '*' - push: - branches: - - master concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/sonar-project.properties b/sonar-project.properties index a8542a151..85a95779d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,12 +3,14 @@ sonar.projectKey=splitio_android-client sonar.projectName=android-client # Path to source directories (multi-module) -# Root project contains modules: main, events, logger, http-api, http -sonar.sources=main/src/main/java,events/src/main/java,logger/src/main/java,http-api/src/main/java,http/src/main/java +# Root project contains modules: api, events-domain, main, events, logger, http-api, http +sonar.sources=api/src/main/java,events-domain/src/main/java,main/src/main/java,events/src/main/java,logger/src/main/java,http-api/src/main/java,http/src/main/java # Path to compiled classes (multi-module) -# Include binary paths for all modules: main, events, logger, http-api, http +# Include binary paths for all modules: api, events-domain, main, events, logger, http-api, http sonar.java.binaries=\ + api/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ + events-domain/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ main/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ events/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ logger/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ @@ -29,6 +31,14 @@ sonar.java.libraries=\ logger/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ logger/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ logger/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + api/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + api/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + api/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + api/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + events-domain/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + events-domain/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + events-domain/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + events-domain/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ http-api/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ http-api/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ http-api/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ @@ -40,7 +50,7 @@ sonar.java.libraries=\ # Path to test directories (multi-module) # Only include test source folders that are guaranteed to exist in all environments -sonar.tests=main/src/test/java,main/src/androidTest/java,main/src/sharedTest/java,events/src/test/java,logger/src/test/java,http-api/src/test/java,http/src/test/java +sonar.tests=api/src/test/java,events-domain/src/test/java,main/src/test/java,main/src/androidTest/java,main/src/sharedTest/java,events/src/test/java,logger/src/test/java,http-api/src/test/java,http/src/test/java # Encoding of the source code sonar.sourceEncoding=UTF-8 From 5a33696c6d69a5cf640654207ef22b8b0fcce210 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Tue, 10 Feb 2026 10:58:24 -0300 Subject: [PATCH 4/6] Add tests --- .../client/network/HttpClientImpl.java | 34 ++++ ...ttpClientImplBuilderConfigurationTest.java | 160 ++++++++++++++++++ .../android/client/SplitFactoryImpl.java | 24 ++- .../SplitFactoryImplConfigMappingTest.java | 118 +++++++++++++ 4 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 http/src/test/java/io/split/android/client/network/HttpClientImplBuilderConfigurationTest.java create mode 100644 main/src/test/java/io/split/android/client/SplitFactoryImplConfigMappingTest.java diff --git a/http/src/main/java/io/split/android/client/network/HttpClientImpl.java b/http/src/main/java/io/split/android/client/network/HttpClientImpl.java index dde0450e4..0ffb2ac95 100644 --- a/http/src/main/java/io/split/android/client/network/HttpClientImpl.java +++ b/http/src/main/java/io/split/android/client/network/HttpClientImpl.java @@ -161,6 +161,40 @@ SSLSocketFactory getSslSocketFactory() { return mSslSocketFactory; } + @VisibleForTesting + long getReadTimeout() { + return mReadTimeout; + } + + @VisibleForTesting + long getConnectionTimeout() { + return mConnectionTimeout; + } + + @VisibleForTesting + @Nullable + HttpProxy getHttpProxy() { + return mHttpProxy; + } + + @VisibleForTesting + @Nullable + SplitUrlConnectionAuthenticator getProxyAuthenticator() { + return mProxyAuthenticator; + } + + @VisibleForTesting + @Nullable + DevelopmentSslConfig getDevelopmentSslConfig() { + return mDevelopmentSslConfig; + } + + @VisibleForTesting + @Nullable + CertificateChecker getCertificateChecker() { + return mCertificateChecker; + } + private Proxy initializeProxy(HttpProxy proxy) { if (proxy != null) { return new Proxy( diff --git a/http/src/test/java/io/split/android/client/network/HttpClientImplBuilderConfigurationTest.java b/http/src/test/java/io/split/android/client/network/HttpClientImplBuilderConfigurationTest.java new file mode 100644 index 000000000..d85907743 --- /dev/null +++ b/http/src/test/java/io/split/android/client/network/HttpClientImplBuilderConfigurationTest.java @@ -0,0 +1,160 @@ +package io.split.android.client.network; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.junit.Test; + +public class HttpClientImplBuilderConfigurationTest { + + @Test + public void configurationAppliesAllValuesWhenBuilderHasDefaults() { + HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080).build(); + SplitAuthenticator authenticator = new SplitAuthenticator() { + @Nullable + @Override + public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) { + return request; + } + }; + CertificatePinningConfiguration pinConfig = mock(CertificatePinningConfiguration.class); + DevelopmentSslConfig devSsl = mock(DevelopmentSslConfig.class); + + HttpClientConfiguration config = HttpClientConfiguration.builder() + .connectionTimeout(5000) + .readTimeout(10000) + .proxy(proxy) + .proxyAuthenticator(authenticator) + .certificatePinningConfiguration(pinConfig) + .developmentSslConfig(devSsl) + .build(); + + HttpClientImpl client = (HttpClientImpl) new HttpClientImpl.Builder() + .setConfiguration(config) + .build(); + + assertEquals(5000, client.getConnectionTimeout()); + assertEquals(10000, client.getReadTimeout()); + assertNotNull(client.getHttpProxy()); + assertEquals("proxy.example.com", client.getHttpProxy().getHost()); + assertEquals(8080, client.getHttpProxy().getPort()); + assertNotNull(client.getProxyAuthenticator()); + assertNotNull(client.getCertificateChecker()); + assertNotNull(client.getDevelopmentSslConfig()); + } + + @Test + public void builderValuesTakePrecedenceOverConfiguration() { + HttpProxy configProxy = HttpProxy.newBuilder("config.proxy.com", 9090).build(); + HttpProxy builderProxy = HttpProxy.newBuilder("builder.proxy.com", 7070).build(); + + HttpClientConfiguration config = HttpClientConfiguration.builder() + .connectionTimeout(5000) + .readTimeout(10000) + .proxy(configProxy) + .build(); + + HttpClientImpl client = (HttpClientImpl) new HttpClientImpl.Builder() + .setConnectionTimeout(1000) + .setReadTimeout(2000) + .setProxy(builderProxy) + .setConfiguration(config) + .build(); + + // Builder values should win + assertEquals(1000, client.getConnectionTimeout()); + assertEquals(2000, client.getReadTimeout()); + assertEquals("builder.proxy.com", client.getHttpProxy().getHost()); + assertEquals(7070, client.getHttpProxy().getPort()); + } + + @Test + public void configurationWithNullOptionalFieldsDoesNotOverrideBuilderDefaults() { + HttpClientConfiguration config = HttpClientConfiguration.builder() + .connectionTimeout(3000) + .readTimeout(6000) + .build(); + + HttpClientImpl client = (HttpClientImpl) new HttpClientImpl.Builder() + .setConfiguration(config) + .build(); + + assertEquals(3000, client.getConnectionTimeout()); + assertEquals(6000, client.getReadTimeout()); + assertNull(client.getHttpProxy()); + assertNull(client.getProxyAuthenticator()); + assertNull(client.getCertificateChecker()); + assertNull(client.getDevelopmentSslConfig()); + } + + @Test + public void buildWithoutConfigurationUsesBuilderDefaults() { + HttpClientImpl client = (HttpClientImpl) new HttpClientImpl.Builder() + .setConnectionTimeout(4000) + .setReadTimeout(8000) + .build(); + + assertEquals(4000, client.getConnectionTimeout()); + assertEquals(8000, client.getReadTimeout()); + assertNull(client.getHttpProxy()); + assertNull(client.getProxyAuthenticator()); + assertNull(client.getCertificateChecker()); + assertNull(client.getDevelopmentSslConfig()); + } + + @Test + public void builderAuthenticatorTakesPrecedenceOverConfiguration() { + SplitAuthenticator configAuth = new SplitAuthenticator() { + @Nullable + @Override + public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) { + request.setHeader("Source", "config"); + return request; + } + }; + SplitAuthenticator builderAuth = new SplitAuthenticator() { + @Nullable + @Override + public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) { + request.setHeader("Source", "builder"); + return request; + } + }; + + HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080).build(); + + HttpClientConfiguration config = HttpClientConfiguration.builder() + .proxy(proxy) + .proxyAuthenticator(configAuth) + .build(); + + HttpClientImpl client = (HttpClientImpl) new HttpClientImpl.Builder() + .setProxy(proxy) + .setProxyAuthenticator(builderAuth) + .setConfiguration(config) + .build(); + + // Builder authenticator should win — proxy authenticator should not be null + assertNotNull(client.getProxyAuthenticator()); + } + + @Test + public void configurationWithNullProxyDoesNotSetProxy() { + HttpClientConfiguration config = HttpClientConfiguration.builder() + .connectionTimeout(1000) + .readTimeout(2000) + .proxy(null) + .build(); + + HttpClientImpl client = (HttpClientImpl) new HttpClientImpl.Builder() + .setConfiguration(config) + .build(); + + assertNull(client.getHttpProxy()); + } +} diff --git a/main/src/main/java/io/split/android/client/SplitFactoryImpl.java b/main/src/main/java/io/split/android/client/SplitFactoryImpl.java index 898a7c37d..3cd4f501b 100644 --- a/main/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/main/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -20,6 +20,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; +import androidx.annotation.VisibleForTesting; + import io.split.android.client.main.BuildConfig; import io.split.android.client.api.Key; import io.split.android.client.common.CompressionUtilProvider; @@ -383,14 +385,7 @@ private static HttpClient getHttpClient(@NonNull String apiToken, @Nullable GeneralInfoStorage generalInfoStorage) { HttpClient defaultHttpClient; if (httpClient == null) { - HttpClientConfiguration httpConfig = HttpClientConfiguration.builder() - .connectionTimeout(config.connectionTimeout()) - .readTimeout(config.readTimeout()) - .developmentSslConfig(config.developmentSslConfig()) - .proxy(config.proxy()) - .certificatePinningConfiguration(config.certificatePinningConfiguration()) - .proxyAuthenticator(config.authenticator()) - .build(); + HttpClientConfiguration httpConfig = buildHttpClientConfiguration(config); defaultHttpClient = new HttpClientImpl.Builder() .setConfiguration(httpConfig) @@ -412,6 +407,19 @@ private static HttpClient getHttpClient(@NonNull String apiToken, return defaultHttpClient; } + @VisibleForTesting + @NonNull + static HttpClientConfiguration buildHttpClientConfiguration(@NonNull SplitClientConfig config) { + return HttpClientConfiguration.builder() + .connectionTimeout(config.connectionTimeout()) + .readTimeout(config.readTimeout()) + .developmentSslConfig(config.developmentSslConfig()) + .proxy(config.proxy()) + .certificatePinningConfiguration(config.certificatePinningConfiguration()) + .proxyAuthenticator(config.authenticator()) + .build(); + } + private static String getFlagsSpec(@Nullable TestingConfig testingConfig) { if (testingConfig == null) { return BuildConfig.FLAGS_SPEC; diff --git a/main/src/test/java/io/split/android/client/SplitFactoryImplConfigMappingTest.java b/main/src/test/java/io/split/android/client/SplitFactoryImplConfigMappingTest.java new file mode 100644 index 000000000..900f67f10 --- /dev/null +++ b/main/src/test/java/io/split/android/client/SplitFactoryImplConfigMappingTest.java @@ -0,0 +1,118 @@ +package io.split.android.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.junit.Test; + +import io.split.android.client.network.AuthenticatedRequest; +import io.split.android.client.network.CertificatePinningConfiguration; +import io.split.android.client.network.DevelopmentSslConfig; +import io.split.android.client.network.HttpClientConfiguration; +import io.split.android.client.network.HttpProxy; +import io.split.android.client.network.SplitAuthenticator; + +public class SplitFactoryImplConfigMappingTest { + + @Test + public void buildHttpClientConfigurationMapsAllFields() { + SplitClientConfig splitConfig = mock(SplitClientConfig.class); + HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080).build(); + CertificatePinningConfiguration pinConfig = mock(CertificatePinningConfiguration.class); + DevelopmentSslConfig devSsl = mock(DevelopmentSslConfig.class); + SplitAuthenticator authenticator = new SplitAuthenticator() { + @Nullable + @Override + public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) { + return request; + } + }; + + when(splitConfig.connectionTimeout()).thenReturn(5000); + when(splitConfig.readTimeout()).thenReturn(10000); + when(splitConfig.proxy()).thenReturn(proxy); + when(splitConfig.certificatePinningConfiguration()).thenReturn(pinConfig); + when(splitConfig.developmentSslConfig()).thenReturn(devSsl); + when(splitConfig.authenticator()).thenReturn(authenticator); + + HttpClientConfiguration result = SplitFactoryImpl.buildHttpClientConfiguration(splitConfig); + + assertEquals(5000, result.getConnectionTimeout()); + assertEquals(10000, result.getReadTimeout()); + assertNotNull(result.getProxy()); + assertEquals("proxy.example.com", result.getProxy().getHost()); + assertEquals(8080, result.getProxy().getPort()); + assertSame(pinConfig, result.getCertificatePinningConfiguration()); + assertSame(devSsl, result.getDevelopmentSslConfig()); + assertSame(authenticator, result.getProxyAuthenticator()); + } + + @Test + public void buildHttpClientConfigurationWithNullOptionals() { + SplitClientConfig splitConfig = mock(SplitClientConfig.class); + + when(splitConfig.connectionTimeout()).thenReturn(3000); + when(splitConfig.readTimeout()).thenReturn(6000); + when(splitConfig.proxy()).thenReturn(null); + when(splitConfig.certificatePinningConfiguration()).thenReturn(null); + when(splitConfig.developmentSslConfig()).thenReturn(null); + when(splitConfig.authenticator()).thenReturn(null); + + HttpClientConfiguration result = SplitFactoryImpl.buildHttpClientConfiguration(splitConfig); + + assertEquals(3000, result.getConnectionTimeout()); + assertEquals(6000, result.getReadTimeout()); + assertNull(result.getProxy()); + assertNull(result.getCertificatePinningConfiguration()); + assertNull(result.getDevelopmentSslConfig()); + assertNull(result.getProxyAuthenticator()); + } + + @Test + public void buildHttpClientConfigurationWithZeroTimeouts() { + SplitClientConfig splitConfig = mock(SplitClientConfig.class); + + when(splitConfig.connectionTimeout()).thenReturn(0); + when(splitConfig.readTimeout()).thenReturn(0); + when(splitConfig.proxy()).thenReturn(null); + when(splitConfig.certificatePinningConfiguration()).thenReturn(null); + when(splitConfig.developmentSslConfig()).thenReturn(null); + when(splitConfig.authenticator()).thenReturn(null); + + HttpClientConfiguration result = SplitFactoryImpl.buildHttpClientConfiguration(splitConfig); + + assertEquals(0, result.getConnectionTimeout()); + assertEquals(0, result.getReadTimeout()); + } + + @Test + public void buildHttpClientConfigurationWithOnlyProxy() { + SplitClientConfig splitConfig = mock(SplitClientConfig.class); + HttpProxy proxy = HttpProxy.newBuilder("myproxy.local", 3128).build(); + + when(splitConfig.connectionTimeout()).thenReturn(15000); + when(splitConfig.readTimeout()).thenReturn(15000); + when(splitConfig.proxy()).thenReturn(proxy); + when(splitConfig.certificatePinningConfiguration()).thenReturn(null); + when(splitConfig.developmentSslConfig()).thenReturn(null); + when(splitConfig.authenticator()).thenReturn(null); + + HttpClientConfiguration result = SplitFactoryImpl.buildHttpClientConfiguration(splitConfig); + + assertEquals(15000, result.getConnectionTimeout()); + assertEquals(15000, result.getReadTimeout()); + assertNotNull(result.getProxy()); + assertEquals("myproxy.local", result.getProxy().getHost()); + assertEquals(3128, result.getProxy().getPort()); + assertNull(result.getCertificatePinningConfiguration()); + assertNull(result.getDevelopmentSslConfig()); + assertNull(result.getProxyAuthenticator()); + } +} From 5ea978ee18d5161c3bfa75ad00f0640582797ecc Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Tue, 10 Feb 2026 13:19:06 -0300 Subject: [PATCH 5/6] Small fixes --- .../java/io/split/android/client/network/HttpProxy.java | 8 ++++---- .../io/split/android/client/network/HttpClientImpl.java | 4 +++- .../java/io/split/android/client/network/UrlEscapers.java | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/http-api/src/main/java/io/split/android/client/network/HttpProxy.java b/http-api/src/main/java/io/split/android/client/network/HttpProxy.java index a6dc011fa..969f69176 100644 --- a/http-api/src/main/java/io/split/android/client/network/HttpProxy.java +++ b/http-api/src/main/java/io/split/android/client/network/HttpProxy.java @@ -29,7 +29,7 @@ private HttpProxy(Builder builder, boolean isLegacy) { mIsLegacy = isLegacy; } - public @Nullable String getHost() { + public @NonNull String getHost() { return mHost; } @@ -61,7 +61,7 @@ public int getPort() { return mCredentialsProvider; } - public static Builder newBuilder(@Nullable String host, int port) { + public static Builder newBuilder(@NonNull String host, int port) { return new Builder(host, port); } @@ -70,7 +70,7 @@ public boolean isLegacy() { } public static class Builder { - private final @Nullable String mHost; + private final @NonNull String mHost; private final int mPort; private @Nullable String mUsername; private @Nullable String mPassword; @@ -80,7 +80,7 @@ public static class Builder { @Nullable private ProxyCredentialsProvider mCredentialsProvider; - private Builder(@Nullable String host, int port) { + private Builder(@NonNull String host, int port) { mHost = host; mPort = port; } diff --git a/http/src/main/java/io/split/android/client/network/HttpClientImpl.java b/http/src/main/java/io/split/android/client/network/HttpClientImpl.java index 0ffb2ac95..8fbff1270 100644 --- a/http/src/main/java/io/split/android/client/network/HttpClientImpl.java +++ b/http/src/main/java/io/split/android/client/network/HttpClientImpl.java @@ -244,7 +244,7 @@ public Builder setTlsUpdater(@Nullable TlsUpdater tlsUpdater) { return this; } - public Builder setProxy(HttpProxy proxy) { + public Builder setProxy(@NonNull HttpProxy proxy) { mProxy = proxy; mProxyCredentialsProvider = proxy.getCredentialsProvider(); return this; @@ -355,6 +355,8 @@ public HttpClient build() { certificateChecker); } + // Configuration timeout values of 0 or less are intentionally ignored by + // setConnectionTimeout / setReadTimeout, leaving the platform default in place. private void applyConfiguration(@NonNull HttpClientConfiguration configuration) { if (mConnectionTimeout == -1) { setConnectionTimeout(configuration.getConnectionTimeout()); diff --git a/http/src/main/java/io/split/android/client/network/UrlEscapers.java b/http/src/main/java/io/split/android/client/network/UrlEscapers.java index d12a6f995..11098b8e0 100644 --- a/http/src/main/java/io/split/android/client/network/UrlEscapers.java +++ b/http/src/main/java/io/split/android/client/network/UrlEscapers.java @@ -3,7 +3,7 @@ /** * Based on Guava UrlEscapers */ -final class UrlEscapers { +public final class UrlEscapers { private UrlEscapers() {} private static final String URL_PATH_OTHER_SAFE_CHARS_LACKING_PLUS = From 17a8df640a960f220cd94a53d260abfa92452ace Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Tue, 10 Feb 2026 15:01:20 -0300 Subject: [PATCH 6/6] Fix cert pin serialization --- .../network/CertificatePinSerializer.java | 67 +++++++++ ...rtificatePinningConfigurationProvider.java | 21 +-- .../io/split/android/client/utils/Json.java | 3 + .../network/CertificatePinSerializerTest.java | 129 ++++++++++++++++++ 4 files changed, 203 insertions(+), 17 deletions(-) create mode 100644 main/src/main/java/io/split/android/client/network/CertificatePinSerializer.java create mode 100644 main/src/test/java/io/split/android/client/network/CertificatePinSerializerTest.java diff --git a/main/src/main/java/io/split/android/client/network/CertificatePinSerializer.java b/main/src/main/java/io/split/android/client/network/CertificatePinSerializer.java new file mode 100644 index 000000000..494ed6177 --- /dev/null +++ b/main/src/main/java/io/split/android/client/network/CertificatePinSerializer.java @@ -0,0 +1,67 @@ +package io.split.android.client.network; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +/** + * Custom Gson {@link TypeAdapter} for {@link CertificatePin} that uses + * {@code "algo"} and {@code "pin"} as JSON keys instead of the raw field names. + */ +public class CertificatePinSerializer extends TypeAdapter { + + @Override + public void write(JsonWriter out, CertificatePin src) throws IOException { + out.beginObject(); + out.name("algo").value(src.getAlgorithm()); + out.name("pin"); + out.beginArray(); + for (byte b : src.getPin()) { + out.value(b); + } + out.endArray(); + out.endObject(); + } + + @Override + public CertificatePin read(JsonReader in) throws IOException { + String algorithm = null; + byte[] pin = null; + + in.beginObject(); + while (in.hasNext()) { + String name = in.nextName(); + switch (name) { + case "algo": + algorithm = in.nextString(); + break; + case "pin": + pin = readByteArray(in); + break; + default: + in.skipValue(); + break; + } + } + in.endObject(); + + return new CertificatePin(pin, algorithm); + } + + private static byte[] readByteArray(JsonReader in) throws IOException { + java.util.List bytes = new java.util.ArrayList<>(); + in.beginArray(); + while (in.hasNext()) { + bytes.add((byte) in.nextInt()); + } + in.endArray(); + + byte[] result = new byte[bytes.size()]; + for (int i = 0; i < bytes.size(); i++) { + result[i] = bytes.get(i); + } + return result; + } +} diff --git a/main/src/main/java/io/split/android/client/network/CertificatePinningConfigurationProvider.java b/main/src/main/java/io/split/android/client/network/CertificatePinningConfigurationProvider.java index 801baa909..aa640fbc5 100644 --- a/main/src/main/java/io/split/android/client/network/CertificatePinningConfigurationProvider.java +++ b/main/src/main/java/io/split/android/client/network/CertificatePinningConfigurationProvider.java @@ -1,10 +1,8 @@ package io.split.android.client.network; -import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; -import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -15,18 +13,14 @@ public class CertificatePinningConfigurationProvider { public static CertificatePinningConfiguration getCertificatePinningConfiguration(String pinsJson) { try { - Type type = new TypeToken>>() { + Type type = new TypeToken>>() { }.getType(); - Map> certificatePins = Json.fromJson(pinsJson, type); + Map> certificatePins = Json.fromJson(pinsJson, type); if (certificatePins != null && !certificatePins.isEmpty()) { CertificatePinningConfiguration.Builder builder = CertificatePinningConfiguration.builder(); - for (Map.Entry> entry : certificatePins.entrySet()) { - Set pins = new HashSet<>(); - for (CertificatePinDto dto : entry.getValue()) { - pins.add(new CertificatePin(dto.pin, dto.algorithm)); - } - builder.addPins(entry.getKey(), pins); + for (Map.Entry> entry : certificatePins.entrySet()) { + builder.addPins(entry.getKey(), entry.getValue()); } return builder @@ -38,11 +32,4 @@ public static CertificatePinningConfiguration getCertificatePinningConfiguration return null; } - - private static class CertificatePinDto { - @SerializedName("pin") - byte[] pin; - @SerializedName("algo") - String algorithm; - } } diff --git a/main/src/main/java/io/split/android/client/utils/Json.java b/main/src/main/java/io/split/android/client/utils/Json.java index a4c4e2e9c..bb97eea95 100644 --- a/main/src/main/java/io/split/android/client/utils/Json.java +++ b/main/src/main/java/io/split/android/client/utils/Json.java @@ -15,6 +15,8 @@ import java.util.Set; import io.split.android.client.dtos.KeyImpression; +import io.split.android.client.network.CertificatePin; +import io.split.android.client.network.CertificatePinSerializer; import io.split.android.client.service.impressions.KeyImpressionSerializer; import io.split.android.client.utils.serializer.DoubleSerializer; @@ -24,6 +26,7 @@ public class Json { .serializeNulls() .registerTypeAdapter(Double.class, new DoubleSerializer()) .registerTypeAdapter(KeyImpression.class, new KeyImpressionSerializer()) + .registerTypeAdapter(CertificatePin.class, new CertificatePinSerializer()) .create(); private static volatile Gson mNonNullJson; diff --git a/main/src/test/java/io/split/android/client/network/CertificatePinSerializerTest.java b/main/src/test/java/io/split/android/client/network/CertificatePinSerializerTest.java new file mode 100644 index 000000000..0dfc1aad5 --- /dev/null +++ b/main/src/test/java/io/split/android/client/network/CertificatePinSerializerTest.java @@ -0,0 +1,129 @@ +package io.split.android.client.network; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Set; + +public class CertificatePinSerializerTest { + + private Gson mGson; + + @Before + public void setUp() { + mGson = new GsonBuilder() + .registerTypeAdapter(CertificatePin.class, new CertificatePinSerializer()) + .create(); + } + + @Test + public void serializeSinglePin() { + CertificatePin pin = new CertificatePin(new byte[]{1, 2, 3}, "sha256"); + + String json = mGson.toJson(pin); + + assertEquals("{\"algo\":\"sha256\",\"pin\":[1,2,3]}", json); + } + + @Test + public void serializeNegativeByteValues() { + CertificatePin pin = new CertificatePin(new byte[]{-80, 50, -99, -126, 11}, "sha256"); + + String json = mGson.toJson(pin); + + assertEquals("{\"algo\":\"sha256\",\"pin\":[-80,50,-99,-126,11]}", json); + } + + @Test + public void deserializeSinglePin() { + String json = "{\"algo\":\"sha1\",\"pin\":[-116,-73,-94,-80,55]}"; + + CertificatePin pin = mGson.fromJson(json, CertificatePin.class); + + assertNotNull(pin); + assertEquals("sha1", pin.getAlgorithm()); + assertArrayEquals(new byte[]{-116, -73, -94, -80, 55}, pin.getPin()); + } + + @Test + public void roundTripPreservesData() { + CertificatePin original = new CertificatePin(new byte[]{-116, -123, 30, -25}, "sha256"); + + String json = mGson.toJson(original); + CertificatePin deserialized = mGson.fromJson(json, CertificatePin.class); + + assertNotNull(deserialized); + assertEquals(original.getAlgorithm(), deserialized.getAlgorithm()); + assertArrayEquals(original.getPin(), deserialized.getPin()); + } + + @Test + public void roundTripMapOfSets() { + String expectedJson = "{\"events.split.io\":[{\"algo\":\"sha256\",\"pin\":[-80,50,-99,-126,11]},{\"algo\":\"sha1\",\"pin\":[-116,-73,-94,-80,55]}],\"sdk.split.io\":[{\"algo\":\"sha256\",\"pin\":[-116,-123,30,-25]}]}"; + + Type type = new TypeToken>>() { + }.getType(); + Map> deserialized = mGson.fromJson(expectedJson, type); + + assertNotNull(deserialized); + assertEquals(2, deserialized.size()); + assertEquals(2, deserialized.get("events.split.io").size()); + assertEquals(1, deserialized.get("sdk.split.io").size()); + + // Re-serialize and deserialize + String reserialized = mGson.toJson(deserialized, type); + Map> roundTripped = mGson.fromJson(reserialized, type); + + assertNotNull(roundTripped); + assertEquals(deserialized.size(), roundTripped.size()); + for (Map.Entry> entry : deserialized.entrySet()) { + Set originalPins = entry.getValue(); + Set roundTrippedPins = roundTripped.get(entry.getKey()); + assertNotNull(roundTrippedPins); + assertEquals(originalPins.size(), roundTrippedPins.size()); + assertEquals(originalPins, roundTrippedPins); + } + } + + @Test + public void deserializeWithUnknownFieldsIsIgnored() { + String json = "{\"algo\":\"sha256\",\"pin\":[1,2],\"extra\":\"ignored\"}"; + + CertificatePin pin = mGson.fromJson(json, CertificatePin.class); + + assertNotNull(pin); + assertEquals("sha256", pin.getAlgorithm()); + assertArrayEquals(new byte[]{1, 2}, pin.getPin()); + } + + @Test + public void deserializeMissingFieldsResultsInNulls() { + String json = "{}"; + + CertificatePin pin = mGson.fromJson(json, CertificatePin.class); + + assertNotNull(pin); + assertNull(pin.getAlgorithm()); + assertNull(pin.getPin()); + } + + @Test + public void serializeEmptyPinArray() { + CertificatePin pin = new CertificatePin(new byte[]{}, "sha256"); + + String json = mGson.toJson(pin); + + assertEquals("{\"algo\":\"sha256\",\"pin\":[]}", json); + } +}