pins) {
if (host == null || host.trim().isEmpty()) {
Logger.e("Host cannot be null or empty. Ignoring entry");
return;
diff --git a/main/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 main/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-api/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java b/http-api/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java
new file mode 100644
index 000000000..b46f38309
--- /dev/null
+++ b/http-api/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java
@@ -0,0 +1,20 @@
+package io.split.android.client.network;
+
+import android.util.Base64;
+
+import io.split.android.client.utils.logger.Logger;
+
+class DefaultBase64Decoder implements Base64Decoder {
+
+ @Override
+ public byte[] decode(String base64) {
+ try {
+ return Base64.decode(base64, Base64.DEFAULT);
+ } catch (IllegalArgumentException e) {
+ Logger.e("Received bytes didn't correspond to a valid Base64 encoded string." + e.getLocalizedMessage());
+ } catch (Exception e) {
+ Logger.e("An unknown error has occurred " + e.getLocalizedMessage());
+ }
+ return null;
+ }
+}
diff --git a/main/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 main/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-api/src/main/java/io/split/android/client/network/HttpClientConfiguration.java b/http-api/src/main/java/io/split/android/client/network/HttpClientConfiguration.java
new file mode 100644
index 000000000..6bd6f7d58
--- /dev/null
+++ b/http-api/src/main/java/io/split/android/client/network/HttpClientConfiguration.java
@@ -0,0 +1,142 @@
+package io.split.android.client.network;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+
+public class HttpClientConfiguration {
+
+ private final long mConnectionTimeout;
+ private final long mReadTimeout;
+ @Nullable
+ private final HttpProxy mProxy;
+ @Nullable
+ private final CertificatePinningConfiguration mCertificatePinningConfiguration;
+ @Nullable
+ private final DevelopmentSslConfig mDevelopmentSslConfig;
+ @Nullable
+ private final SplitAuthenticator mProxyAuthenticator;
+
+ private HttpClientConfiguration(Builder builder) {
+ mConnectionTimeout = builder.mConnectionTimeout;
+ mReadTimeout = builder.mReadTimeout;
+ mProxy = builder.mProxy;
+ mCertificatePinningConfiguration = builder.mCertificatePinningConfiguration;
+ mDevelopmentSslConfig = builder.mDevelopmentSslConfig;
+ mProxyAuthenticator = builder.mProxyAuthenticator;
+ }
+
+ public long getConnectionTimeout() {
+ return mConnectionTimeout;
+ }
+
+ public long getReadTimeout() {
+ return mReadTimeout;
+ }
+
+ @Nullable
+ public HttpProxy getProxy() {
+ return mProxy;
+ }
+
+ @Nullable
+ public CertificatePinningConfiguration getCertificatePinningConfiguration() {
+ return mCertificatePinningConfiguration;
+ }
+
+ @Nullable
+ public DevelopmentSslConfig getDevelopmentSslConfig() {
+ return mDevelopmentSslConfig;
+ }
+
+ @Nullable
+ public SplitAuthenticator getProxyAuthenticator() {
+ return mProxyAuthenticator;
+ }
+
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private long mConnectionTimeout;
+ private long mReadTimeout;
+ @Nullable
+ private HttpProxy mProxy;
+ @Nullable
+ private CertificatePinningConfiguration mCertificatePinningConfiguration;
+ @Nullable
+ private DevelopmentSslConfig mDevelopmentSslConfig;
+ @Nullable
+ private SplitAuthenticator mProxyAuthenticator;
+
+ private Builder() {
+ }
+
+ /**
+ * Sets the connection timeout in milliseconds.
+ */
+ @NonNull
+ public Builder connectionTimeout(long connectionTimeout) {
+ mConnectionTimeout = connectionTimeout;
+ return this;
+ }
+
+ /**
+ * Sets the read timeout in milliseconds.
+ */
+ @NonNull
+ public Builder readTimeout(long readTimeout) {
+ mReadTimeout = readTimeout;
+ return this;
+ }
+
+ /**
+ * Sets the HTTP proxy configuration.
+ */
+ @NonNull
+ public Builder proxy(@Nullable HttpProxy proxy) {
+ mProxy = proxy;
+ return this;
+ }
+
+ /**
+ * Sets the certificate pinning configuration.
+ */
+ @NonNull
+ public Builder certificatePinningConfiguration(@Nullable CertificatePinningConfiguration configuration) {
+ mCertificatePinningConfiguration = configuration;
+ return this;
+ }
+
+ /**
+ * Sets the development SSL configuration.
+ *
+ * This is intended for development/testing environments only.
+ */
+ @NonNull
+ public Builder developmentSslConfig(@Nullable DevelopmentSslConfig developmentSslConfig) {
+ mDevelopmentSslConfig = developmentSslConfig;
+ return this;
+ }
+
+ /**
+ * Sets the proxy authenticator.
+ */
+ @NonNull
+ public Builder proxyAuthenticator(@Nullable SplitAuthenticator proxyAuthenticator) {
+ mProxyAuthenticator = proxyAuthenticator;
+ return this;
+ }
+
+ /**
+ * Builds the configuration.
+ */
+ @NonNull
+ public HttpClientConfiguration build() {
+ return new HttpClientConfiguration(this);
+ }
+ }
+}
diff --git a/main/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 93%
rename from main/src/main/java/io/split/android/client/network/HttpProxy.java
rename to http-api/src/main/java/io/split/android/client/network/HttpProxy.java
index a6dc011fa..969f69176 100644
--- a/main/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/main/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 main/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/main/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 main/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/main/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 main/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/main/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 main/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/main/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 81%
rename from main/src/main/java/io/split/android/client/network/SplitAuthenticator.java
rename to http-api/src/main/java/io/split/android/client/network/SplitAuthenticator.java
index 542ff42dc..494ba736e 100644
--- a/main/src/main/java/io/split/android/client/network/SplitAuthenticator.java
+++ b/http-api/src/main/java/io/split/android/client/network/SplitAuthenticator.java
@@ -1,6 +1,6 @@
package io.split.android.client.network;
/** @noinspection unused*/
-public abstract class SplitAuthenticator implements Authenticator {
+public abstract class SplitAuthenticator implements Authenticator {
}
diff --git a/main/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 main/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/main/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 main/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-api/src/test/java/io/split/android/client/network/HttpClientConfigurationTest.java b/http-api/src/test/java/io/split/android/client/network/HttpClientConfigurationTest.java
new file mode 100644
index 000000000..f722a3439
--- /dev/null
+++ b/http-api/src/test/java/io/split/android/client/network/HttpClientConfigurationTest.java
@@ -0,0 +1,97 @@
+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 org.junit.Test;
+
+public class HttpClientConfigurationTest {
+
+ @Test
+ public void builderSetsConnectionTimeout() {
+ HttpClientConfiguration config = HttpClientConfiguration.builder()
+ .connectionTimeout(15_000)
+ .build();
+
+ assertEquals(15_000, config.getConnectionTimeout());
+ }
+
+ @Test
+ public void builderSetsReadTimeout() {
+ HttpClientConfiguration config = HttpClientConfiguration.builder()
+ .readTimeout(30_000)
+ .build();
+
+ assertEquals(30_000, config.getReadTimeout());
+ }
+
+ @Test
+ public void builderSetsProxy() {
+ HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080).build();
+ HttpClientConfiguration config = HttpClientConfiguration.builder()
+ .proxy(proxy)
+ .build();
+
+ assertNotNull(config.getProxy());
+ assertEquals("proxy.example.com", config.getProxy().getHost());
+ assertEquals(8080, config.getProxy().getPort());
+ }
+
+ @Test
+ public void builderSetsCertificatePinningConfiguration() {
+ CertificatePinningConfiguration certConfig = CertificatePinningConfiguration.builder()
+ .addPin("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
+ .build();
+ HttpClientConfiguration config = HttpClientConfiguration.builder()
+ .certificatePinningConfiguration(certConfig)
+ .build();
+
+ assertNotNull(config.getCertificatePinningConfiguration());
+ }
+
+ @Test
+ public void builderSetsDevelopmentSslConfig() {
+ // DevelopmentSslConfig requires non-null args; just verify null default
+ HttpClientConfiguration config = HttpClientConfiguration.builder().build();
+ assertNull(config.getDevelopmentSslConfig());
+ }
+
+ @Test
+ public void builderSetsProxyAuthenticator() {
+ HttpClientConfiguration config = HttpClientConfiguration.builder().build();
+ assertNull(config.getProxyAuthenticator());
+ }
+
+ @Test
+ public void defaultValuesAreZeroAndNull() {
+ HttpClientConfiguration config = HttpClientConfiguration.builder().build();
+
+ assertEquals(0, config.getConnectionTimeout());
+ assertEquals(0, config.getReadTimeout());
+ assertNull(config.getProxy());
+ assertNull(config.getCertificatePinningConfiguration());
+ assertNull(config.getDevelopmentSslConfig());
+ assertNull(config.getProxyAuthenticator());
+ }
+
+ @Test
+ public void builderSetsAllFields() {
+ HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080).build();
+ CertificatePinningConfiguration certConfig = CertificatePinningConfiguration.builder()
+ .addPin("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
+ .build();
+
+ HttpClientConfiguration config = HttpClientConfiguration.builder()
+ .connectionTimeout(10_000)
+ .readTimeout(20_000)
+ .proxy(proxy)
+ .certificatePinningConfiguration(certConfig)
+ .build();
+
+ assertEquals(10_000, config.getConnectionTimeout());
+ assertEquals(20_000, config.getReadTimeout());
+ assertNotNull(config.getProxy());
+ assertNotNull(config.getCertificatePinningConfiguration());
+ }
+}
diff --git a/main/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 main/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/.gitignore b/http/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/http/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/http/README.md b/http/README.md
new file mode 100644
index 000000000..12e59f39f
--- /dev/null
+++ b/http/README.md
@@ -0,0 +1,124 @@
+# HTTP module
+
+HTTP client for the Split SDK.
+
+## Building an `HttpClient`
+
+### Minimal
+
+```java
+HttpClient client = new HttpClientImpl.Builder()
+ .setConnectionTimeout(15_000)
+ .setReadTimeout(15_000)
+ .build();
+```
+
+### With `HttpClientConfiguration` (preferred)
+
+Bundle all settings into a single config object from `:http-api`:
+
+```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
+// Simple GET
+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();
+
+// SSE streaming
+HttpStreamRequest stream = client.streamRequest(uri);
+HttpStreamResponse streamResp = stream.execute();
+```
+
+## Global headers
+
+```java
+client.setHeader("Authorization", "Bearer " + apiKey);
+client.addHeaders(commonHeaders);
+
+// Streaming-specific headers (only applied to streamRequest calls)
+client.setStreamingHeader("SplitSDKClientKey", clientKey);
+client.addStreamingHeaders(streamingHeaders);
+```
+
+## URI building
+
+```java
+URI uri = new URIBuilder(new URI("https://sdk.split.io/api"), "splitChanges")
+ .addParameter("since", "-1")
+ .build();
+```
diff --git a/http/build.gradle b/http/build.gradle
new file mode 100644
index 000000000..a7367b06e
--- /dev/null
+++ b/http/build.gradle
@@ -0,0 +1,26 @@
+plugins {
+ id 'com.android.library'
+}
+
+apply from: "$rootDir/gradle/common-android-library.gradle"
+
+android {
+ namespace 'io.split.android.client.network.http'
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation libs.annotation
+ implementation project(':logger')
+ api project(':http-api')
+
+ testImplementation libs.junit4
+ testImplementation libs.mockitoCore
+ testImplementation libs.mockitoInline
+ testImplementation libs.okhttpMockwebserver
+ testImplementation libs.okhttpTls
+}
diff --git a/http/consumer-rules.pro b/http/consumer-rules.pro
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/http/consumer-rules.pro
@@ -0,0 +1 @@
+
diff --git a/http/proguard-rules.pro b/http/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/http/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/http/src/main/AndroidManifest.xml b/http/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..8bdb7e14b
--- /dev/null
+++ b/http/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/main/src/main/java/io/split/android/client/network/Base64Encoder.java b/http/src/main/java/io/split/android/client/network/Base64Encoder.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/Base64Encoder.java
rename to http/src/main/java/io/split/android/client/network/Base64Encoder.java
diff --git a/main/src/main/java/io/split/android/client/network/BaseHttpResponse.java b/http/src/main/java/io/split/android/client/network/BaseHttpResponse.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/BaseHttpResponse.java
rename to http/src/main/java/io/split/android/client/network/BaseHttpResponse.java
diff --git a/main/src/main/java/io/split/android/client/network/BaseHttpResponseImpl.java b/http/src/main/java/io/split/android/client/network/BaseHttpResponseImpl.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/BaseHttpResponseImpl.java
rename to http/src/main/java/io/split/android/client/network/BaseHttpResponseImpl.java
diff --git a/main/src/main/java/io/split/android/client/network/CertificateChecker.java b/http/src/main/java/io/split/android/client/network/CertificateChecker.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/CertificateChecker.java
rename to http/src/main/java/io/split/android/client/network/CertificateChecker.java
diff --git a/main/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java b/http/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java
rename to http/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java
diff --git a/main/src/main/java/io/split/android/client/network/ChainCleaner.java b/http/src/main/java/io/split/android/client/network/ChainCleaner.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/ChainCleaner.java
rename to http/src/main/java/io/split/android/client/network/ChainCleaner.java
diff --git a/main/src/main/java/io/split/android/client/network/ChainCleanerImpl.java b/http/src/main/java/io/split/android/client/network/ChainCleanerImpl.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/ChainCleanerImpl.java
rename to http/src/main/java/io/split/android/client/network/ChainCleanerImpl.java
diff --git a/http/src/main/java/io/split/android/client/network/DefaultBase64Encoder.java b/http/src/main/java/io/split/android/client/network/DefaultBase64Encoder.java
new file mode 100644
index 000000000..4106c7784
--- /dev/null
+++ b/http/src/main/java/io/split/android/client/network/DefaultBase64Encoder.java
@@ -0,0 +1,22 @@
+package io.split.android.client.network;
+
+import android.util.Base64;
+
+class DefaultBase64Encoder implements Base64Encoder {
+
+ @Override
+ public String encode(String value) {
+ if (value == null) {
+ return null;
+ }
+ return Base64.encodeToString(value.getBytes(), Base64.NO_WRAP);
+ }
+
+ @Override
+ public String encode(byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+ return Base64.encodeToString(bytes, Base64.NO_WRAP);
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/network/HttpClient.java b/http/src/main/java/io/split/android/client/network/HttpClient.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/HttpClient.java
rename to http/src/main/java/io/split/android/client/network/HttpClient.java
diff --git a/main/src/main/java/io/split/android/client/network/HttpClientImpl.java b/http/src/main/java/io/split/android/client/network/HttpClientImpl.java
similarity index 83%
rename from main/src/main/java/io/split/android/client/network/HttpClientImpl.java
rename to http/src/main/java/io/split/android/client/network/HttpClientImpl.java
index f41271796..8fbff1270 100644
--- a/main/src/main/java/io/split/android/client/network/HttpClientImpl.java
+++ b/http/src/main/java/io/split/android/client/network/HttpClientImpl.java
@@ -1,7 +1,5 @@
package io.split.android.client.network;
-import android.content.Context;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -20,8 +18,6 @@
import javax.net.ssl.SSLSocketFactory;
-import io.split.android.client.utils.Base64Util;
-import io.split.android.client.utils.Utils;
import io.split.android.client.utils.logger.Logger;
public class HttpClientImpl implements HttpClient {
@@ -165,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(
@@ -180,7 +210,7 @@ private SplitUrlConnectionAuthenticator initializeProxyAuthenticator(HttpProxy p
return null;
} else if (proxyAuthenticator != null) {
return new SplitUrlConnectionAuthenticator(proxyAuthenticator);
- } else if (!Utils.isNullOrEmpty(proxy.getUsername())) {
+ } else if (proxy.getUsername() != null && !proxy.getUsername().isEmpty()) {
return createBasicAuthenticator(proxy.getUsername(), proxy.getPassword());
}
@@ -188,18 +218,7 @@ private SplitUrlConnectionAuthenticator initializeProxyAuthenticator(HttpProxy p
}
private static SplitUrlConnectionAuthenticator createBasicAuthenticator(String username, String password) {
- return new SplitUrlConnectionAuthenticator(new SplitBasicAuthenticator(username, password, new Base64Encoder() {
-
- @Override
- public String encode(String value) {
- return Base64Util.encode(value);
- }
-
- @Override
- public String encode(byte[] bytes) {
- return Base64Util.encode(bytes);
- }
- }));
+ return new SplitUrlConnectionAuthenticator(new SplitBasicAuthenticator(username, password, new DefaultBase64Encoder()));
}
public static class Builder {
@@ -211,18 +230,21 @@ public static class Builder {
private long mConnectionTimeout = -1;
private DevelopmentSslConfig mDevelopmentSslConfig = null;
private SSLSocketFactory mSslSocketFactory = null;
- private Context mHostAppContext;
+ @Nullable
+ private TlsUpdater mTlsUpdater;
private UrlSanitizer mUrlSanitizer;
private CertificatePinningConfiguration mCertificatePinningConfiguration;
private CertificateChecker mCertificateChecker;
private Base64Decoder mBase64Decoder = new DefaultBase64Decoder();
+ @Nullable
+ private HttpClientConfiguration mConfiguration;
- public Builder setContext(Context context) {
- mHostAppContext = context;
+ public Builder setTlsUpdater(@Nullable TlsUpdater tlsUpdater) {
+ mTlsUpdater = tlsUpdater;
return this;
}
- public Builder setProxy(HttpProxy proxy) {
+ public Builder setProxy(@NonNull HttpProxy proxy) {
mProxy = proxy;
mProxyCredentialsProvider = proxy.getCredentialsProvider();
return this;
@@ -277,15 +299,24 @@ 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 (LegacyTlsUpdater.couldBeOld()) {
- LegacyTlsUpdater.update(mHostAppContext);
+ if (mTlsUpdater != null && mTlsUpdater.couldBeOld()) {
+ mTlsUpdater.update();
}
if (mProxy != null) {
mSslSocketFactory = createSslSocketFactoryFromProxy(mProxy);
- } else if (LegacyTlsUpdater.couldBeOld()) {
+ } else if (mTlsUpdater != null && mTlsUpdater.couldBeOld()) {
try {
mSslSocketFactory = new Tls12OnlySocketFactory();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
@@ -324,6 +355,29 @@ 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());
+ }
+ 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/main/src/main/java/io/split/android/client/network/HttpException.java b/http/src/main/java/io/split/android/client/network/HttpException.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/HttpException.java
rename to http/src/main/java/io/split/android/client/network/HttpException.java
diff --git a/main/src/main/java/io/split/android/client/network/HttpMethod.java b/http/src/main/java/io/split/android/client/network/HttpMethod.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/HttpMethod.java
rename to http/src/main/java/io/split/android/client/network/HttpMethod.java
diff --git a/main/src/main/java/io/split/android/client/network/HttpOverTunnelExecutor.java b/http/src/main/java/io/split/android/client/network/HttpOverTunnelExecutor.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/HttpOverTunnelExecutor.java
rename to http/src/main/java/io/split/android/client/network/HttpOverTunnelExecutor.java
diff --git a/main/src/main/java/io/split/android/client/network/HttpRequest.java b/http/src/main/java/io/split/android/client/network/HttpRequest.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/HttpRequest.java
rename to http/src/main/java/io/split/android/client/network/HttpRequest.java
diff --git a/main/src/main/java/io/split/android/client/network/HttpRequestHelper.java b/http/src/main/java/io/split/android/client/network/HttpRequestHelper.java
similarity index 97%
rename from main/src/main/java/io/split/android/client/network/HttpRequestHelper.java
rename to http/src/main/java/io/split/android/client/network/HttpRequestHelper.java
index 4688f00b7..14e5a5b06 100644
--- a/main/src/main/java/io/split/android/client/network/HttpRequestHelper.java
+++ b/http/src/main/java/io/split/android/client/network/HttpRequestHelper.java
@@ -1,6 +1,5 @@
package io.split.android.client.network;
-import static io.split.android.client.utils.Utils.getAsInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -100,6 +99,13 @@ static void applyTimeouts(long readTimeout, long connectionTimeout, HttpURLConne
}
}
+ private static int getAsInt(long value) {
+ if (value > Integer.MAX_VALUE) {
+ return Integer.MAX_VALUE;
+ }
+ return (int) value;
+ }
+
static void applySslConfig(SSLSocketFactory sslSocketFactory, DevelopmentSslConfig developmentSslConfig, HttpURLConnection connection) {
if (sslSocketFactory != null) {
if (connection instanceof HttpsURLConnection) {
diff --git a/main/src/main/java/io/split/android/client/network/HttpRequestImpl.java b/http/src/main/java/io/split/android/client/network/HttpRequestImpl.java
similarity index 94%
rename from main/src/main/java/io/split/android/client/network/HttpRequestImpl.java
rename to http/src/main/java/io/split/android/client/network/HttpRequestImpl.java
index 1f2a0c402..864a9836f 100644
--- a/main/src/main/java/io/split/android/client/network/HttpRequestImpl.java
+++ b/http/src/main/java/io/split/android/client/network/HttpRequestImpl.java
@@ -1,6 +1,6 @@
package io.split.android.client.network;
-import static io.split.android.client.utils.Utils.checkNotNull;
+import static java.util.Objects.requireNonNull;
import static io.split.android.client.network.HttpRequestHelper.applySslConfig;
import static io.split.android.client.network.HttpRequestHelper.applyTimeouts;
@@ -29,14 +29,19 @@
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;
-import io.split.android.client.service.http.HttpStatus;
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";
+ /**
+ * Non-retryable status code for SSL errors.
+ * Mirrors HttpStatus.INTERNAL_NON_RETRYABLE from :main.
+ */
+ static final int NON_RETRYABLE_STATUS_CODE = 9009;
+
private final URI mUri;
private final String mBody;
private final HttpMethod mHttpMethod;
@@ -73,11 +78,11 @@ public class HttpRequestImpl implements HttpRequest {
@Nullable SSLSocketFactory sslSocketFactory,
@NonNull UrlSanitizer urlSanitizer,
@Nullable CertificateChecker certificateChecker) {
- mUri = checkNotNull(uri);
- mHttpMethod = checkNotNull(httpMethod);
+ mUri = requireNonNull(uri);
+ mHttpMethod = requireNonNull(httpMethod);
mBody = body;
- mUrlSanitizer = checkNotNull(urlSanitizer);
- mHeaders = new HashMap<>(checkNotNull(headers));
+ mUrlSanitizer = requireNonNull(urlSanitizer);
+ mHeaders = new HashMap<>(requireNonNull(headers));
mProxy = proxy;
mHttpProxy = httpProxy;
mProxyAuthenticator = proxyAuthenticator;
@@ -119,7 +124,7 @@ private HttpResponse getRequest(AtomicBoolean wasRetried) throws HttpException {
} catch (ProtocolException e) {
throw new HttpException("Http method not allowed: " + e.getLocalizedMessage());
} catch (SSLPeerUnverifiedException e) {
- throw new HttpException("SSL Peer Unverified: " + e.getLocalizedMessage(), HttpStatus.INTERNAL_NON_RETRYABLE.getCode());
+ throw new HttpException("SSL Peer Unverified: " + e.getLocalizedMessage(), NON_RETRYABLE_STATUS_CODE);
} catch (IOException e) {
throw new HttpException("Something happened while retrieving data: " + e.getLocalizedMessage());
} finally {
@@ -146,7 +151,7 @@ private HttpResponse postRequest(AtomicBoolean wasRetried) throws HttpException
response = handleProxyAuthentication(response, false, wasRetried);
}
} catch (SSLPeerUnverifiedException e) {
- throw new HttpException("SSL Peer Unverified: " + e.getLocalizedMessage(), HttpStatus.INTERNAL_NON_RETRYABLE.getCode());
+ throw new HttpException("SSL Peer Unverified: " + e.getLocalizedMessage(), NON_RETRYABLE_STATUS_CODE);
} catch (IOException e) {
throw new HttpException("Something happened while posting data: " + e.getLocalizedMessage());
} finally {
diff --git a/main/src/main/java/io/split/android/client/network/HttpResponse.java b/http/src/main/java/io/split/android/client/network/HttpResponse.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/HttpResponse.java
rename to http/src/main/java/io/split/android/client/network/HttpResponse.java
diff --git a/main/src/main/java/io/split/android/client/network/HttpResponseConnectionAdapter.java b/http/src/main/java/io/split/android/client/network/HttpResponseConnectionAdapter.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/HttpResponseConnectionAdapter.java
rename to http/src/main/java/io/split/android/client/network/HttpResponseConnectionAdapter.java
diff --git a/main/src/main/java/io/split/android/client/network/HttpResponseImpl.java b/http/src/main/java/io/split/android/client/network/HttpResponseImpl.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/HttpResponseImpl.java
rename to http/src/main/java/io/split/android/client/network/HttpResponseImpl.java
diff --git a/main/src/main/java/io/split/android/client/network/HttpStreamRequest.java b/http/src/main/java/io/split/android/client/network/HttpStreamRequest.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/HttpStreamRequest.java
rename to http/src/main/java/io/split/android/client/network/HttpStreamRequest.java
diff --git a/main/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java b/http/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java
similarity index 95%
rename from main/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java
rename to http/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java
index d6f48b8d9..08d4f5376 100644
--- a/main/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java
+++ b/http/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java
@@ -2,7 +2,7 @@
import static io.split.android.client.network.HttpRequestHelper.checkPins;
import static io.split.android.client.network.HttpRequestHelper.createConnection;
-import static io.split.android.client.utils.Utils.checkNotNull;
+import static java.util.Objects.requireNonNull;
import static io.split.android.client.network.HttpRequestHelper.applySslConfig;
import static io.split.android.client.network.HttpRequestHelper.applyTimeouts;
@@ -28,10 +28,9 @@
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;
-import io.split.android.client.service.http.HttpStatus;
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;
@@ -72,11 +71,11 @@ public class HttpStreamRequestImpl implements HttpStreamRequest {
@Nullable HttpProxy httpProxy,
@Nullable ProxyCredentialsProvider proxyCredentialsProvider,
@Nullable ProxyCacertConnectionHandler proxyCacertConnectionHandler) {
- mUri = checkNotNull(uri);
+ mUri = requireNonNull(uri);
mHttpMethod = HttpMethod.GET;
mProxy = proxy;
- mUrlSanitizer = checkNotNull(urlSanitizer);
- mHeaders = new HashMap<>(checkNotNull(headers));
+ mUrlSanitizer = requireNonNull(urlSanitizer);
+ mHeaders = new HashMap<>(requireNonNull(headers));
mProxyAuthenticator = proxyAuthenticator;
mConnectionTimeout = connectionTimeout;
mDevelopmentSslConfig = developmentSslConfig;
@@ -141,7 +140,7 @@ private HttpStreamResponse getRequest() throws HttpException, IOException {
throw new HttpException("Http method not allowed: " + e.getLocalizedMessage());
} catch (SSLPeerUnverifiedException e) {
disconnect();
- throw new HttpException("SSL peer not verified: " + e.getLocalizedMessage(), HttpStatus.INTERNAL_NON_RETRYABLE.getCode());
+ throw new HttpException("SSL peer not verified: " + e.getLocalizedMessage(), HttpRequestImpl.NON_RETRYABLE_STATUS_CODE);
} catch (SocketException e) {
disconnect();
// Let socket-related IOExceptions pass through unwrapped for consistent error handling
diff --git a/main/src/main/java/io/split/android/client/network/HttpStreamResponse.java b/http/src/main/java/io/split/android/client/network/HttpStreamResponse.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/HttpStreamResponse.java
rename to http/src/main/java/io/split/android/client/network/HttpStreamResponse.java
diff --git a/main/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java b/http/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java
similarity index 96%
rename from main/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java
rename to http/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java
index bf24d0e74..bae64f68d 100644
--- a/main/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/network/PercentEscaper.java b/http/src/main/java/io/split/android/client/network/PercentEscaper.java
similarity index 97%
rename from main/src/main/java/io/split/android/client/network/PercentEscaper.java
rename to http/src/main/java/io/split/android/client/network/PercentEscaper.java
index b61bed710..9f99ceb8e 100644
--- a/main/src/main/java/io/split/android/client/network/PercentEscaper.java
+++ b/http/src/main/java/io/split/android/client/network/PercentEscaper.java
@@ -1,6 +1,6 @@
package io.split.android.client.network;
-import static io.split.android.client.utils.Utils.checkNotNull;
+import static java.util.Objects.requireNonNull;
/**
* Based on Guava PercentEscaper
@@ -37,7 +37,7 @@ final class PercentEscaper extends UnicodeEscaper {
* @throws IllegalArgumentException if any of the parameters were invalid
*/
public PercentEscaper(String safeChars, boolean plusForSpace) {
- checkNotNull(safeChars); // eager for GWT.
+ requireNonNull(safeChars); // eager for GWT.
// Avoid any misunderstandings about the behavior of this escaper
if (safeChars.matches(".*[0-9A-Za-z].*")) {
throw new IllegalArgumentException(
@@ -78,7 +78,7 @@ private static boolean[] createSafeOctets(String safeChars) {
*/
@Override
protected int nextEscapeIndex(CharSequence csq, int index, int end) {
- checkNotNull(csq);
+ requireNonNull(csq);
for (; index < end; index++) {
char c = csq.charAt(index);
if (c >= safeOctets.length || !safeOctets[c]) {
@@ -94,7 +94,7 @@ protected int nextEscapeIndex(CharSequence csq, int index, int end) {
*/
@Override
public String escape(String s) {
- checkNotNull(s);
+ requireNonNull(s);
int slen = s.length();
for (int index = 0; index < slen; index++) {
char c = s.charAt(index);
diff --git a/main/src/main/java/io/split/android/client/network/ProxyCacertConnectionHandler.java b/http/src/main/java/io/split/android/client/network/ProxyCacertConnectionHandler.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/ProxyCacertConnectionHandler.java
rename to http/src/main/java/io/split/android/client/network/ProxyCacertConnectionHandler.java
diff --git a/main/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProvider.java b/http/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProvider.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProvider.java
rename to http/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProvider.java
diff --git a/main/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProviderImpl.java b/http/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProviderImpl.java
similarity index 99%
rename from main/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProviderImpl.java
rename to http/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProviderImpl.java
index 49a84c134..8978258cf 100644
--- a/main/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProviderImpl.java
+++ b/http/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProviderImpl.java
@@ -1,6 +1,6 @@
package io.split.android.client.network;
-import static io.split.android.client.utils.Utils.checkNotNull;
+import static java.util.Objects.requireNonNull;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -43,7 +43,7 @@ class ProxySslSocketFactoryProviderImpl implements ProxySslSocketFactoryProvider
}
ProxySslSocketFactoryProviderImpl(@NonNull Base64Decoder base64Decoder) {
- mBase64Decoder = checkNotNull(base64Decoder);
+ mBase64Decoder = requireNonNull(base64Decoder);
}
@Override
diff --git a/main/src/main/java/io/split/android/client/network/RawHttpResponseParser.java b/http/src/main/java/io/split/android/client/network/RawHttpResponseParser.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/RawHttpResponseParser.java
rename to http/src/main/java/io/split/android/client/network/RawHttpResponseParser.java
diff --git a/main/src/main/java/io/split/android/client/network/SplitAuthenticatedRequest.java b/http/src/main/java/io/split/android/client/network/SplitAuthenticatedRequest.java
similarity index 97%
rename from main/src/main/java/io/split/android/client/network/SplitAuthenticatedRequest.java
rename to http/src/main/java/io/split/android/client/network/SplitAuthenticatedRequest.java
index 9b426385c..cddb6370e 100644
--- a/main/src/main/java/io/split/android/client/network/SplitAuthenticatedRequest.java
+++ b/http/src/main/java/io/split/android/client/network/SplitAuthenticatedRequest.java
@@ -8,7 +8,7 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-public class SplitAuthenticatedRequest implements AuthenticatedRequest {
+public class SplitAuthenticatedRequest implements AuthenticatedRequest {
private final String mUrl;
private final Map mHeaders = new ConcurrentHashMap<>();
diff --git a/main/src/main/java/io/split/android/client/network/SplitBasicAuthenticator.java b/http/src/main/java/io/split/android/client/network/SplitBasicAuthenticator.java
similarity index 91%
rename from main/src/main/java/io/split/android/client/network/SplitBasicAuthenticator.java
rename to http/src/main/java/io/split/android/client/network/SplitBasicAuthenticator.java
index bd49d9ca4..b87c6699f 100644
--- a/main/src/main/java/io/split/android/client/network/SplitBasicAuthenticator.java
+++ b/http/src/main/java/io/split/android/client/network/SplitBasicAuthenticator.java
@@ -19,7 +19,7 @@ class SplitBasicAuthenticator extends SplitAuthenticator {
@Nullable
@Override
- public SplitAuthenticatedRequest authenticate(@NonNull SplitAuthenticatedRequest request) {
+ public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) {
String credential = basic(mUsername, mPassword);
request.setHeader(PROXY_AUTHORIZATION_HEADER, credential);
diff --git a/main/src/main/java/io/split/android/client/network/SplitUrlConnectionAuthenticator.java b/http/src/main/java/io/split/android/client/network/SplitUrlConnectionAuthenticator.java
similarity index 88%
rename from main/src/main/java/io/split/android/client/network/SplitUrlConnectionAuthenticator.java
rename to http/src/main/java/io/split/android/client/network/SplitUrlConnectionAuthenticator.java
index fdb97f302..2c0cd3d5a 100644
--- a/main/src/main/java/io/split/android/client/network/SplitUrlConnectionAuthenticator.java
+++ b/http/src/main/java/io/split/android/client/network/SplitUrlConnectionAuthenticator.java
@@ -12,7 +12,7 @@ class SplitUrlConnectionAuthenticator {
}
HttpURLConnection authenticate(HttpURLConnection connection) {
- SplitAuthenticatedRequest authenticatedRequest = mProxyAuthenticator.authenticate(new SplitAuthenticatedRequest(connection));
+ AuthenticatedRequest authenticatedRequest = mProxyAuthenticator.authenticate(new SplitAuthenticatedRequest(connection));
if (authenticatedRequest != null) {
Map headers = authenticatedRequest.getHeaders();
diff --git a/main/src/main/java/io/split/android/client/network/SslProxyTunnelEstablisher.java b/http/src/main/java/io/split/android/client/network/SslProxyTunnelEstablisher.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/SslProxyTunnelEstablisher.java
rename to http/src/main/java/io/split/android/client/network/SslProxyTunnelEstablisher.java
diff --git a/main/src/main/java/io/split/android/client/network/Tls12OnlySocketFactory.java b/http/src/main/java/io/split/android/client/network/Tls12OnlySocketFactory.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/Tls12OnlySocketFactory.java
rename to http/src/main/java/io/split/android/client/network/Tls12OnlySocketFactory.java
diff --git a/http/src/main/java/io/split/android/client/network/TlsUpdater.java b/http/src/main/java/io/split/android/client/network/TlsUpdater.java
new file mode 100644
index 000000000..4fff431f4
--- /dev/null
+++ b/http/src/main/java/io/split/android/client/network/TlsUpdater.java
@@ -0,0 +1,14 @@
+package io.split.android.client.network;
+
+public interface TlsUpdater {
+
+ /**
+ * Return true if the device may need a TLS update.
+ */
+ boolean couldBeOld();
+
+ /**
+ * Perform the TLS update.
+ */
+ void update();
+}
diff --git a/main/src/main/java/io/split/android/client/network/TrustManagerProvider.java b/http/src/main/java/io/split/android/client/network/TrustManagerProvider.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/TrustManagerProvider.java
rename to http/src/main/java/io/split/android/client/network/TrustManagerProvider.java
diff --git a/main/src/main/java/io/split/android/client/network/URIBuilder.java b/http/src/main/java/io/split/android/client/network/URIBuilder.java
similarity index 76%
rename from main/src/main/java/io/split/android/client/network/URIBuilder.java
rename to http/src/main/java/io/split/android/client/network/URIBuilder.java
index e5aacc0e5..3611aaf27 100644
--- a/main/src/main/java/io/split/android/client/network/URIBuilder.java
+++ b/http/src/main/java/io/split/android/client/network/URIBuilder.java
@@ -1,25 +1,23 @@
package io.split.android.client.network;
-import static io.split.android.client.utils.Utils.checkNotNull;
+import static java.util.Objects.requireNonNull;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
-
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.AbstractMap;
import java.util.LinkedHashSet;
+import java.util.Map;
import java.util.Set;
-import io.split.android.client.utils.Utils;
-
public class URIBuilder {
private final URI mRootURI;
- private final Set> mParams;
+ private final Set> mParams;
private String mPath;
private String mQueryString;
public URIBuilder(@NonNull URI rootURI, String path) {
- mRootURI = checkNotNull(rootURI);
+ mRootURI = requireNonNull(rootURI);
String rootPath = mRootURI.getRawPath();
if (path != null && rootPath != null) {
mPath = String.format("%s/%s", rootPath, path);
@@ -40,13 +38,13 @@ public URIBuilder(@NonNull URI rootURI) {
public URIBuilder addParameter(@NonNull String param, @NonNull String value) {
if (param != null && value != null) {
- mParams.add(new Pair<>(param, value));
+ mParams.add(new AbstractMap.SimpleEntry<>(param, value));
}
return this;
}
public URIBuilder defaultQueryString(@NonNull String queryString) {
- if (!Utils.isNullOrEmpty(queryString)) {
+ if (queryString != null && !queryString.isEmpty()) {
mQueryString = queryString;
}
return this;
@@ -57,14 +55,14 @@ public URI build() throws URISyntaxException {
String params = null;
if (mParams.size() > 0) {
StringBuilder query = new StringBuilder();
- for (Pair param : mParams) {
- query.append(param.first).append("=").append(param.second).append("&");
+ for (Map.Entry param : mParams) {
+ query.append(param.getKey()).append("=").append(param.getValue()).append("&");
}
params = query.substring(0, query.length() - 1);
}
- if (!Utils.isNullOrEmpty(mQueryString)) {
- if (!Utils.isNullOrEmpty(params)) {
+ if (mQueryString != null && !mQueryString.isEmpty()) {
+ if (params != null && !params.isEmpty()) {
if (!"&".equals(mQueryString.substring(0, 1))) {
params = params + "&";
}
diff --git a/main/src/main/java/io/split/android/client/network/UnicodeEscaper.java b/http/src/main/java/io/split/android/client/network/UnicodeEscaper.java
similarity index 98%
rename from main/src/main/java/io/split/android/client/network/UnicodeEscaper.java
rename to http/src/main/java/io/split/android/client/network/UnicodeEscaper.java
index 4ed19ab54..7f3f6fd67 100644
--- a/main/src/main/java/io/split/android/client/network/UnicodeEscaper.java
+++ b/http/src/main/java/io/split/android/client/network/UnicodeEscaper.java
@@ -1,6 +1,6 @@
package io.split.android.client.network;
-import static io.split.android.client.utils.Utils.checkNotNull;
+import static java.util.Objects.requireNonNull;
/**
* Based on Guava UnicodeEscaper
@@ -14,7 +14,7 @@ protected UnicodeEscaper() {}
protected abstract char[] escape(int cp);
public String escape(String string) {
- checkNotNull(string);
+ requireNonNull(string);
int end = string.length();
int index = nextEscapeIndex(string, 0, end);
return index == end ? string : escapeSlow(string, index);
@@ -136,7 +136,7 @@ protected final String escapeSlow(String s, int index) {
* surrogate character at the end of the sequence
*/
protected static int codePointAt(CharSequence seq, int index, int end) {
- checkNotNull(seq);
+ requireNonNull(seq);
if (index < end) {
char c1 = seq.charAt(index++);
if (c1 < Character.MIN_HIGH_SURROGATE || c1 > Character.MAX_LOW_SURROGATE) {
diff --git a/main/src/main/java/io/split/android/client/network/UrlEscapers.java b/http/src/main/java/io/split/android/client/network/UrlEscapers.java
similarity index 98%
rename from main/src/main/java/io/split/android/client/network/UrlEscapers.java
rename to http/src/main/java/io/split/android/client/network/UrlEscapers.java
index d12a6f995..11098b8e0 100644
--- a/main/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 =
diff --git a/main/src/main/java/io/split/android/client/network/UrlSanitizer.java b/http/src/main/java/io/split/android/client/network/UrlSanitizer.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/UrlSanitizer.java
rename to http/src/main/java/io/split/android/client/network/UrlSanitizer.java
diff --git a/main/src/main/java/io/split/android/client/network/UrlSanitizerImpl.java b/http/src/main/java/io/split/android/client/network/UrlSanitizerImpl.java
similarity index 100%
rename from main/src/main/java/io/split/android/client/network/UrlSanitizerImpl.java
rename to http/src/main/java/io/split/android/client/network/UrlSanitizerImpl.java
diff --git a/main/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java b/http/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java
rename to http/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java
diff --git a/main/src/test/java/io/split/android/client/network/ChainCleanerImplTest.java b/http/src/test/java/io/split/android/client/network/ChainCleanerImplTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/ChainCleanerImplTest.java
rename to http/src/test/java/io/split/android/client/network/ChainCleanerImplTest.java
diff --git a/main/src/test/java/io/split/android/client/network/DefaultBase64EncoderTest.java b/http/src/test/java/io/split/android/client/network/DefaultBase64EncoderTest.java
similarity index 57%
rename from main/src/test/java/io/split/android/client/network/DefaultBase64EncoderTest.java
rename to http/src/test/java/io/split/android/client/network/DefaultBase64EncoderTest.java
index 738300ce7..ddbbc5078 100644
--- a/main/src/test/java/io/split/android/client/network/DefaultBase64EncoderTest.java
+++ b/http/src/test/java/io/split/android/client/network/DefaultBase64EncoderTest.java
@@ -2,6 +2,8 @@
import static org.mockito.Mockito.mockStatic;
+import android.util.Base64;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -9,39 +11,37 @@
import java.nio.charset.StandardCharsets;
-import io.split.android.client.utils.Base64Util;
-
public class DefaultBase64EncoderTest {
-
+
private DefaultBase64Encoder encoder;
- private MockedStatic mockedBase64Util;
-
+ private MockedStatic mockedBase64;
+
@Before
public void setUp() {
encoder = new DefaultBase64Encoder();
- mockedBase64Util = mockStatic(Base64Util.class);
+ mockedBase64 = mockStatic(Base64.class);
}
-
+
@After
public void tearDown() {
- mockedBase64Util.close();
+ mockedBase64.close();
}
-
+
@Test
- public void encodeStringUsesBase64Util() {
+ public void encodeStringUsesAndroidBase64() {
String input = "test string";
-
+
encoder.encode(input);
-
- mockedBase64Util.verify(() -> Base64Util.encode(input));
+
+ mockedBase64.verify(() -> Base64.encodeToString(input.getBytes(), Base64.NO_WRAP));
}
-
+
@Test
- public void encodeByteArrayUsesBase64Util() {
+ public void encodeByteArrayUsesAndroidBase64() {
byte[] input = "test bytes".getBytes(StandardCharsets.UTF_8);
-
+
encoder.encode(input);
-
- mockedBase64Util.verify(() -> Base64Util.encode(input));
+
+ mockedBase64.verify(() -> Base64.encodeToString(input, Base64.NO_WRAP));
}
}
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/test/java/io/split/android/client/network/HttpClientTunnellingProxyTest.java b/http/src/test/java/io/split/android/client/network/HttpClientTunnellingProxyTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/HttpClientTunnellingProxyTest.java
rename to http/src/test/java/io/split/android/client/network/HttpClientTunnellingProxyTest.java
diff --git a/main/src/test/java/io/split/android/client/network/HttpOverTunnelExecutorTest.java b/http/src/test/java/io/split/android/client/network/HttpOverTunnelExecutorTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/HttpOverTunnelExecutorTest.java
rename to http/src/test/java/io/split/android/client/network/HttpOverTunnelExecutorTest.java
diff --git a/main/src/test/java/io/split/android/client/network/HttpRequestHelperTest.java b/http/src/test/java/io/split/android/client/network/HttpRequestHelperTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/HttpRequestHelperTest.java
rename to http/src/test/java/io/split/android/client/network/HttpRequestHelperTest.java
diff --git a/main/src/test/java/io/split/android/client/network/HttpResponseConnectionAdapterTest.java b/http/src/test/java/io/split/android/client/network/HttpResponseConnectionAdapterTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/HttpResponseConnectionAdapterTest.java
rename to http/src/test/java/io/split/android/client/network/HttpResponseConnectionAdapterTest.java
diff --git a/main/src/test/java/io/split/android/client/network/HttpStreamResponseTest.java b/http/src/test/java/io/split/android/client/network/HttpStreamResponseTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/HttpStreamResponseTest.java
rename to http/src/test/java/io/split/android/client/network/HttpStreamResponseTest.java
diff --git a/main/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java b/http/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java
rename to http/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java
diff --git a/main/src/test/java/io/split/android/client/network/RawHttpResponseParserTest.java b/http/src/test/java/io/split/android/client/network/RawHttpResponseParserTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/RawHttpResponseParserTest.java
rename to http/src/test/java/io/split/android/client/network/RawHttpResponseParserTest.java
diff --git a/main/src/test/java/io/split/android/client/network/SplitAuthenticatorTest.java b/http/src/test/java/io/split/android/client/network/SplitAuthenticatorTest.java
similarity index 91%
rename from main/src/test/java/io/split/android/client/network/SplitAuthenticatorTest.java
rename to http/src/test/java/io/split/android/client/network/SplitAuthenticatorTest.java
index 3380c43a1..dae394a09 100644
--- a/main/src/test/java/io/split/android/client/network/SplitAuthenticatorTest.java
+++ b/http/src/test/java/io/split/android/client/network/SplitAuthenticatorTest.java
@@ -18,9 +18,9 @@ public class SplitAuthenticatorTest {
@Test
public void authenticatorModifiesHeaders() {
- Authenticator> splitAuthenticator = new Authenticator>() {
+ Authenticator splitAuthenticator = new Authenticator() {
@Override
- public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) {
+ public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) {
request.setHeader("new-header", "value");
return request;
@@ -48,7 +48,7 @@ public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequ
assertEquals("value", finalHeaders.get("new-header"));
}
- private static class AuthenticatedMockRequest implements AuthenticatedRequest {
+ private static class AuthenticatedMockRequest implements AuthenticatedRequest {
private final MockRequest mRequest;
diff --git a/main/src/test/java/io/split/android/client/network/SplitBasicAuthenticatorTest.java b/http/src/test/java/io/split/android/client/network/SplitBasicAuthenticatorTest.java
similarity index 91%
rename from main/src/test/java/io/split/android/client/network/SplitBasicAuthenticatorTest.java
rename to http/src/test/java/io/split/android/client/network/SplitBasicAuthenticatorTest.java
index 7b56e0291..5b0f27531 100644
--- a/main/src/test/java/io/split/android/client/network/SplitBasicAuthenticatorTest.java
+++ b/http/src/test/java/io/split/android/client/network/SplitBasicAuthenticatorTest.java
@@ -29,7 +29,7 @@ public void callingAuthenticateUsesEncoder() {
@Test
public void callingAuthenticateReturnsCorrectHeaderInRequest() {
SplitBasicAuthenticator authenticator = new SplitBasicAuthenticator("user", "pass", mBase64Encoder);
- SplitAuthenticatedRequest request = authenticator.authenticate(mock(SplitAuthenticatedRequest.class));
+ AuthenticatedRequest request = authenticator.authenticate(mock(SplitAuthenticatedRequest.class));
verify(request).setHeader("Proxy-Authorization", "Basic user:pass");
}
diff --git a/main/src/test/java/io/split/android/client/network/SplitUrlConnectionAuthenticatorTest.java b/http/src/test/java/io/split/android/client/network/SplitUrlConnectionAuthenticatorTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/SplitUrlConnectionAuthenticatorTest.java
rename to http/src/test/java/io/split/android/client/network/SplitUrlConnectionAuthenticatorTest.java
diff --git a/main/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java b/http/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java
rename to http/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java
diff --git a/main/src/test/java/io/split/android/client/network/TrustManagerProviderTest.java b/http/src/test/java/io/split/android/client/network/TrustManagerProviderTest.java
similarity index 100%
rename from main/src/test/java/io/split/android/client/network/TrustManagerProviderTest.java
rename to http/src/test/java/io/split/android/client/network/TrustManagerProviderTest.java
diff --git a/main/build.gradle b/main/build.gradle
index 7ec1e3110..63f91db29 100644
--- a/main/build.gradle
+++ b/main/build.gradle
@@ -52,7 +52,9 @@ dependencies {
// Public api modules
api project(':logger')
api project(':api')
+ api project(':http-api')
// Internal module dependencies
+ implementation project(':http')
implementation project(':events-domain')
// External dependencies
diff --git a/main/src/androidTest/java/tests/integration/ProxyFactoryTest.java b/main/src/androidTest/java/tests/integration/ProxyFactoryTest.java
index 28f80e19d..cfbe02240 100644
--- a/main/src/androidTest/java/tests/integration/ProxyFactoryTest.java
+++ b/main/src/androidTest/java/tests/integration/ProxyFactoryTest.java
@@ -27,7 +27,7 @@
import io.split.android.client.SplitFactoryBuilder;
import io.split.android.client.api.Key;
import io.split.android.client.events.SplitEvent;
-import io.split.android.client.network.SplitAuthenticatedRequest;
+import io.split.android.client.network.AuthenticatedRequest;
import io.split.android.client.network.SplitAuthenticator;
import io.split.android.client.service.impressions.ImpressionsMode;
import io.split.android.client.service.synchronizer.ThreadUtils;
@@ -248,7 +248,7 @@ public MockResponse dispatch(RecordedRequest request) {
.serviceEndpoints(endpoints)
.proxyAuthenticator(new SplitAuthenticator() {
@Override
- public SplitAuthenticatedRequest authenticate(@NonNull SplitAuthenticatedRequest request) {
+ public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) {
request.setHeader("Proxy-Authorization", "Bearer 1234567890");
return request;
}
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 8bb12d71f..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;
@@ -32,7 +34,9 @@
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;
import io.split.android.client.service.SplitApiFacade;
import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor;
@@ -381,20 +385,12 @@ 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())
- .setContext(context)
- .setProxyAuthenticator(config.authenticator());
- if (config.proxy() != null) {
- builder.setProxy(config.proxy());
- }
- if (config.certificatePinningConfiguration() != null) {
- builder.setCertificatePinningConfiguration(config.certificatePinningConfiguration());
- }
+ HttpClientConfiguration httpConfig = buildHttpClientConfiguration(config);
- defaultHttpClient = builder.build();
+ defaultHttpClient = new HttpClientImpl.Builder()
+ .setConfiguration(httpConfig)
+ .setTlsUpdater(new LegacyTlsUpdaterAdapter(context))
+ .build();
// This should be extracted; has nothing to do with the method.
if (config.proxy() != null && generalInfoStorage != null) {
@@ -411,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/main/java/io/split/android/client/network/Algorithm.java b/main/src/main/java/io/split/android/client/network/Algorithm.java
deleted file mode 100644
index 2e193751f..000000000
--- a/main/src/main/java/io/split/android/client/network/Algorithm.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package io.split.android.client.network;
-
-class Algorithm {
-
- static final String SHA256 = "sha256";
- static final String SHA1 = "sha1";
-}
diff --git a/main/src/main/java/io/split/android/client/network/Authenticator.java b/main/src/main/java/io/split/android/client/network/Authenticator.java
deleted file mode 100644
index c23a39994..000000000
--- a/main/src/main/java/io/split/android/client/network/Authenticator.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package io.split.android.client.network;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-interface Authenticator> {
-
- @Nullable T authenticate(@NonNull T request);
-}
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/DefaultBase64Decoder.java b/main/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java
deleted file mode 100644
index c84903fb6..000000000
--- a/main/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.split.android.client.network;
-
-import io.split.android.client.utils.Base64Util;
-
-class DefaultBase64Decoder implements Base64Decoder {
-
- @Override
- public byte[] decode(String base64) {
- return Base64Util.bytesDecode(base64);
- }
-}
diff --git a/main/src/main/java/io/split/android/client/network/DefaultBase64Encoder.java b/main/src/main/java/io/split/android/client/network/DefaultBase64Encoder.java
deleted file mode 100644
index e1333ca80..000000000
--- a/main/src/main/java/io/split/android/client/network/DefaultBase64Encoder.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package io.split.android.client.network;
-
-import io.split.android.client.utils.Base64Util;
-
-class DefaultBase64Encoder implements Base64Encoder {
-
- @Override
- public String encode(String value) {
- return Base64Util.encode(value);
- }
-
- @Override
- public String encode(byte[] bytes) {
- return Base64Util.encode(bytes);
- }
-}
diff --git a/main/src/main/java/io/split/android/client/network/LegacyTlsUpdaterAdapter.java b/main/src/main/java/io/split/android/client/network/LegacyTlsUpdaterAdapter.java
new file mode 100644
index 000000000..162fcee9b
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/network/LegacyTlsUpdaterAdapter.java
@@ -0,0 +1,29 @@
+package io.split.android.client.network;
+
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Adapter that bridges the :http module's {@link TlsUpdater} interface with the
+ * :main module's {@link LegacyTlsUpdater} class.
+ */
+public class LegacyTlsUpdaterAdapter implements TlsUpdater {
+
+ @Nullable
+ private final Context mContext;
+
+ public LegacyTlsUpdaterAdapter(@Nullable Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public boolean couldBeOld() {
+ return LegacyTlsUpdater.couldBeOld();
+ }
+
+ @Override
+ public void update() {
+ LegacyTlsUpdater.update(mContext);
+ }
+}
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/main/java/io/split/android/client/utils/Utils.java b/main/src/main/java/io/split/android/client/utils/Utils.java
index 8341d776c..ff8e7d4eb 100644
--- a/main/src/main/java/io/split/android/client/utils/Utils.java
+++ b/main/src/main/java/io/split/android/client/utils/Utils.java
@@ -55,14 +55,6 @@ public static void checkArgument(boolean expression) {
}
}
- public static int getAsInt(long value) {
- if (value > Integer.MAX_VALUE) {
- return Integer.MAX_VALUE;
- } else {
- return (int) value;
- }
- }
-
public static List> partition(List list, int size) {
if (list == null) {
return new ArrayList<>();
diff --git a/main/src/test/java/io/split/android/client/SplitClientConfigTest.java b/main/src/test/java/io/split/android/client/SplitClientConfigTest.java
index b97ea6381..4163818ed 100644
--- a/main/src/test/java/io/split/android/client/SplitClientConfigTest.java
+++ b/main/src/test/java/io/split/android/client/SplitClientConfigTest.java
@@ -16,6 +16,7 @@
import java.util.concurrent.TimeUnit;
import io.split.android.client.fallback.FallbackTreatmentsConfiguration;
+import io.split.android.client.network.AuthenticatedRequest;
import io.split.android.client.network.CertificatePinningConfiguration;
import io.split.android.client.network.ProxyConfiguration;
import io.split.android.client.network.SplitAuthenticatedRequest;
@@ -298,7 +299,7 @@ public void proxyAuthenticatorAndProxyConfigurationSetLogWarning() {
.proxyAuthenticator(new SplitAuthenticator() {
@Nullable
@Override
- public SplitAuthenticatedRequest authenticate(@NonNull SplitAuthenticatedRequest request) {
+ public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) {
return null;
}
})
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());
+ }
+}
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