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..e4dbec6f2
--- /dev/null
+++ b/http/.gitignore
@@ -0,0 +1,3 @@
+/build
+.classpath
+.settings
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..41bfe00c5
--- /dev/null
+++ b/http/build.gradle
@@ -0,0 +1,26 @@
+plugins {
+ id 'com.android.library'
+}
+
+apply from: "$projectDir/../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 clientModuleProject('logger')
+ api clientModuleProject('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/logger/.gitignore b/logger/.gitignore
index 3a11ced48..6009265cd 100644
--- a/logger/.gitignore
+++ b/logger/.gitignore
@@ -2,3 +2,5 @@
.gradle
*.iml
.DS_Store
+.classpath
+.settings
diff --git a/logger/build.gradle b/logger/build.gradle
index a45c8cd36..0cc88bb53 100644
--- a/logger/build.gradle
+++ b/logger/build.gradle
@@ -2,7 +2,7 @@ plugins {
id 'com.android.library'
}
-apply from: "$rootDir/gradle/common-android-library.gradle"
+apply from: "$projectDir/../gradle/common-android-library.gradle"
android {
namespace 'io.split.android.client.logger'
diff --git a/main/.gitignore b/main/.gitignore
index 3a11ced48..6009265cd 100644
--- a/main/.gitignore
+++ b/main/.gitignore
@@ -2,3 +2,5 @@
.gradle
*.iml
.DS_Store
+.classpath
+.settings
diff --git a/main/build.gradle b/main/build.gradle
index 7ec1e3110..6d1432030 100644
--- a/main/build.gradle
+++ b/main/build.gradle
@@ -2,14 +2,14 @@ plugins {
id 'com.android.library'
}
-apply from: "$rootDir/gradle/common-android-library.gradle"
+apply from: "$projectDir/../gradle/common-android-library.gradle"
android {
namespace 'io.split.android.client.main'
defaultConfig {
multiDexEnabled true
- consumerProguardFiles "$rootDir/split-proguard-rules.pro"
+ consumerProguardFiles "$projectDir/../split-proguard-rules.pro"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
@@ -50,10 +50,20 @@ android {
dependencies {
// Public api modules
- api project(':logger')
- api project(':api')
+ api clientModuleProject('executor')
+ api clientModuleProject('logger')
+ api clientModuleProject('api')
+ api clientModuleProject('http-api')
+ api clientModuleProject('fallback')
+ implementation clientModuleProject('backoff')
+ implementation clientModuleProject('tracker')
+ api clientModuleProject('submitter')
+
// Internal module dependencies
- implementation project(':events-domain')
+ implementation clientModuleProject(':http')
+ implementation clientModuleProject(':events-domain')
+ implementation clientModuleProject(':streaming')
+ implementation clientModuleProject(':streaming-support')
// External dependencies
implementation libs.roomRuntime
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/androidTest/java/tests/integration/rollout/FreshInstallPrefetchPersistenceIntegrationTest.java b/main/src/androidTest/java/tests/integration/rollout/FreshInstallPrefetchPersistenceIntegrationTest.java
new file mode 100644
index 000000000..3f18664b6
--- /dev/null
+++ b/main/src/androidTest/java/tests/integration/rollout/FreshInstallPrefetchPersistenceIntegrationTest.java
@@ -0,0 +1,131 @@
+package tests.integration.rollout;
+
+import static helper.IntegrationHelper.dummyApiKey;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import helper.DatabaseHelper;
+import io.split.android.client.SplitFilter;
+import io.split.android.client.dtos.Split;
+import io.split.android.client.dtos.SplitChange;
+import io.split.android.client.dtos.Status;
+import io.split.android.client.service.splits.SplitChangeProcessor;
+import io.split.android.client.storage.cipher.SplitCipher;
+import io.split.android.client.storage.cipher.SplitCipherFactory;
+import io.split.android.client.storage.db.SplitRoomDatabase;
+import io.split.android.client.storage.splits.PersistentSplitsStorage;
+import io.split.android.client.storage.splits.SplitsStorage;
+import io.split.android.client.storage.splits.SplitsStorageImpl;
+import io.split.android.client.storage.splits.SqLitePersistentSplitsStorage;
+
+public class FreshInstallPrefetchPersistenceIntegrationTest {
+
+ private static final long CHANGE_NUMBER = 1778482333302L;
+ private static final int SPLIT_COUNT_OVER_ASYNC_THRESHOLD = 60;
+ private static final String FIRST_FLAG_NAME = "fresh_install_flag_0";
+
+ private SplitRoomDatabase mRoomDb;
+ private PersistentSplitsStorage mPersistentStorage;
+ private SplitChangeProcessor mSplitChangeProcessor;
+
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ mRoomDb = DatabaseHelper.getTestDatabase(context);
+ mRoomDb.clearAllTables();
+
+ SplitCipher cipher = SplitCipherFactory.create(dummyApiKey(), false);
+ mPersistentStorage = new SqLitePersistentSplitsStorage(mRoomDb, cipher);
+ mSplitChangeProcessor = new SplitChangeProcessor((Map) null, null);
+ }
+
+ @Test
+ public void processKillBeforeAsyncWriteCompletes_dbRemainsConsistent() throws InterruptedException {
+ // Block the executor so the first write doesn't complete
+ CountDownLatch blockLatch = new CountDownLatch(1);
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ executor.submit(() -> {
+ try {
+ blockLatch.await();
+ } catch (InterruptedException e) {
+ // shutdownNow will interrupt this
+ }
+ });
+
+ SplitsStorage storage = new SplitsStorageImpl(mPersistentStorage);
+
+ // First update queues behind the blocked task
+ storage.update(
+ mSplitChangeProcessor.process(SplitChange.create(-1, CHANGE_NUMBER, createSplits())),
+ executor);
+
+ // Simulate process kill — first write never completes
+ executor.shutdownNow();
+ executor.awaitTermination(1, TimeUnit.SECONDS);
+
+ // Second update (empty delta) — submit is rejected since executor is shut down
+ storage.update(
+ mSplitChangeProcessor.process(SplitChange.create(CHANGE_NUMBER, CHANGE_NUMBER, new ArrayList<>())),
+ executor);
+
+ // DB should be untouched — no partial CN write
+ SplitsStorage reloadedStorage = new SplitsStorageImpl(mPersistentStorage);
+ reloadedStorage.loadLocal();
+
+ assertEquals(-1, reloadedStorage.getTill());
+ assertEquals(0, mRoomDb.splitDao().getAll().size());
+ }
+
+ @Test
+ public void fullSnapshotAndEmptyDeltaPersistCorrectlyWhenExecutorIsRunning() throws InterruptedException {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ SplitsStorage storage = new SplitsStorageImpl(mPersistentStorage);
+
+ storage.update(
+ mSplitChangeProcessor.process(SplitChange.create(-1, CHANGE_NUMBER, createSplits())),
+ executor);
+ storage.update(
+ mSplitChangeProcessor.process(SplitChange.create(CHANGE_NUMBER, CHANGE_NUMBER, new ArrayList<>())),
+ executor);
+
+ executor.shutdown();
+ executor.awaitTermination(5, TimeUnit.SECONDS);
+
+ SplitsStorage reloadedStorage = new SplitsStorageImpl(mPersistentStorage);
+ reloadedStorage.loadLocal();
+
+ assertEquals(CHANGE_NUMBER, reloadedStorage.getTill());
+ assertEquals(SPLIT_COUNT_OVER_ASYNC_THRESHOLD, mRoomDb.splitDao().getAll().size());
+ assertNotNull(reloadedStorage.get(FIRST_FLAG_NAME));
+ }
+
+ private static List createSplits() {
+ List splits = new ArrayList<>();
+ for (int i = 0; i < SPLIT_COUNT_OVER_ASYNC_THRESHOLD; i++) {
+ Split split = new Split();
+ split.name = "fresh_install_flag_" + i;
+ split.status = Status.ACTIVE;
+ split.changeNumber = CHANGE_NUMBER;
+ split.trafficTypeName = "user";
+ split.defaultTreatment = "on";
+ split.killed = false;
+ splits.add(split);
+ }
+ return splits;
+ }
+}
diff --git a/main/src/androidTest/java/tests/service/CompressionTest.java b/main/src/androidTest/java/tests/service/CompressionTest.java
index 1d5dbd15c..40a4a20aa 100644
--- a/main/src/androidTest/java/tests/service/CompressionTest.java
+++ b/main/src/androidTest/java/tests/service/CompressionTest.java
@@ -8,18 +8,17 @@
import org.junit.Before;
import org.junit.Test;
-import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import helper.CompressionHelper;
import helper.FileHelper;
+import io.split.android.client.streaming.support.CompressionUtil;
+import io.split.android.client.streaming.support.Gzip;
+import io.split.android.client.streaming.support.Zlib;
import io.split.android.client.utils.Base64Util;
-import io.split.android.client.utils.CompressionUtil;
-import io.split.android.client.utils.Gzip;
import io.split.android.client.utils.StringHelper;
-import io.split.android.client.utils.Zlib;
public class CompressionTest {
diff --git a/main/src/androidTest/java/tests/service/MySegmentV2PayloadDecoderTest.java b/main/src/androidTest/java/tests/service/MySegmentV2PayloadDecoderTest.java
index ebdf5057a..f9f139db0 100644
--- a/main/src/androidTest/java/tests/service/MySegmentV2PayloadDecoderTest.java
+++ b/main/src/androidTest/java/tests/service/MySegmentV2PayloadDecoderTest.java
@@ -17,9 +17,8 @@
import io.split.android.client.service.sseclient.notifications.KeyList;
import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder;
import io.split.android.client.service.sseclient.notifications.NotificationParser;
-import io.split.android.client.utils.Gzip;
-import io.split.android.client.utils.MurmurHash3;
-import io.split.android.client.utils.Zlib;
+import io.split.android.client.streaming.support.Gzip;
+import io.split.android.client.streaming.support.Zlib;
public class MySegmentV2PayloadDecoderTest {
diff --git a/main/src/androidTest/java/tests/storage/SplitsStorageTest.java b/main/src/androidTest/java/tests/storage/SplitsStorageTest.java
index 7922a6126..e8fd2bd4c 100644
--- a/main/src/androidTest/java/tests/storage/SplitsStorageTest.java
+++ b/main/src/androidTest/java/tests/storage/SplitsStorageTest.java
@@ -2,6 +2,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -20,7 +21,10 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Queue;
import java.util.Set;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -32,7 +36,9 @@
import io.split.android.client.storage.db.GeneralInfoEntity;
import io.split.android.client.storage.db.SplitEntity;
import io.split.android.client.storage.db.SplitRoomDatabase;
+import io.split.android.client.storage.splits.PersistentSplitsStorage;
import io.split.android.client.storage.splits.ProcessedSplitChange;
+import io.split.android.client.storage.splits.SplitsSnapshot;
import io.split.android.client.storage.splits.SplitsStorage;
import io.split.android.client.storage.splits.SplitsStorageImpl;
import io.split.android.client.storage.splits.SqLitePersistentSplitsStorage;
@@ -534,6 +540,34 @@ public void nullFlagsSpecValueIsValid() {
assertEquals("", flagsSpec);
}
+ @Test
+ public void asyncPersistentUpdateReceivesMetadataSnapshot() {
+ CapturingPersistentSplitsStorage persistentStorage = new CapturingPersistentSplitsStorage();
+ ControlledExecutorService executor = new ControlledExecutorService();
+ SplitsStorage splitsStorage = new SplitsStorageImpl(persistentStorage);
+
+ splitsStorage.update(
+ new ProcessedSplitChange(
+ Collections.singletonList(newSplit("split_1", Status.ACTIVE, "type_1", Collections.singleton("set_1"))),
+ Collections.emptyList(),
+ 1L,
+ 0L),
+ executor);
+ splitsStorage.update(
+ new ProcessedSplitChange(
+ Collections.singletonList(newSplit("split_2", Status.ACTIVE, "type_2", Collections.singleton("set_2"))),
+ Collections.emptyList(),
+ 2L,
+ 0L),
+ executor);
+
+ executor.runNext();
+
+ assertEquals(Collections.singletonMap("type_1", 1), persistentStorage.lastTrafficTypes);
+ assertEquals(Collections.singleton("split_1"), persistentStorage.lastFlagSets.get("set_1"));
+ assertNull(persistentStorage.lastFlagSets.get("set_2"));
+ }
+
private Split newSplit(String name, Status status, String trafficType) {
return newSplit(name, status, trafficType, Collections.emptySet());
}
@@ -564,4 +598,108 @@ private static SplitEntity newSplitEntity(String name, String trafficType, Set tasks = new ConcurrentLinkedQueue<>();
+
+ void runNext() {
+ Runnable task = tasks.poll();
+ assertNotNull(task);
+ task.run();
+ }
+
+ @Override
+ public void shutdown() {
+ }
+
+ @Override
+ public List shutdownNow() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return false;
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return false;
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) {
+ return false;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ tasks.add(command);
+ }
+ }
+
+ private static class CapturingPersistentSplitsStorage implements PersistentSplitsStorage {
+ Map lastTrafficTypes;
+ Map> lastFlagSets;
+
+ @Override
+ public boolean update(ProcessedSplitChange splitChange, Map trafficTypes, Map> flagSets) {
+ lastTrafficTypes = new HashMap<>(trafficTypes);
+ lastFlagSets = new HashMap<>();
+ for (Map.Entry> entry : flagSets.entrySet()) {
+ lastFlagSets.put(entry.getKey(), new HashSet<>(entry.getValue()));
+ }
+ return true;
+ }
+
+ @Override
+ public SplitsSnapshot getSnapshot() {
+ return new SplitsSnapshot(Collections.emptyList(), -1L, 0L, "", "", Collections.emptyMap(), Collections.emptyMap());
+ }
+
+ @Override
+ public List getAll() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void update(Split splitName) {
+ // no-op
+ }
+
+ @Override
+ public String getFilterQueryString() {
+ return "";
+ }
+
+ @Override
+ public void updateFilterQueryString(String queryString) {
+ // no-op
+ }
+
+ @Override
+ public String getFlagsSpec() {
+ return "";
+ }
+
+ @Override
+ public void updateFlagsSpec(String flagsSpec) {
+ // no-op
+ }
+
+ @Override
+ public void delete(List splitNames) {
+ // no-op
+ }
+
+ @Override
+ public void clear() {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+ }
}
diff --git a/main/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java b/main/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java
index 8f3539423..b2865386f 100644
--- a/main/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java
+++ b/main/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java
@@ -34,7 +34,7 @@
import io.split.android.client.SplitClientConfig;
import io.split.android.client.SplitFilter;
import io.split.android.client.network.CertificatePinningConfiguration;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.synchronizer.WorkManagerWrapper;
import io.split.android.client.service.workmanager.EventsRecorderWorker;
import io.split.android.client.service.workmanager.ImpressionsRecorderWorker;
diff --git a/main/src/main/java/io/split/android/client/EventsTracker.java b/main/src/main/java/io/split/android/client/EventsTracker.java
deleted file mode 100644
index 800b8c0c2..000000000
--- a/main/src/main/java/io/split/android/client/EventsTracker.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package io.split.android.client;
-
-import java.util.Map;
-
-public interface EventsTracker {
- void enableTracking(boolean enable);
- boolean track(String key, String trafficType, String eventType, double value, Map properties, boolean isSdkReady);
-}
\ No newline at end of file
diff --git a/main/src/main/java/io/split/android/client/EventsTrackerImpl.java b/main/src/main/java/io/split/android/client/EventsTrackerImpl.java
deleted file mode 100644
index 0b8d18982..000000000
--- a/main/src/main/java/io/split/android/client/EventsTrackerImpl.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package io.split.android.client;
-
-import static io.split.android.client.utils.Utils.checkNotNull;
-
-import androidx.annotation.NonNull;
-
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import io.split.android.client.dtos.Event;
-import io.split.android.client.service.synchronizer.SyncManager;
-import io.split.android.client.telemetry.model.Method;
-import io.split.android.client.telemetry.storage.TelemetryStorageProducer;
-import io.split.android.client.utils.logger.Logger;
-import io.split.android.client.validators.EventValidator;
-import io.split.android.client.validators.PropertyValidator;
-import io.split.android.client.validators.ValidationErrorInfo;
-import io.split.android.client.validators.ValidationMessageLogger;
-
-public class EventsTrackerImpl implements EventsTracker {
- // Estimated event size without properties
- private final static int ESTIMATED_EVENT_SIZE_WITHOUT_PROPS = 1024;
-
- private final EventValidator mEventValidator;
- private final ValidationMessageLogger mValidationLogger;
- private final TelemetryStorageProducer mTelemetryStorageProducer;
- private final PropertyValidator mPropertyValidator;
- private final SyncManager mSyncManager;
- private final AtomicBoolean isTrackingEnabled = new AtomicBoolean(true);
-
- public EventsTrackerImpl(@NonNull EventValidator eventValidator,
- @NonNull ValidationMessageLogger validationLogger,
- @NonNull TelemetryStorageProducer telemetryStorageProducer,
- @NonNull PropertyValidator eventPropertiesProcessor,
- @NonNull SyncManager syncManager) {
-
- mEventValidator = checkNotNull(eventValidator);
- mValidationLogger = checkNotNull(validationLogger);
- mTelemetryStorageProducer = checkNotNull(telemetryStorageProducer);
- mPropertyValidator = checkNotNull(eventPropertiesProcessor);
- mSyncManager = checkNotNull(syncManager);
- }
-
- public void enableTracking(boolean enable) {
- isTrackingEnabled.set(enable);
- }
-
- public boolean track(String key, String trafficType, String eventType,
- double value, Map properties, boolean isSdkReady) {
-
- if (!isTrackingEnabled.get()) {
- Logger.v("Event not tracked because tracking is disabled");
- return false;
- }
-
- try {
- final String validationTag = "track";
-
- Event event = new Event();
- event.eventTypeId = eventType;
- event.trafficTypeName = trafficType;
- event.key = key;
- event.value = value;
- event.timestamp = System.currentTimeMillis();
- event.properties = properties;
-
- ValidationErrorInfo errorInfo = mEventValidator.validate(event, isSdkReady);
- if (errorInfo != null) {
-
- if (errorInfo.isError()) {
- mValidationLogger.e(errorInfo, validationTag);
- return false;
- }
- mValidationLogger.w(errorInfo, validationTag);
- event.trafficTypeName = event.trafficTypeName.toLowerCase();
- }
-
- PropertyValidator.Result processedProperties =
- mPropertyValidator.validate(event.properties, validationTag);
- if (!processedProperties.isValid()) {
- return false;
- }
-
- long startTime = System.currentTimeMillis();
-
- event.properties = processedProperties.getProperties();
- event.setSizeInBytes(ESTIMATED_EVENT_SIZE_WITHOUT_PROPS + processedProperties.getSizeInBytes());
- mSyncManager.pushEvent(event);
-
- mTelemetryStorageProducer.recordLatency(Method.TRACK, System.currentTimeMillis() - startTime);
-
- return true;
- } catch (Exception exception) {
- mTelemetryStorageProducer.recordException(Method.TRACK);
- }
- return false;
- }
-}
diff --git a/main/src/main/java/io/split/android/client/PropertyValidatorImpl.java b/main/src/main/java/io/split/android/client/PropertyValidatorImpl.java
deleted file mode 100644
index 01cc06ef6..000000000
--- a/main/src/main/java/io/split/android/client/PropertyValidatorImpl.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package io.split.android.client;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import io.split.android.client.utils.logger.Logger;
-import io.split.android.client.validators.PropertyValidator;
-import io.split.android.client.validators.ValidationConfig;
-
-
-public class PropertyValidatorImpl implements PropertyValidator {
-
- private final static int MAX_PROPS_COUNT = 300;
- private final static int MAXIMUM_EVENT_PROPERTY_BYTES =
- ValidationConfig.getInstance().getMaximumEventPropertyBytes();
-
- @Override
- public Result validate(Map properties, String validationTag) {
- if (properties == null) {
- return Result.valid(null, 0);
- }
-
- if (properties.size() > MAX_PROPS_COUNT) {
- Logger.w(validationTag + "Event has more than " + MAX_PROPS_COUNT +
- " properties. Some of them will be trimmed when processed");
- }
- int sizeInBytes = 0;
- Map finalProperties = new HashMap<>(properties);
-
- for (Map.Entry entry : properties.entrySet()) {
- Object value = entry.getValue();
- String key = entry.getKey();
-
- if (value != null && isInvalidValueType(value)) {
- finalProperties.put(key, null);
- }
- sizeInBytes += calculateEventSizeInBytes(key, value);
-
- if (sizeInBytes > MAXIMUM_EVENT_PROPERTY_BYTES) {
- Logger.w(validationTag +
- "The maximum size allowed for the " +
- " properties is 32kb. Current is " + key +
- ". Event not queued");
- return Result.invalid("Event properties size is too large", sizeInBytes);
- }
- }
- return Result.valid(finalProperties, sizeInBytes);
- }
-
- private static boolean isInvalidValueType(Object value) {
- return !(value instanceof Number) &&
- !(value instanceof Boolean) &&
- !(value instanceof String);
- }
-
- private static int calculateEventSizeInBytes(String key, Object value) {
- int valueSize = 0;
- if(value != null && value.getClass() == String.class) {
- valueSize = value.toString().getBytes().length;
- }
- return valueSize + key.getBytes().length;
- }
-}
diff --git a/main/src/main/java/io/split/android/client/RetryBackoffCounterTimerFactory.java b/main/src/main/java/io/split/android/client/RetryBackoffCounterTimerFactory.java
index 7e2ec611d..6c0154549 100644
--- a/main/src/main/java/io/split/android/client/RetryBackoffCounterTimerFactory.java
+++ b/main/src/main/java/io/split/android/client/RetryBackoffCounterTimerFactory.java
@@ -1,13 +1,13 @@
package io.split.android.client;
import io.split.android.client.service.executor.SplitTaskExecutor;
-import io.split.android.client.service.sseclient.FixedIntervalBackoffCounter;
-import io.split.android.client.service.sseclient.ReconnectBackoffCounter;
+import io.split.android.client.backoff.FixedIntervalBackoffCounter;
+import io.split.android.client.backoff.ExponentialBackoffCounter;
import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer;
public class RetryBackoffCounterTimerFactory {
public RetryBackoffCounterTimer create(SplitTaskExecutor splitTaskExecutor, int base) {
- return new RetryBackoffCounterTimer(splitTaskExecutor, new ReconnectBackoffCounter(base));
+ return new RetryBackoffCounterTimer(splitTaskExecutor, new ExponentialBackoffCounter(base));
}
public RetryBackoffCounterTimer createWithFixedInterval(SplitTaskExecutor splitTaskExecutor, int retryIntervalInSeconds, int maxAttempts) {
diff --git a/main/src/main/java/io/split/android/client/SplitClientConfig.java b/main/src/main/java/io/split/android/client/SplitClientConfig.java
index 0d5b0ecc8..3fd0a845d 100644
--- a/main/src/main/java/io/split/android/client/SplitClientConfig.java
+++ b/main/src/main/java/io/split/android/client/SplitClientConfig.java
@@ -13,9 +13,9 @@
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
-import io.split.android.client.main.BuildConfig;
import io.split.android.client.impressions.ImpressionListener;
import io.split.android.client.network.CertificatePinningConfiguration;
+import io.split.android.client.network.SdkVersionProvider;
import io.split.android.client.network.DevelopmentSslConfig;
import io.split.android.client.network.HttpProxy;
import io.split.android.client.network.ProxyConfiguration;
@@ -242,7 +242,7 @@ private SplitClientConfig(String endpoint,
mUserConsent = userConsent;
- splitSdkVersion = "Android-" + BuildConfig.SPLIT_VERSION_NAME;
+ splitSdkVersion = SdkVersionProvider.getSdkVersion();
mShouldRecordTelemetry = shouldRecordTelemetry;
diff --git a/main/src/main/java/io/split/android/client/SplitClientImpl.java b/main/src/main/java/io/split/android/client/SplitClientImpl.java
index 571efa169..c3795a416 100644
--- a/main/src/main/java/io/split/android/client/SplitClientImpl.java
+++ b/main/src/main/java/io/split/android/client/SplitClientImpl.java
@@ -18,6 +18,7 @@
import io.split.android.client.events.SplitEventsManager;
import io.split.android.client.impressions.ImpressionListener;
import io.split.android.client.shared.SplitClientContainer;
+import io.split.android.client.tracker.Tracker;
import io.split.android.client.utils.logger.Logger;
import io.split.android.client.validators.SplitValidator;
import io.split.android.client.validators.TreatmentManager;
@@ -35,7 +36,7 @@ public final class SplitClientImpl implements SplitClient {
private final TreatmentManager mTreatmentManager;
private final ValidationMessageLogger mValidationLogger;
private final AttributesManager mAttributesManager;
- private final EventsTracker mEventsTracker;
+ private final Tracker mEventsTracker;
private static final double TRACK_DEFAULT_VALUE = 0.0;
@@ -48,7 +49,7 @@ public SplitClientImpl(SplitFactory container,
ImpressionListener impressionListener,
SplitClientConfig config,
SplitEventsManager eventsManager,
- EventsTracker eventsTracker,
+ Tracker eventsTracker,
AttributesManager attributesManager,
SplitValidator splitValidator,
TreatmentManager treatmentManager) {
diff --git a/main/src/main/java/io/split/android/client/SplitFactoryHelper.java b/main/src/main/java/io/split/android/client/SplitFactoryHelper.java
index 4360c873c..8cfa6771c 100644
--- a/main/src/main/java/io/split/android/client/SplitFactoryHelper.java
+++ b/main/src/main/java/io/split/android/client/SplitFactoryHelper.java
@@ -22,7 +22,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
-import io.split.android.client.common.CompressionUtilProvider;
+import io.split.android.client.streaming.support.CompressionUtilProvider;
import io.split.android.client.events.EventsManagerCoordinator;
import io.split.android.client.events.SplitInternalEvent;
import io.split.android.client.lifecycle.SplitLifecycleManager;
@@ -42,7 +42,7 @@
import io.split.android.client.service.impressions.strategy.ImpressionStrategyProvider;
import io.split.android.client.service.mysegments.AllSegmentsResponseParser;
import io.split.android.client.service.sseclient.EventStreamParser;
-import io.split.android.client.service.sseclient.ReconnectBackoffCounter;
+import io.split.android.client.backoff.ExponentialBackoffCounter;
import io.split.android.client.service.sseclient.SseJwtParser;
import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster;
import io.split.android.client.service.sseclient.notifications.InstantUpdateChangeNotification;
@@ -54,13 +54,22 @@
import io.split.android.client.service.sseclient.reactor.MySegmentsUpdateWorkerRegistry;
import io.split.android.client.service.sseclient.reactor.SplitUpdatesWorker;
import io.split.android.client.service.sseclient.sseclient.BackoffCounterTimer;
+import io.split.android.client.service.sseclient.sseclient.HttpFetcherStreamingAuthFetcher;
+import io.split.android.client.service.sseclient.sseclient.NotificationProcessorUpdateListener;
import io.split.android.client.service.sseclient.sseclient.PushNotificationManager;
import io.split.android.client.service.sseclient.sseclient.SseAuthenticator;
import io.split.android.client.service.sseclient.sseclient.SseClient;
-import io.split.android.client.service.sseclient.sseclient.SseClientImpl;
+import io.split.android.client.service.sseclient.sseclient.HttpClientStreamingTransport;
+import io.split.android.client.service.sseclient.sseclient.DefaultSseClient;
+import io.split.android.client.service.sseclient.sseclient.EventSourceClientImpl;
import io.split.android.client.service.sseclient.sseclient.SseHandler;
import io.split.android.client.service.sseclient.sseclient.SseRefreshTokenTimer;
+import io.split.android.client.service.sseclient.sseclient.SplitTaskExecutorStreamingScheduler;
import io.split.android.client.service.sseclient.sseclient.StreamingComponents;
+import io.split.android.client.service.sseclient.sseclient.TelemetryRuntimeProducerStreamingTelemetry;
+import io.split.android.client.service.sseclient.spi.StreamingScheduler;
+import io.split.android.client.service.sseclient.spi.StreamingTelemetry;
+import io.split.android.client.service.sseclient.spi.UpdateNotificationListener;
import io.split.android.client.service.synchronizer.RolloutCacheManager;
import io.split.android.client.service.synchronizer.RolloutCacheManagerImpl;
import io.split.android.client.service.synchronizer.SyncGuardian;
@@ -274,7 +283,7 @@ SyncManager buildSyncManager(SplitClientConfig config,
BackoffCounterTimer backoffCounterTimer = null;
if (config.syncEnabled()) {
- backoffCounterTimer = new BackoffCounterTimer(splitTaskExecutor, new ReconnectBackoffCounter(1));
+ backoffCounterTimer = new BackoffCounterTimer(splitTaskExecutor, new ExponentialBackoffCounter(1));
}
return new SyncManagerImpl(config,
@@ -288,18 +297,19 @@ SyncManager buildSyncManager(SplitClientConfig config,
}
@NonNull
- PushNotificationManager getPushNotificationManager(SplitTaskExecutor splitTaskExecutor,
+ PushNotificationManager getPushNotificationManager(StreamingScheduler scheduler,
SseAuthenticator sseAuthenticator,
PushManagerEventBroadcaster pushManagerEventBroadcaster,
SseClient sseClient,
- TelemetryRuntimeProducer telemetryRuntimeProducer,
+ StreamingTelemetry telemetry,
long defaultSseConnectionDelayInSecs,
int sseDisconnectionDelayInSecs) {
return new PushNotificationManager(pushManagerEventBroadcaster,
sseAuthenticator,
sseClient,
- new SseRefreshTokenTimer(splitTaskExecutor, pushManagerEventBroadcaster),
- telemetryRuntimeProducer,
+ new SseRefreshTokenTimer(scheduler, pushManagerEventBroadcaster),
+ scheduler,
+ telemetry,
defaultSseConnectionDelayInSecs,
sseDisconnectionDelayInSecs,
null);
@@ -307,18 +317,21 @@ PushNotificationManager getPushNotificationManager(SplitTaskExecutor splitTaskEx
public SseClient getSseClient(String streamingServiceUrlString,
NotificationParser notificationParser,
- NotificationProcessor notificationProcessor,
- TelemetryRuntimeProducer telemetryRuntimeProducer,
+ UpdateNotificationListener updateListener,
+ StreamingTelemetry telemetry,
PushManagerEventBroadcaster pushManagerEventBroadcaster,
HttpClient httpClient) {
SseHandler sseHandler = new SseHandler(notificationParser,
- notificationProcessor,
- telemetryRuntimeProducer,
+ updateListener,
+ telemetry,
pushManagerEventBroadcaster);
- return new SseClientImpl(URI.create(streamingServiceUrlString),
- httpClient,
- new EventStreamParser(),
+ EventSourceClientImpl eventSourceClient = new EventSourceClientImpl(
+ new HttpClientStreamingTransport(httpClient),
+ new EventStreamParser());
+
+ return new DefaultSseClient(URI.create(streamingServiceUrlString),
+ eventSourceClient,
sseHandler);
}
@@ -396,22 +409,25 @@ public StreamingComponents buildStreamingComponents(@NonNull SplitTaskExecutor s
notificationParser, splitsUpdateNotificationQueue);
PushManagerEventBroadcaster pushManagerEventBroadcaster = new PushManagerEventBroadcaster();
+ StreamingScheduler scheduler = new SplitTaskExecutorStreamingScheduler(splitTaskExecutor);
+ StreamingTelemetry streamingTelemetry = new TelemetryRuntimeProducerStreamingTelemetry(storageContainer.getTelemetryStorage());
+ UpdateNotificationListener updateListener = new NotificationProcessorUpdateListener(notificationProcessor);
SseClient sseClient = getSseClient(config.streamingServiceUrl(),
notificationParser,
- notificationProcessor,
- storageContainer.getTelemetryStorage(),
+ updateListener,
+ streamingTelemetry,
pushManagerEventBroadcaster,
defaultHttpClient);
- SseAuthenticator sseAuthenticator = new SseAuthenticator(splitApiFacade.getSseAuthenticationFetcher(),
+ SseAuthenticator sseAuthenticator = new SseAuthenticator(new HttpFetcherStreamingAuthFetcher(splitApiFacade.getSseAuthenticationFetcher()),
new SseJwtParser(), flagsSpec);
- PushNotificationManager pushNotificationManager = getPushNotificationManager(splitTaskExecutor,
+ PushNotificationManager pushNotificationManager = getPushNotificationManager(scheduler,
sseAuthenticator,
pushManagerEventBroadcaster,
sseClient,
- storageContainer.getTelemetryStorage(),
+ streamingTelemetry,
config.defaultSSEConnectionDelay(),
config.sseDisconnectionDelay());
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..b010fa6ee 100644
--- a/main/src/main/java/io/split/android/client/SplitFactoryImpl.java
+++ b/main/src/main/java/io/split/android/client/SplitFactoryImpl.java
@@ -20,9 +20,11 @@
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;
+import io.split.android.client.streaming.support.CompressionUtilProvider;
import io.split.android.client.events.EventsManagerCoordinator;
import io.split.android.client.factory.FactoryMonitor;
import io.split.android.client.factory.FactoryMonitorImpl;
@@ -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;
@@ -66,15 +70,21 @@
import io.split.android.client.storage.general.GeneralInfoStorage;
import io.split.android.client.storage.splits.SplitsStorage;
import io.split.android.client.telemetry.TelemetrySynchronizer;
+import io.split.android.client.dtos.Event;
+import io.split.android.client.telemetry.model.Method;
import io.split.android.client.telemetry.storage.TelemetryStorage;
+import io.split.android.client.tracker.DefaultTracker;
+import io.split.android.client.tracker.Tracker;
+import io.split.android.client.tracker.TrackerEvent;
import io.split.android.client.utils.logger.Logger;
import io.split.android.client.validators.ApiKeyValidator;
import io.split.android.client.validators.ApiKeyValidatorImpl;
-import io.split.android.client.validators.EventValidator;
import io.split.android.client.validators.EventValidatorImpl;
import io.split.android.client.validators.KeyValidator;
import io.split.android.client.validators.KeyValidatorImpl;
+import io.split.android.client.validators.PropertyValidatorImpl;
import io.split.android.client.validators.SplitValidatorImpl;
+import io.split.android.client.validators.TrafficTypeValidatorImpl;
import io.split.android.client.validators.ValidationConfig;
import io.split.android.client.validators.ValidationErrorInfo;
import io.split.android.client.validators.ValidationMessageLogger;
@@ -381,20 +391,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 +413,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;
@@ -536,7 +551,7 @@ public static class EventsTrackerProvider {
private final SplitsStorage mSplitsStorage;
private final TelemetryStorage mTelemetryStorage;
private final SyncManager mSyncManager;
- private volatile EventsTracker mEventsTracker;
+ private volatile Tracker mEventsTracker;
public EventsTrackerProvider(SplitsStorage splitsStorage, TelemetryStorage telemetryStorage, SyncManager syncManager) {
mSplitsStorage = splitsStorage;
@@ -544,13 +559,32 @@ public EventsTrackerProvider(SplitsStorage splitsStorage, TelemetryStorage telem
mSyncManager = syncManager;
}
- public EventsTracker getEventsTracker() {
+ public Tracker getEventsTracker() {
if (mEventsTracker == null) {
synchronized (this) {
if (mEventsTracker == null) {
- EventValidator eventsValidator = new EventValidatorImpl(new KeyValidatorImpl(), mSplitsStorage);
- mEventsTracker = new EventsTrackerImpl(eventsValidator, new ValidationMessageLoggerImpl(), mTelemetryStorage,
- new PropertyValidatorImpl(), mSyncManager);
+ mEventsTracker = new DefaultTracker(
+ new EventValidatorImpl(
+ new KeyValidatorImpl(),
+ new TrafficTypeValidatorImpl(mSplitsStorage)
+ ),
+ new ValidationMessageLoggerImpl(),
+ new PropertyValidatorImpl(
+ new ValidationMessageLoggerImpl()
+ ),
+ trackerEvent -> {
+ Event event = new Event();
+ event.eventTypeId = trackerEvent.eventType;
+ event.trafficTypeName = trackerEvent.trafficType;
+ event.key = trackerEvent.key;
+ event.value = trackerEvent.value;
+ event.timestamp = trackerEvent.timestamp;
+ event.properties = trackerEvent.properties;
+ event.setSizeInBytes(trackerEvent.sizeInBytes);
+ mSyncManager.pushEvent(event);
+ },
+ latencyMs -> mTelemetryStorage.recordLatency(Method.TRACK, latencyMs),
+ () -> mTelemetryStorage.recordException(Method.TRACK));
}
}
}
diff --git a/main/src/main/java/io/split/android/client/dtos/Event.java b/main/src/main/java/io/split/android/client/dtos/Event.java
index fe8c986d9..d10397363 100644
--- a/main/src/main/java/io/split/android/client/dtos/Event.java
+++ b/main/src/main/java/io/split/android/client/dtos/Event.java
@@ -3,7 +3,7 @@
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
-import io.split.android.client.storage.common.InBytesSizable;
+import io.split.android.client.submitter.InBytesSizable;
import io.split.android.client.utils.deserializer.EventDeserializer;
@JsonAdapter(EventDeserializer.class)
diff --git a/main/src/main/java/io/split/android/client/dtos/KeyImpression.java b/main/src/main/java/io/split/android/client/dtos/KeyImpression.java
index 8bf7f2e7e..6cd3795e8 100644
--- a/main/src/main/java/io/split/android/client/dtos/KeyImpression.java
+++ b/main/src/main/java/io/split/android/client/dtos/KeyImpression.java
@@ -6,7 +6,7 @@
import java.util.Objects;
import io.split.android.client.service.ServiceConstants;
-import io.split.android.client.storage.common.InBytesSizable;
+import io.split.android.client.submitter.InBytesSizable;
import io.split.android.client.impressions.Impression;
public class KeyImpression implements InBytesSizable, Identifiable {
diff --git a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java
index 1b5e58499..5fb309e76 100644
--- a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java
+++ b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java
@@ -16,7 +16,8 @@
import io.split.android.client.EvaluationOptions;
import io.split.android.client.EvaluatorImpl;
import io.split.android.client.FlagSetsFilter;
-import io.split.android.client.PropertyValidatorImpl;
+import io.split.android.client.validators.PropertyValidatorImpl;
+import io.split.android.client.validators.PropertyValidatorAdapter;
import io.split.android.client.SplitClient;
import io.split.android.client.SplitClientConfig;
import io.split.android.client.SplitFactory;
@@ -87,7 +88,9 @@ public LocalhostSplitClient(@NonNull LocalhostSplitFactory container,
new SplitValidatorImpl(), getImpressionsListener(splitClientConfig),
splitClientConfig.labelsEnabled(), eventsManager, attributesManager, attributesMerger,
telemetryStorageProducer, flagSetsFilter, splitsStorage, new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl(),
- new PropertyValidatorImpl(), mFallbackTreatmentsCalculator);
+ new PropertyValidatorAdapter(
+ new PropertyValidatorImpl(new ValidationMessageLoggerImpl())),
+ mFallbackTreatmentsCalculator);
}
@Override
diff --git a/main/src/main/java/io/split/android/client/localhost/LocalhostTrafficTypeValidator.java b/main/src/main/java/io/split/android/client/localhost/LocalhostTrafficTypeValidator.java
new file mode 100644
index 000000000..2b4ed2010
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/localhost/LocalhostTrafficTypeValidator.java
@@ -0,0 +1,18 @@
+package io.split.android.client.localhost;
+
+import io.split.android.client.tracker.TrafficTypeValidator;
+
+/**
+ * Traffic type validator for localhost mode.
+ *
+ * In localhost mode, all traffic types are considered valid since we're not
+ * connected to the Split backend and can't validate against real feature flags.
+ */
+public class LocalhostTrafficTypeValidator implements TrafficTypeValidator {
+
+ @Override
+ public boolean isValid(String trafficTypeName) {
+ // In localhost mode, accept all traffic types
+ return true;
+ }
+}
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/network/SdkVersionProvider.java b/main/src/main/java/io/split/android/client/network/SdkVersionProvider.java
new file mode 100644
index 000000000..9198e60fd
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/network/SdkVersionProvider.java
@@ -0,0 +1,10 @@
+package io.split.android.client.network;
+
+import io.split.android.client.main.BuildConfig;
+
+public class SdkVersionProvider {
+
+ public static String getSdkVersion() {
+ return "Android-" + BuildConfig.SPLIT_VERSION_NAME;
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/CleanUpDatabaseTask.java b/main/src/main/java/io/split/android/client/service/CleanUpDatabaseTask.java
index 1eca9ba92..a487d9ef4 100644
--- a/main/src/main/java/io/split/android/client/service/CleanUpDatabaseTask.java
+++ b/main/src/main/java/io/split/android/client/service/CleanUpDatabaseTask.java
@@ -4,7 +4,7 @@
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.impressions.observer.PersistentImpressionsObserverCacheStorage;
import io.split.android.client.storage.events.PersistentEventsStorage;
import io.split.android.client.storage.impressions.PersistentImpressionsCountStorage;
diff --git a/main/src/main/java/io/split/android/client/service/HttpRecorderSubmitterAdapter.java b/main/src/main/java/io/split/android/client/service/HttpRecorderSubmitterAdapter.java
new file mode 100644
index 000000000..beb51fa7b
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/HttpRecorderSubmitterAdapter.java
@@ -0,0 +1,28 @@
+package io.split.android.client.service;
+
+import androidx.annotation.NonNull;
+
+import io.split.android.client.service.http.HttpRecorder;
+import io.split.android.client.service.http.HttpRecorderException;
+import io.split.android.client.service.http.HttpStatus;
+import io.split.android.client.submitter.RecorderException;
+import io.split.android.client.submitter.RecorderSubmitter;
+
+public class HttpRecorderSubmitterAdapter implements RecorderSubmitter {
+ private final HttpRecorder mHttpRecorder;
+
+ public HttpRecorderSubmitterAdapter(@NonNull HttpRecorder httpRecorder) {
+ mHttpRecorder = httpRecorder;
+ }
+
+ @Override
+ public void execute(@NonNull T data) throws RecorderException {
+ try {
+ mHttpRecorder.execute(data);
+ } catch (HttpRecorderException e) {
+ Integer httpStatus = e.getHttpStatus();
+ boolean retryable = !HttpStatus.isNotRetryable(HttpStatus.fromCode(httpStatus));
+ throw new RecorderException(e.getMessage(), httpStatus, retryable);
+ }
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/SplitTaskType.java b/main/src/main/java/io/split/android/client/service/SplitTaskType.java
new file mode 100644
index 000000000..0ee0d016e
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/SplitTaskType.java
@@ -0,0 +1,25 @@
+package io.split.android.client.service;
+
+public enum SplitTaskType implements io.split.android.client.service.executor.SplitTaskType {
+ SPLITS_SYNC,
+ MY_SEGMENTS_SYNC,
+ EVENTS_RECORDER,
+ IMPRESSIONS_RECORDER,
+ LOAD_LOCAL_SPLITS,
+ LOAD_LOCAL_MY_SEGMENTS,
+ SSE_AUTHENTICATION_TASK,
+ SPLIT_KILL,
+ FILTER_SPLITS_CACHE,
+ CLEAN_UP_DATABASE,
+ IMPRESSIONS_COUNT_RECORDER,
+ SAVE_IMPRESSIONS_COUNT,
+ MY_SEGMENTS_UPDATE,
+ LOAD_LOCAL_ATTRIBUTES,
+ TELEMETRY_CONFIG_TASK,
+ TELEMETRY_STATS_TASK,
+ SAVE_UNIQUE_KEYS_TASK,
+ UNIQUE_KEYS_RECORDER_TASK,
+ MY_LARGE_SEGMENTS_UPDATE,
+ LOAD_LOCAL_RULE_BASED_SEGMENTS,
+ RULE_BASED_SEGMENT_SYNC,
+}
diff --git a/main/src/main/java/io/split/android/client/service/TelemetryRecorderAdapter.java b/main/src/main/java/io/split/android/client/service/TelemetryRecorderAdapter.java
new file mode 100644
index 000000000..87610860f
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/TelemetryRecorderAdapter.java
@@ -0,0 +1,33 @@
+package io.split.android.client.service;
+
+import androidx.annotation.NonNull;
+
+import io.split.android.client.submitter.RecorderTelemetry;
+import io.split.android.client.telemetry.model.OperationType;
+import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
+
+public class TelemetryRecorderAdapter implements RecorderTelemetry {
+ private final TelemetryRuntimeProducer mTelemetryProducer;
+ private final OperationType mOperationType;
+
+ public TelemetryRecorderAdapter(@NonNull TelemetryRuntimeProducer telemetryProducer,
+ @NonNull OperationType operationType) {
+ mTelemetryProducer = telemetryProducer;
+ mOperationType = operationType;
+ }
+
+ @Override
+ public void recordSuccess(long timestamp) {
+ mTelemetryProducer.recordSuccessfulSync(mOperationType, timestamp);
+ }
+
+ @Override
+ public void recordError(Integer httpStatus) {
+ mTelemetryProducer.recordSyncError(mOperationType, httpStatus);
+ }
+
+ @Override
+ public void recordLatency(long latencyMs) {
+ mTelemetryProducer.recordSyncLatency(mOperationType, latencyMs);
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/attributes/LoadAttributesTask.java b/main/src/main/java/io/split/android/client/service/attributes/LoadAttributesTask.java
index fd7883012..58cebeee2 100644
--- a/main/src/main/java/io/split/android/client/service/attributes/LoadAttributesTask.java
+++ b/main/src/main/java/io/split/android/client/service/attributes/LoadAttributesTask.java
@@ -7,7 +7,7 @@
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.storage.attributes.AttributesStorage;
import io.split.android.client.storage.attributes.PersistentAttributesStorage;
diff --git a/main/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java b/main/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java
index d380af4e7..e26d121da 100644
--- a/main/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java
+++ b/main/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java
@@ -1,116 +1,42 @@
package io.split.android.client.service.events;
-import static io.split.android.client.utils.Utils.checkNotNull;
-import static io.split.android.client.utils.Utils.partition;
-
import androidx.annotation.NonNull;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import io.split.android.client.dtos.Event;
-import io.split.android.client.service.executor.SplitTask;
-import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskExecutionStatus;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.HttpRecorderSubmitterAdapter;
+import io.split.android.client.service.TelemetryRecorderAdapter;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.http.HttpRecorder;
-import io.split.android.client.service.http.HttpRecorderException;
-import io.split.android.client.service.http.HttpStatus;
import io.split.android.client.storage.events.PersistentEventsStorage;
+import io.split.android.client.submitter.RecorderTask;
import io.split.android.client.telemetry.model.OperationType;
import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
-import io.split.android.client.utils.logger.Logger;
-public class EventsRecorderTask implements SplitTask {
- public final static int FAILING_CHUNK_SIZE = 20;
- private final PersistentEventsStorage mPersistentEventsStorage;
- private final HttpRecorder> mHttpRecorder;
- private final EventsRecorderTaskConfig mConfig;
- private final TelemetryRuntimeProducer mTelemetryRuntimeProducer;
+public class EventsRecorderTask extends RecorderTask> {
+
+ public static final int FAILING_CHUNK_SIZE = 20;
public EventsRecorderTask(@NonNull HttpRecorder> httpRecorder,
- @NonNull PersistentEventsStorage persistentEventsStorage,
+ @NonNull PersistentEventsStorage storage,
@NonNull EventsRecorderTaskConfig config,
@NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) {
- mHttpRecorder = checkNotNull(httpRecorder);
- mPersistentEventsStorage = checkNotNull(persistentEventsStorage);
- mConfig = checkNotNull(config);
- mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer);
+ super(storage,
+ new HttpRecorderSubmitterAdapter<>(httpRecorder),
+ config.getEventsPerPush(),
+ SplitTaskType.EVENTS_RECORDER,
+ new TelemetryRecorderAdapter(telemetryRuntimeProducer, OperationType.EVENTS),
+ FAILING_CHUNK_SIZE);
}
@Override
- @NonNull
- public SplitTaskExecutionInfo execute() {
- SplitTaskExecutionStatus status = SplitTaskExecutionStatus.SUCCESS;
- int nonSentRecords = 0;
- long nonSentBytes = 0;
- List events;
- List failingEvents = new ArrayList<>();
- boolean doNotRetry = false;
- do {
- events = mPersistentEventsStorage.pop(mConfig.getEventsPerPush());
- if (events.size() > 0) {
- long startTime = System.currentTimeMillis();
- long latency = 0;
- try {
- Logger.d("Posting %d Split events", events.size());
- mHttpRecorder.execute(events);
-
- long now = System.currentTimeMillis();
- latency = now - startTime;
- mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.EVENTS, now);
-
- mPersistentEventsStorage.delete(events);
- Logger.d("%d split events sent", events.size());
- } catch (HttpRecorderException e) {
- status = SplitTaskExecutionStatus.ERROR;
- nonSentRecords += mConfig.getEventsPerPush();
- nonSentBytes += sumEventBytes(events);
- Logger.e("Event recorder task: Some events couldn't be sent" +
- "Saving to send them in a new iteration: " +
- e.getLocalizedMessage());
- failingEvents.addAll(events);
-
- mTelemetryRuntimeProducer.recordSyncError(OperationType.EVENTS, e.getHttpStatus());
-
- if (HttpStatus.isNotRetryable(e.getHttpStatus())) {
- doNotRetry = true;
- break;
- }
- } finally {
- mTelemetryRuntimeProducer.recordSyncLatency(OperationType.EVENTS, latency);
- }
- }
- } while (events.size() == mConfig.getEventsPerPush());
-
- // Update events by chunks to avoid sqlite errors
- List> failingChunks = partition(failingEvents, FAILING_CHUNK_SIZE);
- for (List chunk : failingChunks) {
- mPersistentEventsStorage.setActive(chunk);
- }
-
- if (status == SplitTaskExecutionStatus.ERROR) {
- Map data = new HashMap<>();
- data.put(SplitTaskExecutionInfo.NON_SENT_RECORDS, nonSentRecords);
- data.put(SplitTaskExecutionInfo.NON_SENT_BYTES, nonSentBytes);
- if (doNotRetry) {
- data.put(SplitTaskExecutionInfo.DO_NOT_RETRY, true);
- }
-
- return SplitTaskExecutionInfo.error(
- SplitTaskType.EVENTS_RECORDER, data);
- }
- return SplitTaskExecutionInfo.success(SplitTaskType.EVENTS_RECORDER);
+ protected List transformForSubmission(List items) {
+ return items;
}
- private long sumEventBytes(List events) {
- long totalBytes = 0;
- for (Event event : events) {
- totalBytes += event.getSizeInBytes();
- }
- return totalBytes;
+ @Override
+ protected long estimateItemSize(Event item) {
+ return item.getSizeInBytes();
}
}
diff --git a/main/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java b/main/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java
index 3dd20800c..d6038ee17 100644
--- a/main/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java
+++ b/main/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java
@@ -44,7 +44,7 @@
import io.split.android.client.service.splits.SplitsSyncTask;
import io.split.android.client.service.splits.SplitsUpdateTask;
import io.split.android.client.service.splits.TargetingRulesCache;
-import io.split.android.client.service.sseclient.ReconnectBackoffCounter;
+import io.split.android.client.backoff.ExponentialBackoffCounter;
import io.split.android.client.service.telemetry.TelemetryConfigRecorderTask;
import io.split.android.client.service.telemetry.TelemetryStatsRecorderTask;
import io.split.android.client.service.telemetry.TelemetryTaskFactory;
@@ -104,7 +104,7 @@ public SplitTaskFactoryImpl(@NonNull SplitClientConfig splitClientConfig,
ruleBasedSegmentStorageProducer,
mSplitsStorageContainer.getGeneralInfoStorage(),
mTelemetryRuntimeProducer,
- new ReconnectBackoffCounter(1, testingConfig.getCdnBackoffTime()),
+ new ExponentialBackoffCounter(1, testingConfig.getCdnBackoffTime()),
flagsSpecFromConfig,
targetingRulesCache);
} else {
diff --git a/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java
index cd761d75b..32ae11600 100644
--- a/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java
+++ b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java
@@ -1,96 +1,34 @@
package io.split.android.client.service.impressions;
-import static io.split.android.client.utils.Utils.checkNotNull;
-
import androidx.annotation.NonNull;
-import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
+import io.split.android.client.service.HttpRecorderSubmitterAdapter;
import io.split.android.client.service.ServiceConstants;
-import io.split.android.client.service.executor.SplitTask;
-import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskExecutionStatus;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.TelemetryRecorderAdapter;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.http.HttpRecorder;
-import io.split.android.client.service.http.HttpRecorderException;
-import io.split.android.client.service.http.HttpStatus;
import io.split.android.client.storage.impressions.PersistentImpressionsCountStorage;
+import io.split.android.client.submitter.RecorderTask;
import io.split.android.client.telemetry.model.OperationType;
import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
-import io.split.android.client.utils.logger.Logger;
-public class ImpressionsCountRecorderTask implements SplitTask {
- private final PersistentImpressionsCountStorage mPersistentStorage;
- private final HttpRecorder mHttpRecorder;
- private static int POP_COUNT = ServiceConstants.DEFAULT_IMPRESSION_COUNT_ROWS_POP;
- private final TelemetryRuntimeProducer mTelemetryRuntimeProducer;
+public class ImpressionsCountRecorderTask extends RecorderTask {
public ImpressionsCountRecorderTask(@NonNull HttpRecorder httpRecorder,
@NonNull PersistentImpressionsCountStorage persistentStorage,
@NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) {
- mHttpRecorder = checkNotNull(httpRecorder);
- mPersistentStorage = checkNotNull(persistentStorage);
- mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer);
+ super(persistentStorage,
+ new HttpRecorderSubmitterAdapter<>(httpRecorder),
+ ServiceConstants.DEFAULT_IMPRESSION_COUNT_ROWS_POP,
+ SplitTaskType.IMPRESSIONS_COUNT_RECORDER,
+ new TelemetryRecorderAdapter(telemetryRuntimeProducer, OperationType.IMPRESSIONS_COUNT),
+ 0);
}
@Override
- @NonNull
- public SplitTaskExecutionInfo execute() {
- SplitTaskExecutionStatus status = SplitTaskExecutionStatus.SUCCESS;
-
- List countList = new ArrayList<>();
- List failedSent = new ArrayList<>();
- boolean doNotRetry = false;
- do {
- countList = mPersistentStorage.pop(POP_COUNT);
- if (countList.size() > 0) {
- long startTime = System.currentTimeMillis();
- long latency = 0;
- try {
- Logger.d("Posting %d Split impressions count", countList.size());
- mHttpRecorder.execute(new ImpressionsCount(countList));
-
- long now = System.currentTimeMillis();
- latency = now - startTime;
- mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.IMPRESSIONS_COUNT, now);
-
- mPersistentStorage.delete(countList);
- Logger.d("%d split impressions count sent", countList.size());
- } catch (HttpRecorderException e) {
- status = SplitTaskExecutionStatus.ERROR;
- Logger.e("Impressions count recorder task: Some counts couldn't be sent. " +
- "Saving to send them in a new iteration\n" +
- e.getLocalizedMessage());
- failedSent.addAll(countList);
-
- mTelemetryRuntimeProducer.recordSyncError(OperationType.IMPRESSIONS_COUNT, e.getHttpStatus());
-
- if (HttpStatus.isNotRetryable(HttpStatus.fromCode(e.getHttpStatus()))) {
- doNotRetry = true;
- break;
- }
- } finally {
- mTelemetryRuntimeProducer.recordSyncLatency(OperationType.IMPRESSIONS_COUNT, latency);
- }
- }
- } while (countList.size() == POP_COUNT);
-
- if (failedSent.size() > 0) {
- mPersistentStorage.setActive(failedSent);
- }
-
- if (status == SplitTaskExecutionStatus.ERROR) {
- Map data = new HashMap<>();
- if (doNotRetry) {
- data.put(SplitTaskExecutionInfo.DO_NOT_RETRY, true);
- }
-
- return SplitTaskExecutionInfo.error(SplitTaskType.IMPRESSIONS_COUNT_RECORDER, data);
- }
-
- return SplitTaskExecutionInfo.success(SplitTaskType.IMPRESSIONS_COUNT_RECORDER);
+ protected ImpressionsCount transformForSubmission(List items) {
+ return new ImpressionsCount(items);
}
}
diff --git a/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java
index 7a4b122c9..d6fdabb7b 100644
--- a/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java
+++ b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java
@@ -1,112 +1,43 @@
package io.split.android.client.service.impressions;
-import static io.split.android.client.utils.Utils.checkNotNull;
-
import androidx.annotation.NonNull;
-import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import io.split.android.client.dtos.KeyImpression;
-import io.split.android.client.service.executor.SplitTask;
-import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskExecutionStatus;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.HttpRecorderSubmitterAdapter;
+import io.split.android.client.service.TelemetryRecorderAdapter;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.http.HttpRecorder;
-import io.split.android.client.service.http.HttpRecorderException;
-import io.split.android.client.service.http.HttpStatus;
import io.split.android.client.storage.impressions.PersistentImpressionsStorage;
+import io.split.android.client.submitter.RecorderTask;
import io.split.android.client.telemetry.model.OperationType;
import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
-import io.split.android.client.utils.logger.Logger;
-public class ImpressionsRecorderTask implements SplitTask {
- public final static int FAILING_CHUNK_SIZE = 20;
- private final PersistentImpressionsStorage mPersistenImpressionsStorage;
- private final HttpRecorder> mHttpRecorder;
- private final ImpressionsRecorderTaskConfig mConfig;
- private final TelemetryRuntimeProducer mTelemetryRuntimeProducer;
+public class ImpressionsRecorderTask extends RecorderTask> {
+
+ private final long mEstimatedSizeInBytes;
public ImpressionsRecorderTask(@NonNull HttpRecorder> httpRecorder,
- @NonNull PersistentImpressionsStorage persistenEventsStorage,
+ @NonNull PersistentImpressionsStorage storage,
@NonNull ImpressionsRecorderTaskConfig config,
@NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) {
- mHttpRecorder = checkNotNull(httpRecorder);
- mPersistenImpressionsStorage = checkNotNull(persistenEventsStorage);
- mConfig = checkNotNull(config);
- mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer);
+ super(storage,
+ new HttpRecorderSubmitterAdapter<>(httpRecorder),
+ config.getImpressionsPerPush(),
+ SplitTaskType.IMPRESSIONS_RECORDER,
+ new TelemetryRecorderAdapter(telemetryRuntimeProducer, OperationType.IMPRESSIONS),
+ 0);
+ this.mEstimatedSizeInBytes = config.getEstimatedSizeInBytes();
}
@Override
- @NonNull
- public SplitTaskExecutionInfo execute() {
- SplitTaskExecutionStatus status = SplitTaskExecutionStatus.SUCCESS;
- int nonSentRecords = 0;
- long nonSentBytes = 0;
- List impressions;
- List failingImpressions = new ArrayList<>();
- boolean doNotRetry = false;
- do {
- impressions = mPersistenImpressionsStorage.pop(mConfig.getImpressionsPerPush());
- if (impressions.size() > 0) {
- long startTime = System.currentTimeMillis();
- long latency = 0;
- try {
- Logger.d("Posting %d Split impressions", impressions.size());
- mHttpRecorder.execute(impressions);
-
- long now = System.currentTimeMillis();
- latency = now - startTime;
- mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.IMPRESSIONS, now);
-
- mPersistenImpressionsStorage.delete(impressions);
- Logger.d("%d split impressions sent", impressions.size());
- } catch (HttpRecorderException e) {
- status = SplitTaskExecutionStatus.ERROR;
- nonSentRecords += mConfig.getImpressionsPerPush();
- nonSentBytes += sumImpressionsBytes(impressions);
- Logger.e("Impressions recorder task: Some impressions couldn't be sent. " +
- "Saving to send them in a new iteration\n" +
- e.getLocalizedMessage());
- failingImpressions.addAll(impressions);
-
- mTelemetryRuntimeProducer.recordSyncError(OperationType.IMPRESSIONS, e.getHttpStatus());
-
- if (HttpStatus.isNotRetryable(HttpStatus.fromCode(e.getHttpStatus()))) {
- doNotRetry = true;
- break;
- }
- } finally {
- mTelemetryRuntimeProducer.recordSyncLatency(OperationType.IMPRESSIONS, latency);
- }
- }
- } while (impressions.size() == mConfig.getImpressionsPerPush());
-
- if (failingImpressions.size() > 0) {
- mPersistenImpressionsStorage.setActive(failingImpressions);
- }
-
- if (status == SplitTaskExecutionStatus.ERROR) {
- Map data = new HashMap<>();
- data.put(SplitTaskExecutionInfo.NON_SENT_RECORDS, nonSentRecords);
- data.put(SplitTaskExecutionInfo.NON_SENT_BYTES, nonSentBytes);
- if (doNotRetry) {
- data.put(SplitTaskExecutionInfo.DO_NOT_RETRY, true);
- }
-
- return SplitTaskExecutionInfo.error(
- SplitTaskType.IMPRESSIONS_RECORDER, data);
- }
- return SplitTaskExecutionInfo.success(SplitTaskType.IMPRESSIONS_RECORDER);
+ protected List transformForSubmission(List items) {
+ return items;
}
- private long sumImpressionsBytes(List impressions) {
- long totalBytes = 0;
- for (KeyImpression impression : impressions) {
- totalBytes += mConfig.getEstimatedSizeInBytes();
- }
- return totalBytes;
+ @Override
+ protected long estimateItemSize(KeyImpression item) {
+ return mEstimatedSizeInBytes;
}
}
diff --git a/main/src/main/java/io/split/android/client/service/impressions/SaveImpressionsCountTask.java b/main/src/main/java/io/split/android/client/service/impressions/SaveImpressionsCountTask.java
index 564a137c1..8e8e5c303 100644
--- a/main/src/main/java/io/split/android/client/service/impressions/SaveImpressionsCountTask.java
+++ b/main/src/main/java/io/split/android/client/service/impressions/SaveImpressionsCountTask.java
@@ -6,7 +6,7 @@
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.storage.impressions.PersistentImpressionsCountStorage;
import static io.split.android.client.utils.Utils.checkNotNull;
diff --git a/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java
index 79ea6dc1c..8fe7e3a99 100644
--- a/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java
+++ b/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java
@@ -16,7 +16,7 @@
import io.split.android.client.service.executor.SplitTaskExecutor;
import io.split.android.client.service.impressions.ImpressionsTaskFactory;
import io.split.android.client.service.impressions.observer.ImpressionsObserver;
-import io.split.android.client.service.synchronizer.RecorderSyncHelper;
+import io.split.android.client.submitter.RecorderSyncHelper;
import io.split.android.client.telemetry.model.ImpressionsDataType;
import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
diff --git a/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java
index 36d86ff7b..ad1d8219f 100644
--- a/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java
+++ b/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java
@@ -15,7 +15,7 @@
import io.split.android.client.service.impressions.ImpressionsTaskFactory;
import io.split.android.client.service.impressions.observer.ImpressionsObserver;
import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer;
-import io.split.android.client.service.synchronizer.RecorderSyncHelper;
+import io.split.android.client.submitter.RecorderSyncHelper;
class DebugTracker implements PeriodicTracker {
diff --git a/main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java
index c331ee61f..3c1a29235 100644
--- a/main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java
+++ b/main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java
@@ -5,7 +5,7 @@
import io.split.android.client.dtos.KeyImpression;
import io.split.android.client.service.ServiceConstants;
import io.split.android.client.service.executor.SplitTaskExecutor;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.impressions.ImpressionManagerRetryTimerProviderImpl;
import io.split.android.client.service.impressions.ImpressionsCounter;
import io.split.android.client.service.impressions.ImpressionsMode;
@@ -13,7 +13,7 @@
import io.split.android.client.service.impressions.observer.ImpressionsObserverImpl;
import io.split.android.client.service.impressions.unique.UniqueKeysTracker;
import io.split.android.client.service.impressions.unique.UniqueKeysTrackerImpl;
-import io.split.android.client.service.synchronizer.RecorderSyncHelperImpl;
+import io.split.android.client.submitter.RecorderSyncHelperImpl;
import io.split.android.client.storage.common.SplitStorageContainer;
import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
diff --git a/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java
index 23f7c4b7c..5c3b85323 100644
--- a/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java
+++ b/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java
@@ -18,7 +18,7 @@
import io.split.android.client.service.impressions.ImpressionsCounter;
import io.split.android.client.service.impressions.ImpressionsTaskFactory;
import io.split.android.client.service.impressions.observer.ImpressionsObserver;
-import io.split.android.client.service.synchronizer.RecorderSyncHelper;
+import io.split.android.client.submitter.RecorderSyncHelper;
import io.split.android.client.telemetry.model.ImpressionsDataType;
import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
diff --git a/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java
index 5f441dfaf..3ca2bc608 100644
--- a/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java
+++ b/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java
@@ -15,7 +15,7 @@
import io.split.android.client.service.impressions.ImpressionsTaskFactory;
import io.split.android.client.service.impressions.observer.ImpressionsObserver;
import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer;
-import io.split.android.client.service.synchronizer.RecorderSyncHelper;
+import io.split.android.client.submitter.RecorderSyncHelper;
class OptimizedTracker implements PeriodicTracker {
diff --git a/main/src/main/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTask.java b/main/src/main/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTask.java
index 2772ea031..a25148f7b 100644
--- a/main/src/main/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTask.java
+++ b/main/src/main/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTask.java
@@ -13,7 +13,7 @@
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.storage.impressions.PersistentImpressionsUniqueStorage;
public class SaveUniqueImpressionsTask implements SplitTask {
diff --git a/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java b/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java
index a258f16ce..cac00741a 100644
--- a/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java
+++ b/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java
@@ -1,7 +1,5 @@
package io.split.android.client.service.impressions.unique;
-import static io.split.android.client.utils.Utils.checkNotNull;
-
import androidx.annotation.NonNull;
import java.util.ArrayList;
@@ -11,111 +9,49 @@
import java.util.Map;
import java.util.Set;
-import io.split.android.client.service.executor.SplitTask;
-import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskExecutionStatus;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.HttpRecorderSubmitterAdapter;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.http.HttpRecorder;
-import io.split.android.client.service.http.HttpRecorderException;
-import io.split.android.client.service.http.HttpStatus;
import io.split.android.client.storage.impressions.PersistentImpressionsUniqueStorage;
-import io.split.android.client.utils.logger.Logger;
+import io.split.android.client.submitter.RecorderTask;
-public class UniqueKeysRecorderTask implements SplitTask {
+public class UniqueKeysRecorderTask extends RecorderTask {
- private final HttpRecorder mHttpRecorder;
- private final PersistentImpressionsUniqueStorage mStorage;
- private final UniqueKeysRecorderTaskConfig mConfig;
+ private final long mEstimatedSizeInBytes;
public UniqueKeysRecorderTask(@NonNull HttpRecorder uniqueImpressionsRecorder,
@NonNull PersistentImpressionsUniqueStorage storage,
@NonNull UniqueKeysRecorderTaskConfig config) {
- mHttpRecorder = checkNotNull(uniqueImpressionsRecorder);
- mStorage = checkNotNull(storage);
- mConfig = checkNotNull(config);
+ super(storage,
+ new HttpRecorderSubmitterAdapter<>(uniqueImpressionsRecorder),
+ config.getElementsPerPush(),
+ SplitTaskType.UNIQUE_KEYS_RECORDER_TASK,
+ null,
+ 0);
+ this.mEstimatedSizeInBytes = config.getEstimatedSizeInBytes();
}
- @NonNull
@Override
- public SplitTaskExecutionInfo execute() {
- SplitTaskExecutionStatus status = SplitTaskExecutionStatus.SUCCESS;
- int nonSentRecords = 0;
- long nonSentBytes = 0;
- List keys;
- List failingKeys = new ArrayList<>();
- boolean doNotRetry = false;
- do {
- keys = mStorage.pop(mConfig.getElementsPerPush());
- if (keys.size() > 0) {
- try {
- Logger.d("Posting %d Split MTKs", keys.size());
- mHttpRecorder.execute(buildMTK(keys));
-
- mStorage.delete(keys);
- Logger.d("%d split MTKs sent", keys.size());
- } catch (HttpRecorderException e) {
- status = SplitTaskExecutionStatus.ERROR;
- nonSentRecords += mConfig.getElementsPerPush();
- nonSentBytes += sumImpressionsBytes(keys);
- Logger.e("MTKs recorder task: Some keys couldn't be sent. " +
- "Saving to send them in a new iteration\n" +
- e.getLocalizedMessage());
- failingKeys.addAll(keys);
-
- if (HttpStatus.isNotRetryable(HttpStatus.fromCode(e.getHttpStatus()))) {
- doNotRetry = true;
- break;
- }
- }
- }
- } while (keys.size() == mConfig.getElementsPerPush());
-
- if (failingKeys.size() > 0) {
- mStorage.setActive(failingKeys);
- }
-
- if (status == SplitTaskExecutionStatus.ERROR) {
- Map data = new HashMap<>();
- data.put(SplitTaskExecutionInfo.NON_SENT_RECORDS, nonSentRecords);
- data.put(SplitTaskExecutionInfo.NON_SENT_BYTES, nonSentBytes);
- if (doNotRetry) {
- data.put(SplitTaskExecutionInfo.DO_NOT_RETRY, true);
- }
-
- return SplitTaskExecutionInfo.error(
- SplitTaskType.UNIQUE_KEYS_RECORDER_TASK, data);
- }
-
- return SplitTaskExecutionInfo.success(SplitTaskType.UNIQUE_KEYS_RECORDER_TASK);
- }
-
- @NonNull
- private static MTK buildMTK(List keys) {
+ protected MTK transformForSubmission(List items) {
Map map = new HashMap<>();
- for (UniqueKey key : keys) {
+ for (UniqueKey key : items) {
String userKey = key.getKey();
if (!map.containsKey(userKey)) {
map.put(userKey, new UniqueKey(userKey, new HashSet<>()));
}
-
UniqueKey uniqueKey = map.get(userKey);
if (uniqueKey != null) {
Set originalFeatures = uniqueKey.getFeatures();
Set newFeatures = key.getFeatures();
newFeatures.addAll(originalFeatures);
-
map.put(userKey, new UniqueKey(userKey, newFeatures));
}
}
-
return new MTK(new ArrayList<>(map.values()));
}
- private long sumImpressionsBytes(List keys) {
- long totalBytes = 0;
- for (UniqueKey key : keys) {
- totalBytes += mConfig.getEstimatedSizeInBytes();
- }
- return totalBytes;
+ @Override
+ protected long estimateItemSize(UniqueKey item) {
+ return mEstimatedSizeInBytes;
}
}
diff --git a/main/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java b/main/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java
index 451d58dad..99ab45950 100644
--- a/main/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java
+++ b/main/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java
@@ -1,6 +1,6 @@
package io.split.android.client.service.mysegments;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
public class LoadMySegmentsTaskConfig {
diff --git a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java
index 5141b887d..873b44a32 100644
--- a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java
+++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java
@@ -28,8 +28,8 @@
import io.split.android.client.service.http.HttpFetcher;
import io.split.android.client.service.http.HttpFetcherException;
import io.split.android.client.service.http.HttpStatus;
-import io.split.android.client.service.sseclient.BackoffCounter;
-import io.split.android.client.service.sseclient.ReconnectBackoffCounter;
+import io.split.android.client.backoff.BackoffCounter;
+import io.split.android.client.backoff.ExponentialBackoffCounter;
import io.split.android.client.service.synchronizer.MySegmentsChangeChecker;
import io.split.android.client.storage.mysegments.MySegmentsStorage;
import io.split.android.client.telemetry.model.OperationType;
@@ -80,7 +80,7 @@ public MySegmentsSyncTask(@NonNull HttpFetcher mySegmentsFetc
config,
targetSegmentsChangeNumber,
targetLargeSegmentsChangeNumber,
- new ReconnectBackoffCounter(1, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT),
+ new ExponentialBackoffCounter(1, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT),
ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES);
}
diff --git a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java
index 77ddd812d..42cadcde1 100644
--- a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java
+++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java
@@ -3,7 +3,7 @@
import androidx.annotation.NonNull;
import io.split.android.client.events.SplitInternalEvent;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.telemetry.model.OperationType;
public class MySegmentsSyncTaskConfig {
diff --git a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java
index 5a3caf515..ad5ba08eb 100644
--- a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java
+++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java
@@ -3,7 +3,7 @@
import androidx.annotation.NonNull;
import io.split.android.client.events.SplitInternalEvent;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum;
public class MySegmentsUpdateTaskConfig {
diff --git a/main/src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java b/main/src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java
index ba8731c4a..3a740d6f5 100644
--- a/main/src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java
+++ b/main/src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java
@@ -6,7 +6,7 @@
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.storage.rbs.RuleBasedSegmentStorage;
import io.split.android.client.utils.logger.Logger;
diff --git a/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java
index 72c05e4a2..656b5aff0 100644
--- a/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java
+++ b/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java
@@ -10,7 +10,7 @@
import io.split.android.client.events.metadata.EventMetadataHelpers;
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.storage.rbs.RuleBasedSegmentStorage;
import io.split.android.client.utils.logger.Logger;
diff --git a/main/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java b/main/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java
index 2fb16db3a..6b6357f3c 100644
--- a/main/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java
+++ b/main/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java
@@ -12,7 +12,7 @@
import io.split.android.client.dtos.Split;
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.storage.splits.PersistentSplitsStorage;
import io.split.android.client.utils.logger.Logger;
diff --git a/main/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java b/main/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java
index 49ca1d512..ab9731bb3 100644
--- a/main/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java
+++ b/main/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java
@@ -7,7 +7,7 @@
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.storage.splits.SplitsStorage;
import io.split.android.client.utils.logger.Logger;
diff --git a/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java
index 6fb8fc8dc..2b6d886b5 100644
--- a/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java
+++ b/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java
@@ -10,7 +10,7 @@
import io.split.android.client.events.metadata.EventMetadataHelpers;
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.rules.ProcessedRuleBasedSegmentChange;
import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor;
import io.split.android.client.storage.rbs.RuleBasedSegmentStorage;
diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java
index 2c86042b0..2b8345f7d 100644
--- a/main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java
+++ b/main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java
@@ -14,7 +14,7 @@
import io.split.android.client.events.metadata.EventMetadataHelpers;
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.storage.splits.ProcessedSplitChange;
import io.split.android.client.storage.splits.SplitsStorage;
import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum;
diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java
index 001d4ec04..53ee6f518 100644
--- a/main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java
+++ b/main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java
@@ -13,7 +13,7 @@
import io.split.android.client.events.metadata.EventMetadataHelpers;
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.storage.splits.SplitsStorage;
import io.split.android.client.utils.logger.Logger;
diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java
index 705331080..febbc82c7 100644
--- a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java
+++ b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java
@@ -25,15 +25,15 @@
import io.split.android.client.network.SplitHttpHeadersBuilder;
import io.split.android.client.service.ServiceConstants;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.http.HttpFetcher;
import io.split.android.client.service.http.HttpFetcherException;
import io.split.android.client.service.http.HttpStatus;
import io.split.android.client.service.rules.ProcessedRuleBasedSegmentChange;
import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor;
import io.split.android.client.storage.splits.ProcessedSplitChange;
-import io.split.android.client.service.sseclient.BackoffCounter;
-import io.split.android.client.service.sseclient.ReconnectBackoffCounter;
+import io.split.android.client.backoff.BackoffCounter;
+import io.split.android.client.backoff.ExponentialBackoffCounter;
import io.split.android.client.storage.general.GeneralInfoStorage;
import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer;
import io.split.android.client.storage.splits.SplitsStorage;
@@ -81,7 +81,7 @@ public SplitsSyncHelper(@NonNull HttpFetcher splitFetcher,
ruleBasedSegmentStorage,
generalInfoStorage,
telemetryRuntimeProducer,
- new ReconnectBackoffCounter(1, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT),
+ new ExponentialBackoffCounter(1, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT),
flagsSpec,
forBackgroundSync,
DEFAULT_PROXY_CHECK_INTERVAL_MILLIS,
@@ -331,7 +331,9 @@ private void updateStorage(boolean clearBeforeUpdate, SplitChange splitChange, R
mLastProcessedSplitChange.set(processedSplitChange);
}
mSplitsStorage.update(processedSplitChange, mExecutor);
- updateRbsStorage(ruleBasedSegmentChange);
+ if (ruleBasedSegmentChange != null) {
+ updateRbsStorage(ruleBasedSegmentChange);
+ }
}
private boolean hasFlagUpdates(@Nullable ProcessedSplitChange processedSplitChange) {
diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java
index d14600725..8bddb0d04 100644
--- a/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java
+++ b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java
@@ -17,7 +17,7 @@
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
import io.split.android.client.service.executor.SplitTaskExecutionStatus;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.synchronizer.SplitsChangeChecker;
import io.split.android.client.storage.rbs.RuleBasedSegmentStorage;
import io.split.android.client.storage.splits.SplitsStorage;
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/StreamingConstants.java b/main/src/main/java/io/split/android/client/service/sseclient/StreamingConstants.java
new file mode 100644
index 000000000..58c1b5e6a
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/StreamingConstants.java
@@ -0,0 +1,21 @@
+package io.split.android.client.service.sseclient;
+
+/**
+ * Constants used by the streaming module.
+ */
+public final class StreamingConstants {
+
+ private StreamingConstants() {
+ // Utility class
+ }
+
+ /**
+ * Buffer size for segment data decompression.
+ */
+ public static final int SEGMENT_DATA_BUFFER_SIZE = 1024 * 10; // 10KB
+
+ /**
+ * Query param for flags spec in streaming auth.
+ */
+ public static final String FLAGS_SPEC_PARAM = "s";
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java
index cff8533d7..8a71d0945 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java
@@ -4,7 +4,7 @@
import com.google.gson.annotations.SerializedName;
-import io.split.android.client.common.CompressionType;
+import io.split.android.client.streaming.support.CompressionType;
public abstract class InstantUpdateChangeNotification extends IncomingNotification {
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java
index 20cfea311..2ed067ad1 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java
@@ -6,7 +6,7 @@
import java.util.Set;
-import io.split.android.client.common.CompressionType;
+import io.split.android.client.streaming.support.CompressionType;
public class MembershipNotification extends IncomingNotification {
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsV2PayloadDecoder.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsV2PayloadDecoder.java
index c646c4c0b..44e07da81 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsV2PayloadDecoder.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsV2PayloadDecoder.java
@@ -5,7 +5,7 @@
import io.split.android.client.exceptions.MySegmentsParsingException;
import io.split.android.client.utils.Base64Util;
-import io.split.android.client.utils.CompressionUtil;
+import io.split.android.client.streaming.support.CompressionUtil;
import io.split.android.client.utils.MurmurHash3;
import io.split.android.client.utils.StringHelper;
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java
index a9eb549c3..5311815e3 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java
@@ -5,8 +5,8 @@
import java.util.Set;
import java.util.concurrent.BlockingQueue;
-import io.split.android.client.common.CompressionType;
-import io.split.android.client.common.CompressionUtilProvider;
+import io.split.android.client.streaming.support.CompressionType;
+import io.split.android.client.streaming.support.CompressionUtilProvider;
import io.split.android.client.service.executor.SplitTaskExecutor;
import io.split.android.client.service.mysegments.MySegmentUpdateParams;
import io.split.android.client.service.mysegments.MySegmentsUpdateTask;
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java
index 898ea21d9..a90e4307c 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java
@@ -4,7 +4,7 @@
import androidx.annotation.NonNull;
-import io.split.android.client.common.CompressionUtilProvider;
+import io.split.android.client.streaming.support.CompressionUtilProvider;
import io.split.android.client.service.executor.SplitTaskExecutor;
import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder;
import io.split.android.client.service.sseclient.notifications.NotificationParser;
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java b/main/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java
index 44f4a1c1b..291175842 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java
@@ -8,7 +8,7 @@
import java.util.concurrent.BlockingQueue;
-import io.split.android.client.common.CompressionUtilProvider;
+import io.split.android.client.streaming.support.CompressionUtilProvider;
import io.split.android.client.dtos.Helper;
import io.split.android.client.dtos.RuleBasedSegment;
import io.split.android.client.dtos.Split;
@@ -23,7 +23,7 @@
import io.split.android.client.storage.rbs.RuleBasedSegmentStorage;
import io.split.android.client.storage.splits.SplitsStorage;
import io.split.android.client.utils.Base64Util;
-import io.split.android.client.utils.CompressionUtil;
+import io.split.android.client.streaming.support.CompressionUtil;
import io.split.android.client.utils.Json;
import io.split.android.client.utils.logger.Logger;
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingAuthException.java b/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingAuthException.java
new file mode 100644
index 000000000..de3a8c2b0
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingAuthException.java
@@ -0,0 +1,32 @@
+package io.split.android.client.service.sseclient.spi;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Exception thrown by streaming auth fetchers.
+ */
+public class StreamingAuthException extends Exception {
+
+ @Nullable
+ private final Integer mStatusCode;
+
+ public StreamingAuthException(String message) {
+ super(message);
+ mStatusCode = null;
+ }
+
+ public StreamingAuthException(String message, Throwable cause) {
+ super(message, cause);
+ mStatusCode = null;
+ }
+
+ public StreamingAuthException(String message, Throwable cause, Integer statusCode) {
+ super(message, cause);
+ mStatusCode = statusCode;
+ }
+
+ @Nullable
+ public Integer getStatusCode() {
+ return mStatusCode;
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingAuthFetcher.java b/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingAuthFetcher.java
new file mode 100644
index 000000000..e722c1962
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingAuthFetcher.java
@@ -0,0 +1,23 @@
+package io.split.android.client.service.sseclient.spi;
+
+import androidx.annotation.NonNull;
+
+import java.util.Map;
+
+import io.split.android.client.service.sseclient.SseAuthenticationResponse;
+
+/**
+ * Abstraction for fetching streaming authentication tokens.
+ */
+public interface StreamingAuthFetcher {
+
+ /**
+ * Executes the auth request with the provided parameters.
+ *
+ * @param params request parameters
+ * @return authentication response
+ * @throws StreamingAuthException when request fails
+ */
+ @NonNull
+ SseAuthenticationResponse execute(@NonNull Map params) throws StreamingAuthException;
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingScheduler.java b/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingScheduler.java
new file mode 100644
index 000000000..ff1e2d185
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingScheduler.java
@@ -0,0 +1,40 @@
+package io.split.android.client.service.sseclient.spi;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Interface for scheduling delayed tasks within the streaming module.
+ * Implementations should provide timer/scheduling capabilities backed
+ * by the host application's task executor.
+ */
+public interface StreamingScheduler {
+
+ /**
+ * Schedules a task to run after the specified delay.
+ *
+ * @param task the runnable to execute
+ * @param delaySeconds delay before execution in seconds
+ * @param listener optional listener to be notified when task completes
+ * @return a unique task ID that can be used to cancel the task
+ */
+ @NonNull
+ String schedule(@NonNull Runnable task, long delaySeconds, @Nullable TaskExecutionListener listener);
+
+ /**
+ * Cancels a previously scheduled task.
+ *
+ * @param taskId the ID returned by schedule()
+ */
+ void cancel(@Nullable String taskId);
+
+ /**
+ * Listener interface for task completion notifications.
+ */
+ interface TaskExecutionListener {
+ /**
+ * Called when a scheduled task has completed execution.
+ */
+ void onTaskExecuted();
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingTelemetry.java b/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingTelemetry.java
new file mode 100644
index 000000000..506f8229e
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/spi/StreamingTelemetry.java
@@ -0,0 +1,104 @@
+package io.split.android.client.service.sseclient.spi;
+
+/**
+ * Interface for recording streaming-related telemetry.
+ * Implementations should bridge to the host application's telemetry system.
+ */
+public interface StreamingTelemetry {
+
+ /**
+ * Records a sync latency measurement for token operations.
+ *
+ * @param latencyMillis the latency in milliseconds
+ */
+ void recordTokenSyncLatency(long latencyMillis);
+
+ /**
+ * Records a successful token sync operation.
+ *
+ * @param timestamp the timestamp of the sync
+ */
+ void recordTokenSuccessfulSync(long timestamp);
+
+ /**
+ * Records a token sync error.
+ *
+ * @param httpStatus the HTTP status code
+ */
+ void recordTokenSyncError(Integer httpStatus);
+
+ /**
+ * Records an authentication rejection.
+ */
+ void recordAuthRejections();
+
+ /**
+ * Records a token refresh.
+ */
+ void recordTokenRefreshes();
+
+ /**
+ * Records a token refresh streaming event.
+ *
+ * @param expirationTime the token expiration time
+ * @param timestamp the timestamp
+ */
+ void recordTokenRefreshEvent(long expirationTime, long timestamp);
+
+ /**
+ * Records a sync mode update (streaming enabled).
+ *
+ * @param streaming true if streaming mode, false if polling
+ * @param timestamp the timestamp
+ */
+ void recordSyncModeUpdate(boolean streaming, long timestamp);
+
+ /**
+ * Records an SSE connection error.
+ *
+ * @param retryable true if the error is retryable
+ * @param timestamp the timestamp
+ */
+ void recordConnectionError(boolean retryable, long timestamp);
+
+ /**
+ * Records an Ably error.
+ *
+ * @param errorCode the error code
+ * @param timestamp the timestamp
+ */
+ void recordAblyError(int errorCode, long timestamp);
+
+ /**
+ * Records an occupancy event on the primary channel.
+ *
+ * @param publisherCount the publisher count
+ * @param timestamp the timestamp
+ */
+ void recordOccupancyPri(int publisherCount, long timestamp);
+
+ /**
+ * Records an occupancy event on the secondary channel.
+ *
+ * @param publisherCount the publisher count
+ * @param timestamp the timestamp
+ */
+ void recordOccupancySec(int publisherCount, long timestamp);
+
+ /**
+ * Records a streaming status change.
+ *
+ * @param status the new status (ENABLED, PAUSED, DISABLED)
+ * @param timestamp the timestamp
+ */
+ void recordStreamingStatus(StreamingStatus status, long timestamp);
+
+ /**
+ * Streaming status values.
+ */
+ enum StreamingStatus {
+ ENABLED,
+ PAUSED,
+ DISABLED
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/spi/UpdateNotificationListener.java b/main/src/main/java/io/split/android/client/service/sseclient/spi/UpdateNotificationListener.java
new file mode 100644
index 000000000..66e3402e0
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/spi/UpdateNotificationListener.java
@@ -0,0 +1,25 @@
+package io.split.android.client.service.sseclient.spi;
+
+import androidx.annotation.NonNull;
+
+import io.split.android.client.service.sseclient.notifications.IncomingNotification;
+
+/**
+ * Listener interface for update notifications from the streaming module.
+ * Host applications implement this to handle split/RBS/kill/membership updates.
+ */
+public interface UpdateNotificationListener {
+
+ /**
+ * Called when an update notification is received.
+ * The notification type can be checked to determine the specific update type:
+ * - SPLIT_UPDATE
+ * - SPLIT_KILL
+ * - RULE_BASED_SEGMENT_UPDATE
+ * - MEMBERSHIPS_MS_UPDATE
+ * - MEMBERSHIPS_LS_UPDATE
+ *
+ * @param notification the incoming update notification
+ */
+ void onUpdateNotification(@NonNull IncomingNotification notification);
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java
index fa682d765..cbc9a5b28 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java
@@ -8,7 +8,7 @@
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
import io.split.android.client.service.executor.SplitTaskExecutionListener;
import io.split.android.client.service.executor.SplitTaskExecutor;
-import io.split.android.client.service.sseclient.BackoffCounter;
+import io.split.android.client.backoff.BackoffCounter;
import io.split.android.client.utils.logger.Logger;
public class BackoffCounterTimer implements SplitTaskExecutionListener {
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/DefaultSseClient.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/DefaultSseClient.java
new file mode 100644
index 000000000..105326282
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/DefaultSseClient.java
@@ -0,0 +1,113 @@
+package io.split.android.client.service.sseclient.sseclient;
+
+import static io.split.android.client.utils.Utils.checkNotNull;
+
+import androidx.annotation.NonNull;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+
+import io.split.android.client.network.URIBuilder;
+import io.split.android.client.service.sseclient.EventStreamParser;
+import io.split.android.client.service.sseclient.SseJwtToken;
+import io.split.android.client.utils.StringHelper;
+import io.split.android.client.utils.logger.Logger;
+
+/**
+ * Split-specific SSE client adapter.
+ *
+ * Builds the Split streaming URL from an {@link SseJwtToken}
+ * (channels, access token, version) and delegates the actual
+ * SSE connection to a generic {@link EventSourceClient}.
+ *
+ * Incoming SSE events are routed through {@link SseHandler}
+ * for Split notification processing.
+ */
+public class DefaultSseClient implements SseClient {
+
+ private static final String PUSH_NOTIFICATION_CHANNELS_PARAM = "channel";
+ private static final String PUSH_NOTIFICATION_TOKEN_PARAM = "accessToken";
+ private static final String PUSH_NOTIFICATION_VERSION_PARAM = "v";
+ private static final String PUSH_NOTIFICATION_VERSION_VALUE = "1.1";
+
+ private final URI mTargetUrl;
+ private final EventSourceClient mEventSourceClient;
+ private final SseHandler mSseHandler;
+ private final StringHelper mStringHelper;
+
+ public DefaultSseClient(@NonNull URI uri,
+ @NonNull EventSourceClient eventSourceClient,
+ @NonNull SseHandler sseHandler) {
+ mTargetUrl = checkNotNull(uri);
+ mEventSourceClient = checkNotNull(eventSourceClient);
+ mSseHandler = checkNotNull(sseHandler);
+ mStringHelper = new StringHelper();
+ }
+
+ @Override
+ public int status() {
+ return mEventSourceClient.status();
+ }
+
+ @Override
+ public void disconnect() {
+ mEventSourceClient.disconnect();
+ }
+
+ @Override
+ public void connect(SseJwtToken token, ConnectionListener connectionListener) {
+ String channels = mStringHelper.join(",", token.getChannels());
+ String rawToken = token.getRawJwt();
+
+ try {
+ URI url = new URIBuilder(mTargetUrl)
+ .addParameter(PUSH_NOTIFICATION_VERSION_PARAM, PUSH_NOTIFICATION_VERSION_VALUE)
+ .addParameter(PUSH_NOTIFICATION_CHANNELS_PARAM, channels)
+ .addParameter(PUSH_NOTIFICATION_TOKEN_PARAM, rawToken)
+ .build();
+
+ mEventSourceClient.connect(url, new EventSourceClient.EventHandler() {
+ private boolean isConnectionConfirmed = false;
+
+ @Override
+ public void onOpen() {
+ Logger.d("Streaming connection opened");
+ }
+
+ @Override
+ public void onMessage(@NonNull Map event) {
+ if (!isConnectionConfirmed) {
+ boolean isKeepAlive = EventStreamParser.KEEP_ALIVE_EVENT.equals(
+ event.get(EventStreamParser.EVENT_FIELD));
+ if (isKeepAlive || mSseHandler.isConnectionConfirmed(event)) {
+ Logger.d("Streaming connection success");
+ isConnectionConfirmed = true;
+ connectionListener.onConnectionSuccess();
+ } else {
+ Logger.d("Streaming error after connection");
+ boolean retryable = mSseHandler.isRetryableError(event);
+ mSseHandler.handleError(retryable);
+ mEventSourceClient.disconnect();
+ return;
+ }
+ }
+
+ boolean isKeepAlive = EventStreamParser.KEEP_ALIVE_EVENT.equals(
+ event.get(EventStreamParser.EVENT_FIELD));
+ if (!isKeepAlive) {
+ mSseHandler.handleIncomingMessage(event);
+ }
+ }
+
+ @Override
+ public void onError(boolean retryable) {
+ mSseHandler.handleError(retryable);
+ }
+ });
+ } catch (URISyntaxException e) {
+ Logger.e("An error has occurred while creating stream URL: " + e.getLocalizedMessage());
+ mSseHandler.handleError(false);
+ }
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/HttpClientStreamingTransport.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/HttpClientStreamingTransport.java
new file mode 100644
index 000000000..aca2d3eba
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/HttpClientStreamingTransport.java
@@ -0,0 +1,92 @@
+package io.split.android.client.service.sseclient.sseclient;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.net.URI;
+
+import io.split.android.client.network.HttpClient;
+import io.split.android.client.network.HttpException;
+import io.split.android.client.network.HttpStreamRequest;
+import io.split.android.client.network.HttpStreamResponse;
+import io.split.android.client.service.sseclient.spi.StreamingTransport;
+
+/**
+ * Adapter that implements StreamingTransport using HttpClient.
+ */
+public class HttpClientStreamingTransport implements StreamingTransport {
+
+ private final HttpClient mHttpClient;
+
+ public HttpClientStreamingTransport(@NonNull HttpClient httpClient) {
+ mHttpClient = httpClient;
+ }
+
+ @NonNull
+ @Override
+ public StreamingConnection connect(@NonNull URI uri) {
+ return new HttpClientStreamingConnection(mHttpClient.streamRequest(uri));
+ }
+
+ private static class HttpClientStreamingConnection implements StreamingConnection {
+ private final HttpStreamRequest mRequest;
+
+ HttpClientStreamingConnection(HttpStreamRequest request) {
+ mRequest = request;
+ }
+
+ @NonNull
+ @Override
+ public StreamingResponse execute() throws StreamingTransportException {
+ try {
+ HttpStreamResponse response = mRequest.execute();
+ return new HttpClientStreamingResponse(response);
+ } catch (HttpException e) {
+ throw new StreamingTransportException(e.getMessage(), e, e.getStatusCode());
+ } catch (IOException e) {
+ throw new StreamingTransportException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void close() {
+ mRequest.close();
+ }
+ }
+
+ private static class HttpClientStreamingResponse implements StreamingResponse {
+ private final HttpStreamResponse mResponse;
+
+ HttpClientStreamingResponse(HttpStreamResponse response) {
+ mResponse = response;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return mResponse.isSuccess();
+ }
+
+ @Override
+ public int getHttpStatus() {
+ return mResponse.getHttpStatus();
+ }
+
+ @Override
+ public boolean isClientRelatedError() {
+ return mResponse.isClientRelatedError();
+ }
+
+ @Nullable
+ @Override
+ public BufferedReader getBufferedReader() {
+ return mResponse.getBufferedReader();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mResponse.close();
+ }
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/HttpFetcherStreamingAuthFetcher.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/HttpFetcherStreamingAuthFetcher.java
new file mode 100644
index 000000000..fee0d7987
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/HttpFetcherStreamingAuthFetcher.java
@@ -0,0 +1,35 @@
+package io.split.android.client.service.sseclient.sseclient;
+
+import androidx.annotation.NonNull;
+
+import java.util.Map;
+
+import io.split.android.client.service.http.HttpFetcher;
+import io.split.android.client.service.http.HttpFetcherException;
+import io.split.android.client.service.sseclient.SseAuthenticationResponse;
+import io.split.android.client.service.sseclient.spi.StreamingAuthException;
+import io.split.android.client.service.sseclient.spi.StreamingAuthFetcher;
+
+/**
+ * Adapter that implements StreamingAuthFetcher using HttpFetcher.
+ */
+public class HttpFetcherStreamingAuthFetcher implements StreamingAuthFetcher {
+
+ private final HttpFetcher mAuthFetcher;
+
+ public HttpFetcherStreamingAuthFetcher(@NonNull HttpFetcher authFetcher) {
+ mAuthFetcher = authFetcher;
+ }
+
+ @NonNull
+ @Override
+ public SseAuthenticationResponse execute(@NonNull Map params) throws StreamingAuthException {
+ try {
+ return mAuthFetcher.execute(params, null);
+ } catch (HttpFetcherException e) {
+ throw new StreamingAuthException(e.getLocalizedMessage(), e, e.getHttpStatus());
+ } catch (Exception e) {
+ throw new StreamingAuthException(e.getLocalizedMessage(), e);
+ }
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java
index eeba8744d..da8f9a9ef 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java
@@ -15,10 +15,7 @@
import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent.EventType;
import io.split.android.client.service.sseclient.notifications.ControlNotification;
import io.split.android.client.service.sseclient.notifications.OccupancyNotification;
-import io.split.android.client.telemetry.model.streaming.OccupancyPriStreamingEvent;
-import io.split.android.client.telemetry.model.streaming.OccupancySecStreamingEvent;
-import io.split.android.client.telemetry.model.streaming.StreamingStatusStreamingEvent;
-import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
+import io.split.android.client.service.sseclient.spi.StreamingTelemetry;
import io.split.android.client.utils.logger.Logger;
public class NotificationManagerKeeper {
@@ -40,11 +37,11 @@ public Publisher(int count, long lastTimestamp) {
private final PushManagerEventBroadcaster mBroadcasterChannel;
private final AtomicLong mLastControlTimestamp = new AtomicLong(0);
private final AtomicBoolean mIsStreamingActive = new AtomicBoolean(true);
- private final TelemetryRuntimeProducer mTelemetryRuntimeProducer;
+ private final StreamingTelemetry mTelemetry;
- public NotificationManagerKeeper(PushManagerEventBroadcaster broadcasterChannel, TelemetryRuntimeProducer telemetryRuntimeProducer) {
+ public NotificationManagerKeeper(PushManagerEventBroadcaster broadcasterChannel, StreamingTelemetry telemetry) {
mBroadcasterChannel = broadcasterChannel;
- mTelemetryRuntimeProducer = telemetryRuntimeProducer;
+ mTelemetry = telemetry;
/// By default we consider one publisher en primary channel available
mPublishers.put(CHANNEL_PRI_KEY, new Publisher(1, 0));
mPublishers.put(CHANNEL_SEC_KEY, new Publisher(0, 0));
@@ -60,20 +57,20 @@ public void handleControlNotification(ControlNotification notification) {
case STREAMING_PAUSED:
mIsStreamingActive.set(false);
mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_SUBSYSTEM_DOWN));
- mTelemetryRuntimeProducer.recordStreamingEvents(new StreamingStatusStreamingEvent(StreamingStatusStreamingEvent.Status.PAUSED, System.currentTimeMillis()));
+ mTelemetry.recordStreamingStatus(StreamingTelemetry.StreamingStatus.PAUSED, System.currentTimeMillis());
break;
case STREAMING_DISABLED:
mIsStreamingActive.set(false);
mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_DISABLED));
- mTelemetryRuntimeProducer.recordStreamingEvents(new StreamingStatusStreamingEvent(StreamingStatusStreamingEvent.Status.DISABLED, System.currentTimeMillis()));
+ mTelemetry.recordStreamingStatus(StreamingTelemetry.StreamingStatus.DISABLED, System.currentTimeMillis());
break;
case STREAMING_RESUMED:
mIsStreamingActive.set(true);
if (publishersCount() > 0) {
mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_SUBSYSTEM_UP));
- mTelemetryRuntimeProducer.recordStreamingEvents(new StreamingStatusStreamingEvent(StreamingStatusStreamingEvent.Status.ENABLED, System.currentTimeMillis()));
+ mTelemetry.recordStreamingStatus(StreamingTelemetry.StreamingStatus.ENABLED, System.currentTimeMillis());
}
break;
@@ -103,9 +100,9 @@ public void handleOccupancyNotification(OccupancyNotification notification) {
updateChannelInfo(channelKey, notification.getMetrics().getPublishers(), notification.getTimestamp());
if (CHANNEL_PRI_KEY.equals(channelKey)) {
- mTelemetryRuntimeProducer.recordStreamingEvents(new OccupancyPriStreamingEvent(publishersCount(), System.currentTimeMillis()));
+ mTelemetry.recordOccupancyPri(publishersCount(), System.currentTimeMillis());
} else if (CHANNEL_SEC_KEY.equals(channelKey)) {
- mTelemetryRuntimeProducer.recordStreamingEvents(new OccupancySecStreamingEvent(publishersCount(), System.currentTimeMillis()));
+ mTelemetry.recordOccupancySec(publishersCount(), System.currentTimeMillis());
}
if (publishersCount() == 0 && prevPublishersCount > 0) {
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationProcessorUpdateListener.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationProcessorUpdateListener.java
new file mode 100644
index 000000000..2d4aaa0bc
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationProcessorUpdateListener.java
@@ -0,0 +1,24 @@
+package io.split.android.client.service.sseclient.sseclient;
+
+import androidx.annotation.NonNull;
+
+import io.split.android.client.service.sseclient.notifications.IncomingNotification;
+import io.split.android.client.service.sseclient.notifications.NotificationProcessor;
+import io.split.android.client.service.sseclient.spi.UpdateNotificationListener;
+
+/**
+ * Adapter that forwards update notifications to NotificationProcessor.
+ */
+public class NotificationProcessorUpdateListener implements UpdateNotificationListener {
+
+ private final NotificationProcessor mNotificationProcessor;
+
+ public NotificationProcessorUpdateListener(@NonNull NotificationProcessor notificationProcessor) {
+ mNotificationProcessor = notificationProcessor;
+ }
+
+ @Override
+ public void onUpdateNotification(@NonNull IncomingNotification notification) {
+ mNotificationProcessor.process(notification);
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java
index 5217889b2..9cb4a546e 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java
@@ -13,20 +13,13 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
-import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor;
-import io.split.android.client.service.executor.SplitTask;
-import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
-import io.split.android.client.service.executor.ThreadFactoryBuilder;
import io.split.android.client.service.sseclient.SseJwtToken;
import io.split.android.client.service.sseclient.feedbackchannel.DelayStatusEvent;
import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster;
import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent;
import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent.EventType;
-import io.split.android.client.telemetry.model.OperationType;
-import io.split.android.client.telemetry.model.streaming.SyncModeUpdateStreamingEvent;
-import io.split.android.client.telemetry.model.streaming.TokenRefreshStreamingEvent;
-import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
+import io.split.android.client.service.sseclient.spi.StreamingScheduler;
+import io.split.android.client.service.sseclient.spi.StreamingTelemetry;
import io.split.android.client.utils.logger.Logger;
public class PushNotificationManager {
@@ -37,21 +30,22 @@ public class PushNotificationManager {
private final PushManagerEventBroadcaster mBroadcasterChannel;
private final SseAuthenticator mSseAuthenticator;
private final SseClient mSseClient;
- private final TelemetryRuntimeProducer mTelemetryRuntimeProducer;
+ private final StreamingTelemetry mTelemetry;
private final SseRefreshTokenTimer mRefreshTokenTimer;
private final SseDisconnectionTimer mDisconnectionTimer;
private final AtomicBoolean mIsPaused;
private final AtomicBoolean mIsStopped;
private Future> mConnectionTask;
- private final SplitTask mBackgroundDisconnectionTask;
+ private final Runnable mBackgroundDisconnectionTask;
private final long mDefaultSSEConnectionDelayInSecs;
public PushNotificationManager(@NonNull PushManagerEventBroadcaster pushManagerEventBroadcaster,
@NonNull SseAuthenticator sseAuthenticator,
@NonNull SseClient sseClient,
@NonNull SseRefreshTokenTimer refreshTokenTimer,
- @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer,
+ @NonNull StreamingScheduler scheduler,
+ @NonNull StreamingTelemetry telemetry,
long defaultSSEConnectionDelayInSecs,
int sseDisconnectionDelayInSecs,
@Nullable ScheduledExecutorService executorService) {
@@ -59,8 +53,8 @@ public PushNotificationManager(@NonNull PushManagerEventBroadcaster pushManagerE
sseAuthenticator,
sseClient,
refreshTokenTimer,
- new SseDisconnectionTimer(new SplitSingleThreadTaskExecutor(), sseDisconnectionDelayInSecs),
- telemetryRuntimeProducer,
+ new SseDisconnectionTimer(scheduler, sseDisconnectionDelayInSecs),
+ telemetry,
defaultSSEConnectionDelayInSecs,
executorService);
}
@@ -71,7 +65,7 @@ public PushNotificationManager(@NonNull PushManagerEventBroadcaster broadcasterC
@NonNull SseClient sseClient,
@NonNull SseRefreshTokenTimer refreshTokenTimer,
@NonNull SseDisconnectionTimer disconnectionTimer,
- @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer,
+ @NonNull StreamingTelemetry telemetry,
long defaultSSEConnectionDelayInSecs,
@Nullable ScheduledExecutorService executor) {
mBroadcasterChannel = checkNotNull(broadcasterChannel);
@@ -79,7 +73,7 @@ public PushNotificationManager(@NonNull PushManagerEventBroadcaster broadcasterC
mSseClient = checkNotNull(sseClient);
mRefreshTokenTimer = checkNotNull(refreshTokenTimer);
mDisconnectionTimer = checkNotNull(disconnectionTimer);
- mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer);
+ mTelemetry = checkNotNull(telemetry);
mIsStopped = new AtomicBoolean(false);
mIsPaused = new AtomicBoolean(false);
mBackgroundDisconnectionTask = new BackgroundDisconnectionTask(mSseClient, mRefreshTokenTimer);
@@ -92,7 +86,7 @@ public PushNotificationManager(@NonNull PushManagerEventBroadcaster broadcasterC
}
public synchronized void start() {
- mTelemetryRuntimeProducer.recordStreamingEvents(new SyncModeUpdateStreamingEvent(SyncModeUpdateStreamingEvent.Mode.STREAMING, System.currentTimeMillis()));
+ mTelemetry.recordSyncModeUpdate(true, System.currentTimeMillis());
Logger.d("Push notification manager started");
connect();
}
@@ -157,17 +151,13 @@ private void shutdownAndAwaitTermination() {
}
private ScheduledThreadPoolExecutor buildExecutor() {
- ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder();
- threadFactoryBuilder.setDaemon(true);
- threadFactoryBuilder.setNameFormat("split-sse_client-%d");
- threadFactoryBuilder.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
- @Override
- public void uncaughtException(Thread t, Throwable e) {
- Logger.e(e, "Error in thread: %s", t.getName());
- }
+ return new ScheduledThreadPoolExecutor(POOL_SIZE, runnable -> {
+ Thread thread = new Thread(runnable);
+ thread.setDaemon(true);
+ thread.setName("split-sse_client-" + thread.getId());
+ thread.setUncaughtExceptionHandler((t, e) -> Logger.e(e, "Error in thread: %s", t.getName()));
+ return thread;
});
-
- return new ScheduledThreadPoolExecutor(POOL_SIZE, threadFactoryBuilder.build());
}
private class StreamingConnection implements Runnable {
@@ -183,7 +173,7 @@ public void run() {
long startTime = System.currentTimeMillis();
SseAuthenticationResult authResult = mSseAuthenticator.authenticate(mDefaultSSEConnectionDelayInSecs);
- mTelemetryRuntimeProducer.recordSyncLatency(OperationType.TOKEN, System.currentTimeMillis() - startTime);
+ mTelemetry.recordTokenSyncLatency(System.currentTimeMillis() - startTime);
if (authResult.isSuccess() && !authResult.isPushEnabled()) {
handlePushDisabled();
@@ -221,7 +211,7 @@ public void run() {
return;
}
- mSseClient.connect(token, new SseClientImpl.ConnectionListener() {
+ mSseClient.connect(token, new SseClient.ConnectionListener() {
@Override
public void onConnectionSuccess() {
mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_SUBSYSTEM_UP));
@@ -231,9 +221,9 @@ public void onConnectionSuccess() {
}
private void recordSuccessfulSyncAndTokenRefreshes(SseJwtToken token) {
- mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.TOKEN, System.currentTimeMillis());
- mTelemetryRuntimeProducer.recordStreamingEvents(new TokenRefreshStreamingEvent(token.getExpirationTime(), System.currentTimeMillis()));
- mTelemetryRuntimeProducer.recordTokenRefreshes();
+ mTelemetry.recordTokenSuccessfulSync(System.currentTimeMillis());
+ mTelemetry.recordTokenRefreshEvent(token.getExpirationTime(), System.currentTimeMillis());
+ mTelemetry.recordTokenRefreshes();
}
private void handlePushDisabled() {
@@ -249,9 +239,9 @@ private void handleNonRetryableError(SseAuthenticationResult authResult) {
}
private void recordNonRetryableError(SseAuthenticationResult authResult) {
- mTelemetryRuntimeProducer.recordAuthRejections();
+ mTelemetry.recordAuthRejections();
if (authResult.getHttpStatus() != null) {
- mTelemetryRuntimeProducer.recordSyncError(OperationType.TOKEN, authResult.getHttpStatus());
+ mTelemetry.recordTokenSyncError(authResult.getHttpStatus());
}
}
@@ -275,7 +265,7 @@ private boolean delay(long seconds) {
}
}
- public static class BackgroundDisconnectionTask implements SplitTask {
+ public static class BackgroundDisconnectionTask implements Runnable {
private final SseClient mSseClient;
private final SseRefreshTokenTimer mRefreshTokenTimer;
@@ -286,13 +276,11 @@ public BackgroundDisconnectionTask(SseClient sseClient, SseRefreshTokenTimer ref
mRefreshTokenTimer = refreshTokenTimer;
}
- @NonNull
@Override
- public SplitTaskExecutionInfo execute() {
+ public void run() {
Logger.d("Disconnecting streaming while in background");
mSseClient.disconnect();
mRefreshTokenTimer.cancel();
- return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK);
}
}
}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java
index 79c14f699..08ad35af8 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java
@@ -11,7 +11,7 @@
import io.split.android.client.service.executor.SplitTaskExecutionListener;
import io.split.android.client.service.executor.SplitTaskExecutionStatus;
import io.split.android.client.service.executor.SplitTaskExecutor;
-import io.split.android.client.service.sseclient.BackoffCounter;
+import io.split.android.client.backoff.BackoffCounter;
import io.split.android.client.utils.logger.Logger;
import java.util.concurrent.TimeUnit;
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SplitTaskExecutorStreamingScheduler.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SplitTaskExecutorStreamingScheduler.java
new file mode 100644
index 000000000..55180f313
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SplitTaskExecutorStreamingScheduler.java
@@ -0,0 +1,54 @@
+package io.split.android.client.service.sseclient.sseclient;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import io.split.android.client.service.executor.SplitTask;
+import io.split.android.client.service.executor.SplitTaskExecutionInfo;
+import io.split.android.client.service.executor.SplitTaskExecutionListener;
+import io.split.android.client.service.executor.SplitTaskExecutor;
+import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.sseclient.spi.StreamingScheduler;
+
+/**
+ * Adapter that implements StreamingScheduler using SplitTaskExecutor.
+ */
+public class SplitTaskExecutorStreamingScheduler implements StreamingScheduler {
+
+ private final SplitTaskExecutor mTaskExecutor;
+
+ public SplitTaskExecutorStreamingScheduler(@NonNull SplitTaskExecutor taskExecutor) {
+ mTaskExecutor = taskExecutor;
+ }
+
+ @NonNull
+ @Override
+ public String schedule(@NonNull Runnable task, long delaySeconds, @Nullable TaskExecutionListener listener) {
+ return mTaskExecutor.schedule(new SplitTask() {
+ @NonNull
+ @Override
+ public SplitTaskExecutionInfo execute() {
+ try {
+ task.run();
+ return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK);
+ } catch (Exception e) {
+ return SplitTaskExecutionInfo.error(SplitTaskType.GENERIC_TASK);
+ }
+ }
+ }, delaySeconds, new SplitTaskExecutionListener() {
+ @Override
+ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) {
+ if (listener != null) {
+ listener.onTaskExecuted();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void cancel(@Nullable String taskId) {
+ if (taskId != null) {
+ mTaskExecutor.stopTask(taskId);
+ }
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java
index 755388e9c..fe889d4f5 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java
@@ -1,6 +1,5 @@
package io.split.android.client.service.sseclient.sseclient;
-import static io.split.android.client.service.ServiceConstants.FLAGS_SPEC_PARAM;
import static io.split.android.client.utils.Utils.checkNotNull;
import androidx.annotation.NonNull;
@@ -12,23 +11,23 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import io.split.android.client.service.http.HttpFetcher;
-import io.split.android.client.service.http.HttpFetcherException;
-import io.split.android.client.service.http.HttpStatus;
import io.split.android.client.service.sseclient.InvalidJwtTokenException;
import io.split.android.client.service.sseclient.SseAuthenticationResponse;
import io.split.android.client.service.sseclient.SseJwtParser;
+import io.split.android.client.service.sseclient.StreamingConstants;
+import io.split.android.client.service.sseclient.spi.StreamingAuthException;
+import io.split.android.client.service.sseclient.spi.StreamingAuthFetcher;
import io.split.android.client.utils.logger.Logger;
public class SseAuthenticator {
private static final String USER_KEY_PARAM = "users";
- private final HttpFetcher mAuthFetcher;
+ private final StreamingAuthFetcher mAuthFetcher;
private final Set mUserKeys;
private final SseJwtParser mJwtParser;
private final String mFlagsSpec;
- public SseAuthenticator(@NonNull HttpFetcher authFetcher,
+ public SseAuthenticator(@NonNull StreamingAuthFetcher authFetcher,
@NonNull SseJwtParser jwtParser,
@Nullable String flagsSpec) {
mAuthFetcher = checkNotNull(authFetcher);
@@ -42,19 +41,19 @@ public SseAuthenticationResult authenticate(long defaultSseConnectionDelaySecs)
try {
Map params = new LinkedHashMap<>();
if (mFlagsSpec != null && !mFlagsSpec.trim().isEmpty()) {
- params.put(FLAGS_SPEC_PARAM, mFlagsSpec);
+ params.put(StreamingConstants.FLAGS_SPEC_PARAM, mFlagsSpec);
}
params.put(USER_KEY_PARAM, mUserKeys);
- authResponse = mAuthFetcher.execute(params, null);
+ authResponse = mAuthFetcher.execute(params);
- } catch (HttpFetcherException httpFetcherException) {
- logError("Unexpected " + httpFetcherException.getLocalizedMessage());
- if (httpFetcherException.getHttpStatus() != null) {
- if (HttpStatus.isNotRetryable(HttpStatus.fromCode(httpFetcherException.getHttpStatus()))) {
+ } catch (StreamingAuthException authException) {
+ logError("Unexpected " + authException.getLocalizedMessage());
+ if (authException.getStatusCode() != null) {
+ if (isNotRetryable(authException.getStatusCode())) {
return unsuccessfulAuthenticationUnrecoverableError();
}
- return unexpectedHttpError(httpFetcherException.getHttpStatus());
+ return unexpectedHttpError(authException.getStatusCode());
} else {
return unexpectedError();
}
@@ -109,4 +108,11 @@ private SseAuthenticationResult unexpectedError() {
private SseAuthenticationResult unexpectedHttpError(int httpStatus) {
return new SseAuthenticationResult(httpStatus);
}
+
+ private boolean isNotRetryable(int httpStatus) {
+ return httpStatus == 400
+ || httpStatus == 403
+ || httpStatus == 414
+ || httpStatus == 9009;
+ }
}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java
deleted file mode 100644
index 78a8f316b..000000000
--- a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java
+++ /dev/null
@@ -1,169 +0,0 @@
-package io.split.android.client.service.sseclient.sseclient;
-
-import static io.split.android.client.utils.Utils.checkNotNull;
-
-import androidx.annotation.NonNull;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import io.split.android.client.network.HttpClient;
-import io.split.android.client.network.HttpException;
-import io.split.android.client.network.HttpStreamRequest;
-import io.split.android.client.network.HttpStreamResponse;
-import io.split.android.client.network.URIBuilder;
-import io.split.android.client.service.http.HttpStatus;
-import io.split.android.client.service.sseclient.EventStreamParser;
-import io.split.android.client.service.sseclient.SseJwtToken;
-import io.split.android.client.utils.StringHelper;
-import io.split.android.client.utils.logger.Logger;
-
-public class SseClientImpl implements SseClient {
-
- private final URI mTargetUrl;
- private final AtomicInteger mStatus;
- private final HttpClient mHttpClient;
- private final EventStreamParser mEventStreamParser;
- private final AtomicBoolean mIsDisconnectCalled;
- private final SseHandler mSseHandler;
-
- private final StringHelper mStringHelper;
-
- private HttpStreamRequest mHttpStreamRequest = null;
- private HttpStreamResponse mHttpStreamResponse = null;
-
- private static final String PUSH_NOTIFICATION_CHANNELS_PARAM = "channel";
- private static final String PUSH_NOTIFICATION_TOKEN_PARAM = "accessToken";
- private static final String PUSH_NOTIFICATION_VERSION_PARAM = "v";
- private static final String PUSH_NOTIFICATION_VERSION_VALUE = "1.1";
-
- public SseClientImpl(@NonNull URI uri,
- @NonNull HttpClient httpClient,
- @NonNull EventStreamParser eventStreamParser,
- @NonNull SseHandler sseHandler) {
- mTargetUrl = checkNotNull(uri);
- mHttpClient = checkNotNull(httpClient);
- mEventStreamParser = checkNotNull(eventStreamParser);
- mSseHandler = checkNotNull(sseHandler);
- mStatus = new AtomicInteger(DISCONNECTED);
- mIsDisconnectCalled = new AtomicBoolean(false);
- mStringHelper = new StringHelper();
- mStatus.set(DISCONNECTED);
- }
-
- @Override
- public int status() {
- return mStatus.get();
- }
-
- @Override
- public void disconnect() {
- if (!mIsDisconnectCalled.getAndSet(true)) {
- close();
- }
- }
-
- private void close() {
- Logger.d("Disconnecting SSE client");
- if (mStatus.getAndSet(DISCONNECTED) != DISCONNECTED) {
- // Close the HttpStreamResponse first to clean up sockets
- if (mHttpStreamResponse != null) {
- try {
- mHttpStreamResponse.close();
- Logger.v("HttpStreamResponse closed successfully");
- } catch (IOException e) {
- Logger.w("Failed to close HttpStreamResponse: " + e.getMessage());
- }
- mHttpStreamResponse = null;
- }
-
- // Close the HttpStreamRequest
- if (mHttpStreamRequest != null) {
- mHttpStreamRequest.close();
- mHttpStreamRequest = null;
- }
- Logger.d("SSE client disconnected");
- }
- }
-
- @Override
- public void connect(SseJwtToken token, ConnectionListener connectionListener) {
- mIsDisconnectCalled.set(false);
- mStatus.set(CONNECTING);
- boolean isConnectionConfirmed = false;
- String channels = mStringHelper.join(",", token.getChannels());
- String rawToken = token.getRawJwt();
- boolean isErrorRetryable = true;
- BufferedReader bufferedReader = null;
- try {
- URI url = new URIBuilder(mTargetUrl)
- .addParameter(PUSH_NOTIFICATION_VERSION_PARAM, PUSH_NOTIFICATION_VERSION_VALUE)
- .addParameter(PUSH_NOTIFICATION_CHANNELS_PARAM, channels)
- .addParameter(PUSH_NOTIFICATION_TOKEN_PARAM, rawToken)
- .build();
- mHttpStreamRequest = mHttpClient.streamRequest(url);
- mHttpStreamResponse = mHttpStreamRequest.execute();
- if (mHttpStreamResponse.isSuccess()) {
- bufferedReader = mHttpStreamResponse.getBufferedReader();
- if (bufferedReader != null) {
- Logger.d("Streaming connection opened");
- mStatus.set(CONNECTED);
- String inputLine;
- Map values = new HashMap<>();
- while ((inputLine = bufferedReader.readLine()) != null) {
- if (mEventStreamParser.parseLineAndAppendValue(inputLine, values)) {
- if (!isConnectionConfirmed) {
- if (mEventStreamParser.isKeepAlive(values) || mSseHandler.isConnectionConfirmed(values)) {
- Logger.d("Streaming connection success");
- isConnectionConfirmed = true;
- connectionListener.onConnectionSuccess();
- } else {
- Logger.d("Streaming error after connection");
- isErrorRetryable = mSseHandler.isRetryableError(values);
- break;
- }
- }
- // Keep alive has to be handled by connection timeout
- if (!mEventStreamParser.isKeepAlive(values)) {
- mSseHandler.handleIncomingMessage(values);
- }
- values = new HashMap<>();
- }
- }
- } else {
- throw (new IOException("Buffer is null"));
- }
- } else {
- Logger.e("Streaming connection error. Http return code " + mHttpStreamResponse.getHttpStatus());
- isErrorRetryable = !mHttpStreamResponse.isClientRelatedError();
- }
- } catch (URISyntaxException e) {
- logError("An error has occurred while creating stream Url ", e);
- isErrorRetryable = false;
- } catch (HttpException e) {
- logError("An error has occurred while creating stream Url ", e);
- isErrorRetryable = !HttpStatus.isNotRetryable(HttpStatus.fromCode(e.getStatusCode()));
- } catch (IOException e) {
- Logger.d("An error has occurred while parsing stream: " + e.getLocalizedMessage());
- isErrorRetryable = true;
- } catch (Exception e) {
- logError("An unexpected error has occurred while receiving stream events from: ", e);
- isErrorRetryable = true;
- } finally {
- if (!mIsDisconnectCalled.getAndSet(false)) {
- mSseHandler.handleError(isErrorRetryable);
- close();
- }
- }
- }
-
- private static void logError(String message, Exception e) {
- Logger.e(message + " : " + e.getLocalizedMessage());
- }
-}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java
index 7b196202d..16d5c824a 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java
@@ -4,37 +4,32 @@
import androidx.annotation.NonNull;
-import io.split.android.client.service.executor.SplitTask;
-import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskExecutionListener;
-import io.split.android.client.service.executor.SplitTaskExecutor;
+import io.split.android.client.service.sseclient.spi.StreamingScheduler;
import io.split.android.client.utils.logger.Logger;
-public class SseDisconnectionTimer implements SplitTaskExecutionListener {
+public class SseDisconnectionTimer {
- private final SplitTaskExecutor mTaskExecutor;
+ private final StreamingScheduler mScheduler;
private final int mInitialDelayInSeconds;
private String mTaskId;
- public SseDisconnectionTimer(@NonNull SplitTaskExecutor taskExecutor, int initialDelayInSeconds) {
- mTaskExecutor = checkNotNull(taskExecutor);
+ public SseDisconnectionTimer(@NonNull StreamingScheduler scheduler, int initialDelayInSeconds) {
+ mScheduler = checkNotNull(scheduler);
mInitialDelayInSeconds = initialDelayInSeconds;
}
public void cancel() {
- if (mTaskId != null) {
- mTaskExecutor.stopTask(mTaskId);
- }
+ mScheduler.cancel(mTaskId);
}
- public void schedule(SplitTask task) {
+ public void schedule(Runnable task) {
Logger.v("Scheduling disconnection in " + mInitialDelayInSeconds + " seconds");
cancel();
- mTaskId = mTaskExecutor.schedule(task, mInitialDelayInSeconds, this);
- }
-
- @Override
- public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) {
- mTaskId = null;
+ mTaskId = mScheduler.schedule(task, mInitialDelayInSeconds, new StreamingScheduler.TaskExecutionListener() {
+ @Override
+ public void onTaskExecuted() {
+ mTaskId = null;
+ }
+ });
}
}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java
index c8b967d9a..0ae3e6542 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java
@@ -16,40 +16,38 @@
import io.split.android.client.service.sseclient.notifications.ControlNotification;
import io.split.android.client.service.sseclient.notifications.IncomingNotification;
import io.split.android.client.service.sseclient.notifications.NotificationParser;
-import io.split.android.client.service.sseclient.notifications.NotificationProcessor;
+import io.split.android.client.service.sseclient.spi.StreamingTelemetry;
+import io.split.android.client.service.sseclient.spi.UpdateNotificationListener;
import io.split.android.client.service.sseclient.notifications.OccupancyNotification;
import io.split.android.client.service.sseclient.notifications.StreamingError;
-import io.split.android.client.telemetry.model.streaming.AblyErrorStreamingEvent;
-import io.split.android.client.telemetry.model.streaming.SseConnectionErrorStreamingEvent;
-import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
import io.split.android.client.utils.logger.Logger;
public class SseHandler {
private final PushManagerEventBroadcaster mBroadcasterChannel;
private final NotificationParser mNotificationParser;
- private final NotificationProcessor mNotificationProcessor;
+ private final UpdateNotificationListener mUpdateListener;
private final NotificationManagerKeeper mNotificationManagerKeeper;
- private final TelemetryRuntimeProducer mTelemetryRuntimeProducer;
+ private final StreamingTelemetry mTelemetry;
public SseHandler(@NonNull NotificationParser notificationParser,
- @NonNull NotificationProcessor notificationProcessor,
- @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer,
+ @NonNull UpdateNotificationListener updateListener,
+ @NonNull StreamingTelemetry telemetry,
@NonNull PushManagerEventBroadcaster broadcasterChannel) {
- this(notificationParser, notificationProcessor, new NotificationManagerKeeper(broadcasterChannel, telemetryRuntimeProducer), broadcasterChannel, telemetryRuntimeProducer);
+ this(notificationParser, updateListener, new NotificationManagerKeeper(broadcasterChannel, telemetry), broadcasterChannel, telemetry);
}
@VisibleForTesting
public SseHandler(@NonNull NotificationParser notificationParser,
- @NonNull NotificationProcessor notificationProcessor,
+ @NonNull UpdateNotificationListener updateListener,
@NonNull NotificationManagerKeeper managerKeeper,
@NonNull PushManagerEventBroadcaster broadcasterChannel,
- @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) {
+ @NonNull StreamingTelemetry telemetry) {
mNotificationParser = checkNotNull(notificationParser);
- mNotificationProcessor = checkNotNull(notificationProcessor);
+ mUpdateListener = checkNotNull(updateListener);
mBroadcasterChannel = checkNotNull(broadcasterChannel);
mNotificationManagerKeeper = checkNotNull(managerKeeper);
- mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer);
+ mTelemetry = checkNotNull(telemetry);
}
public boolean isConnectionConfirmed(Map values) {
@@ -88,7 +86,7 @@ public void handleIncomingMessage(Map values) {
case MEMBERSHIPS_MS_UPDATE:
case MEMBERSHIPS_LS_UPDATE:
if (mNotificationManagerKeeper.isStreamingActive()) {
- mNotificationProcessor.process(incomingNotification);
+ mUpdateListener.onUpdateNotification(incomingNotification);
}
break;
default:
@@ -100,13 +98,7 @@ public void handleIncomingMessage(Map values) {
public void handleError(boolean retryable) {
PushStatusEvent event = new PushStatusEvent(retryable ? EventType.PUSH_RETRYABLE_ERROR : EventType.PUSH_NON_RETRYABLE_ERROR);
mBroadcasterChannel.pushMessage(event);
-
- mTelemetryRuntimeProducer.recordStreamingEvents(
- new SseConnectionErrorStreamingEvent(
- (retryable) ? SseConnectionErrorStreamingEvent.Status.REQUESTED : SseConnectionErrorStreamingEvent.Status.NON_REQUESTED,
- System.currentTimeMillis()
- )
- );
+ mTelemetry.recordConnectionError(retryable, System.currentTimeMillis());
}
public boolean isRetryableError(Map values) {
@@ -162,7 +154,7 @@ private void handleError(String jsonData) {
return;
}
- mTelemetryRuntimeProducer.recordStreamingEvents(new AblyErrorStreamingEvent(errorNotification.getCode(), System.currentTimeMillis()));
+ mTelemetry.recordAblyError(errorNotification.getCode(), System.currentTimeMillis());
PushStatusEvent message = new PushStatusEvent(
errorNotification.isRetryable() ? EventType.PUSH_RETRYABLE_ERROR : EventType.PUSH_NON_RETRYABLE_ERROR);
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java
index 88980ccfe..5d5a0e935 100644
--- a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java
@@ -4,43 +4,42 @@
import androidx.annotation.NonNull;
-import io.split.android.client.service.executor.SplitTask;
-import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskExecutionListener;
-import io.split.android.client.service.executor.SplitTaskExecutor;
-import io.split.android.client.service.executor.SplitTaskType;
import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster;
import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent;
import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent.EventType;
+import io.split.android.client.service.sseclient.spi.StreamingScheduler;
import io.split.android.client.utils.logger.Logger;
-public class SseRefreshTokenTimer implements SplitTaskExecutionListener {
+public class SseRefreshTokenTimer {
private final static int RECONNECT_TIME_BEFORE_TOKEN_EXP_IN_SECONDS = 600;
- SplitTaskExecutor mTaskExecutor;
- PushManagerEventBroadcaster mBroadcasterChannel;
- String mTaskId;
+ private final StreamingScheduler mScheduler;
+ private final PushManagerEventBroadcaster mBroadcasterChannel;
+ private String mTaskId;
- public SseRefreshTokenTimer(@NonNull SplitTaskExecutor taskExecutor, @NonNull PushManagerEventBroadcaster broadcasterChannel) {
- mTaskExecutor = checkNotNull(taskExecutor);
+ public SseRefreshTokenTimer(@NonNull StreamingScheduler scheduler, @NonNull PushManagerEventBroadcaster broadcasterChannel) {
+ mScheduler = checkNotNull(scheduler);
mBroadcasterChannel = checkNotNull(broadcasterChannel);
}
public void cancel() {
- mTaskExecutor.stopTask(mTaskId);
+ mScheduler.cancel(mTaskId);
}
public void schedule(long issueAtTime, long expirationTime) {
cancel();
long reconnectTime = reconnectTime(issueAtTime, expirationTime);
- mTaskId = mTaskExecutor.schedule(new SplitTask() {
- @NonNull
+ mTaskId = mScheduler.schedule(new Runnable() {
@Override
- public SplitTaskExecutionInfo execute() {
+ public void run() {
Logger.d("Informing sse token expired through pushing retryable error.");
mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_RETRYABLE_ERROR));
- return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK);
}
- }, reconnectTime, null);
+ }, reconnectTime, new StreamingScheduler.TaskExecutionListener() {
+ @Override
+ public void onTaskExecuted() {
+ mTaskId = null;
+ }
+ });
}
private long reconnectTime(long issuedAtTime, long expirationTime) {
@@ -48,8 +47,4 @@ private long reconnectTime(long issuedAtTime, long expirationTime) {
, 0L);
}
- @Override
- public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) {
- mTaskId = null;
- }
}
diff --git a/main/src/main/java/io/split/android/client/service/sseclient/sseclient/TelemetryRuntimeProducerStreamingTelemetry.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/TelemetryRuntimeProducerStreamingTelemetry.java
new file mode 100644
index 000000000..6cb127fda
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/TelemetryRuntimeProducerStreamingTelemetry.java
@@ -0,0 +1,105 @@
+package io.split.android.client.service.sseclient.sseclient;
+
+import androidx.annotation.NonNull;
+
+import io.split.android.client.service.sseclient.spi.StreamingTelemetry;
+import io.split.android.client.telemetry.model.OperationType;
+import io.split.android.client.telemetry.model.streaming.AblyErrorStreamingEvent;
+import io.split.android.client.telemetry.model.streaming.OccupancyPriStreamingEvent;
+import io.split.android.client.telemetry.model.streaming.OccupancySecStreamingEvent;
+import io.split.android.client.telemetry.model.streaming.SseConnectionErrorStreamingEvent;
+import io.split.android.client.telemetry.model.streaming.StreamingStatusStreamingEvent;
+import io.split.android.client.telemetry.model.streaming.SyncModeUpdateStreamingEvent;
+import io.split.android.client.telemetry.model.streaming.TokenRefreshStreamingEvent;
+import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer;
+
+/**
+ * Adapter that implements StreamingTelemetry using TelemetryRuntimeProducer.
+ */
+public class TelemetryRuntimeProducerStreamingTelemetry implements StreamingTelemetry {
+
+ private final TelemetryRuntimeProducer mTelemetryRuntimeProducer;
+
+ public TelemetryRuntimeProducerStreamingTelemetry(@NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) {
+ mTelemetryRuntimeProducer = telemetryRuntimeProducer;
+ }
+
+ @Override
+ public void recordTokenSyncLatency(long latencyMillis) {
+ mTelemetryRuntimeProducer.recordSyncLatency(OperationType.TOKEN, latencyMillis);
+ }
+
+ @Override
+ public void recordTokenSuccessfulSync(long timestamp) {
+ mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.TOKEN, timestamp);
+ }
+
+ @Override
+ public void recordTokenSyncError(Integer httpStatus) {
+ mTelemetryRuntimeProducer.recordSyncError(OperationType.TOKEN, httpStatus);
+ }
+
+ @Override
+ public void recordAuthRejections() {
+ mTelemetryRuntimeProducer.recordAuthRejections();
+ }
+
+ @Override
+ public void recordTokenRefreshes() {
+ mTelemetryRuntimeProducer.recordTokenRefreshes();
+ }
+
+ @Override
+ public void recordTokenRefreshEvent(long expirationTime, long timestamp) {
+ mTelemetryRuntimeProducer.recordStreamingEvents(new TokenRefreshStreamingEvent(expirationTime, timestamp));
+ }
+
+ @Override
+ public void recordSyncModeUpdate(boolean streaming, long timestamp) {
+ SyncModeUpdateStreamingEvent.Mode mode = streaming
+ ? SyncModeUpdateStreamingEvent.Mode.STREAMING
+ : SyncModeUpdateStreamingEvent.Mode.POLLING;
+ mTelemetryRuntimeProducer.recordStreamingEvents(new SyncModeUpdateStreamingEvent(mode, timestamp));
+ }
+
+ @Override
+ public void recordConnectionError(boolean retryable, long timestamp) {
+ SseConnectionErrorStreamingEvent.Status status = retryable
+ ? SseConnectionErrorStreamingEvent.Status.REQUESTED
+ : SseConnectionErrorStreamingEvent.Status.NON_REQUESTED;
+ mTelemetryRuntimeProducer.recordStreamingEvents(new SseConnectionErrorStreamingEvent(status, timestamp));
+ }
+
+ @Override
+ public void recordAblyError(int errorCode, long timestamp) {
+ mTelemetryRuntimeProducer.recordStreamingEvents(new AblyErrorStreamingEvent(errorCode, timestamp));
+ }
+
+ @Override
+ public void recordOccupancyPri(int publisherCount, long timestamp) {
+ mTelemetryRuntimeProducer.recordStreamingEvents(new OccupancyPriStreamingEvent(publisherCount, timestamp));
+ }
+
+ @Override
+ public void recordOccupancySec(int publisherCount, long timestamp) {
+ mTelemetryRuntimeProducer.recordStreamingEvents(new OccupancySecStreamingEvent(publisherCount, timestamp));
+ }
+
+ @Override
+ public void recordStreamingStatus(StreamingStatus status, long timestamp) {
+ StreamingStatusStreamingEvent.Status telemetryStatus;
+ switch (status) {
+ case PAUSED:
+ telemetryStatus = StreamingStatusStreamingEvent.Status.PAUSED;
+ break;
+ case DISABLED:
+ telemetryStatus = StreamingStatusStreamingEvent.Status.DISABLED;
+ break;
+ case ENABLED:
+ default:
+ telemetryStatus = StreamingStatusStreamingEvent.Status.ENABLED;
+ break;
+ }
+ mTelemetryRuntimeProducer.recordStreamingEvents(new StreamingStatusStreamingEvent(telemetryStatus, timestamp));
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java
index abf55e7fe..d0609534a 100644
--- a/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java
+++ b/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java
@@ -19,7 +19,7 @@
import io.split.android.client.service.executor.SplitTaskExecutionListener;
import io.split.android.client.service.executor.SplitTaskExecutor;
import io.split.android.client.service.executor.SplitTaskFactory;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.impressions.StrategyImpressionManager;
import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster;
import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer;
@@ -30,7 +30,9 @@
import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerRegistry;
import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerRegistryImpl;
import io.split.android.client.shared.UserConsent;
-import io.split.android.client.storage.common.StoragePusher;
+import io.split.android.client.submitter.RecorderSyncHelper;
+import io.split.android.client.submitter.RecorderSyncHelperImpl;
+import io.split.android.client.submitter.StoragePusher;
import io.split.android.client.storage.splits.SplitsStorage;
import io.split.android.client.telemetry.model.EventsDataRecordsEnum;
import io.split.android.client.telemetry.model.streaming.SyncModeUpdateStreamingEvent;
@@ -305,14 +307,12 @@ private void scheduleEventsRecorderTask() {
@Override
public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) {
- switch (taskInfo.getTaskType()) {
- case SPLITS_SYNC:
- mFeatureFlagsSynchronizer.submitLoadingTask(null);
- break;
- case MY_SEGMENTS_SYNC:
- Logger.d("Loading my segments updated in background");
- mMySegmentsSynchronizerRegistry.submitMySegmentsLoadingTask();
- break;
+ io.split.android.client.service.executor.SplitTaskType type = taskInfo.getTaskType();
+ if (type == SplitTaskType.SPLITS_SYNC) {
+ mFeatureFlagsSynchronizer.submitLoadingTask(null);
+ } else if (type == SplitTaskType.MY_SEGMENTS_SYNC) {
+ Logger.d("Loading my segments updated in background");
+ mMySegmentsSynchronizerRegistry.submitMySegmentsLoadingTask();
}
}
}
diff --git a/main/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java b/main/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java
index 2b86786fd..0e7c94814 100644
--- a/main/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java
+++ b/main/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java
@@ -29,7 +29,7 @@
import io.split.android.client.service.ServiceConstants;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
import io.split.android.client.service.executor.SplitTaskExecutionListener;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.impressions.ImpressionManagerConfig;
import io.split.android.client.service.synchronizer.mysegments.MySegmentsWorkManagerWrapper;
import io.split.android.client.service.workmanager.EventsRecorderWorker;
diff --git a/main/src/main/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTask.java b/main/src/main/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTask.java
index 5083359dd..568668d86 100644
--- a/main/src/main/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTask.java
+++ b/main/src/main/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTask.java
@@ -8,7 +8,7 @@
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.http.HttpRecorder;
import io.split.android.client.service.http.HttpRecorderException;
import io.split.android.client.service.http.HttpStatus;
diff --git a/main/src/main/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTask.java b/main/src/main/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTask.java
index c6c8fbd21..464055ac2 100644
--- a/main/src/main/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTask.java
+++ b/main/src/main/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTask.java
@@ -8,7 +8,7 @@
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
-import io.split.android.client.service.executor.SplitTaskType;
+import io.split.android.client.service.SplitTaskType;
import io.split.android.client.service.http.HttpRecorder;
import io.split.android.client.service.http.HttpRecorderException;
import io.split.android.client.service.http.HttpStatus;
diff --git a/main/src/main/java/io/split/android/client/service/workmanager/HttpClientProvider.java b/main/src/main/java/io/split/android/client/service/workmanager/HttpClientProvider.java
index 2d7e7119d..6d235ef88 100644
--- a/main/src/main/java/io/split/android/client/service/workmanager/HttpClientProvider.java
+++ b/main/src/main/java/io/split/android/client/service/workmanager/HttpClientProvider.java
@@ -6,7 +6,6 @@
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
-import io.split.android.client.main.BuildConfig;
import io.split.android.client.dtos.HttpProxyDto;
import io.split.android.client.network.BasicCredentialsProvider;
import io.split.android.client.network.BearerCredentialsProvider;
@@ -14,6 +13,7 @@
import io.split.android.client.network.CertificatePinningConfigurationProvider;
import io.split.android.client.network.HttpClient;
import io.split.android.client.network.HttpClientImpl;
+import io.split.android.client.network.SdkVersionProvider;
import io.split.android.client.network.HttpProxy;
import io.split.android.client.network.SplitHttpHeadersBuilder;
import io.split.android.client.storage.cipher.SplitCipherFactory;
@@ -43,7 +43,7 @@ private static HttpClient buildHttpClient(String apiKey, @Nullable CertificatePi
.build();
SplitHttpHeadersBuilder headersBuilder = new SplitHttpHeadersBuilder();
- headersBuilder.setClientVersion(BuildConfig.SPLIT_VERSION_NAME);
+ headersBuilder.setClientVersion(SdkVersionProvider.getSdkVersion());
headersBuilder.setApiToken(apiKey);
headersBuilder.addJsonTypeHeaders();
httpClient.addHeaders(headersBuilder.build());
diff --git a/main/src/main/java/io/split/android/client/storage/common/PersistentStorage.java b/main/src/main/java/io/split/android/client/storage/common/PersistentStorage.java
index c111303d1..21744d98f 100644
--- a/main/src/main/java/io/split/android/client/storage/common/PersistentStorage.java
+++ b/main/src/main/java/io/split/android/client/storage/common/PersistentStorage.java
@@ -4,16 +4,15 @@
import java.util.List;
-public interface PersistentStorage extends StoragePusher {
+import io.split.android.client.submitter.RecorderStorage;
+import io.split.android.client.submitter.StoragePusher;
+
+public interface PersistentStorage extends StoragePusher, RecorderStorage {
// Push method is defined in StoragePusher interface
void pushMany(@NonNull List elements);
- List pop(int count);
-
- void setActive(@NonNull List elements);
-
- void delete(@NonNull List elements);
+ // pop, delete, and setActive are inherited from RecorderStorage
void deleteInvalid(long maxTimestamp);
}
diff --git a/main/src/main/java/io/split/android/client/storage/events/EventsStorage.java b/main/src/main/java/io/split/android/client/storage/events/EventsStorage.java
index 1eb2d1ae7..5da50977a 100644
--- a/main/src/main/java/io/split/android/client/storage/events/EventsStorage.java
+++ b/main/src/main/java/io/split/android/client/storage/events/EventsStorage.java
@@ -11,7 +11,7 @@
import io.split.android.client.dtos.Event;
import io.split.android.client.storage.common.Storage;
-import io.split.android.client.storage.common.StoragePusher;
+import io.split.android.client.submitter.StoragePusher;
import io.split.android.client.utils.logger.Logger;
public class EventsStorage implements Storage, StoragePusher {
diff --git a/main/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java b/main/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java
index 403fbe220..188e3d2db 100644
--- a/main/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java
+++ b/main/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java
@@ -12,7 +12,7 @@
import io.split.android.client.dtos.KeyImpression;
import io.split.android.client.storage.common.PersistentStorage;
import io.split.android.client.storage.common.Storage;
-import io.split.android.client.storage.common.StoragePusher;
+import io.split.android.client.submitter.StoragePusher;
import io.split.android.client.utils.logger.Logger;
public class ImpressionsStorage implements Storage, StoragePusher {
diff --git a/main/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java b/main/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java
index f4659ecaf..6439a69b0 100644
--- a/main/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java
+++ b/main/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java
@@ -24,12 +24,11 @@
import java.util.concurrent.atomic.AtomicBoolean;
import io.split.android.client.dtos.Split;
+import io.split.android.client.utils.logger.Logger;
import io.split.android.client.utils.Json;
public class SplitsStorageImpl implements SplitsStorage {
- private static final int ASYNC_WRITE_THRESHOLD = 50;
-
private final PersistentSplitsStorage mPersistentStorage;
private final Map mInMemorySplits;
private final Map> mFlagSets;
@@ -166,10 +165,14 @@ public boolean update(ProcessedSplitChange splitChange, ExecutorService mExecuto
mChangeNumber = splitChange.getChangeNumber();
mUpdateTimestamp = splitChange.getUpdateTimestamp();
- // If the amount of elements is greater than the threshold,
- // we will use the executor to update the persistent storage asynchronously
- if (((activeSplits != null && activeSplits.size() > ASYNC_WRITE_THRESHOLD) || (archivedSplits != null && archivedSplits.size() > ASYNC_WRITE_THRESHOLD)) && mExecutor != null) {
- mExecutor.submit(() -> mPersistentStorage.update(splitChange, mTrafficTypes, mFlagSets));
+ if (mExecutor != null) {
+ try {
+ Map trafficTypesSnapshot = new HashMap<>(mTrafficTypes);
+ Map> flagSetsSnapshot = copyFlagSets(mFlagSets);
+ mExecutor.submit(() -> mPersistentStorage.update(splitChange, trafficTypesSnapshot, flagSetsSnapshot));
+ } catch (Exception e) {
+ Logger.v("Failed to submit persistent write: " + e.getLocalizedMessage());
+ }
} else {
mPersistentStorage.update(splitChange, mTrafficTypes, mFlagSets);
}
@@ -177,6 +180,15 @@ public boolean update(ProcessedSplitChange splitChange, ExecutorService mExecuto
return appliedUpdates;
}
+ @NonNull
+ private static Map> copyFlagSets(Map> flagSets) {
+ Map> flagSetsSnapshot = new HashMap<>();
+ for (Map.Entry> entry : flagSets.entrySet()) {
+ flagSetsSnapshot.put(entry.getKey(), new HashSet<>(entry.getValue()));
+ }
+ return flagSetsSnapshot;
+ }
+
@Override
@WorkerThread
public void updateWithoutChecks(Split split) {
diff --git a/main/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerImpl.java b/main/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerImpl.java
index d6346b887..a5e01b9ae 100644
--- a/main/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerImpl.java
+++ b/main/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerImpl.java
@@ -11,7 +11,7 @@
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
import io.split.android.client.service.executor.SplitTaskExecutionListener;
import io.split.android.client.service.executor.SplitTaskExecutor;
-import io.split.android.client.service.sseclient.FixedIntervalBackoffCounter;
+import io.split.android.client.backoff.FixedIntervalBackoffCounter;
import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer;
import io.split.android.client.service.telemetry.TelemetryTaskFactory;
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/main/java/io/split/android/client/validators/EventValidator.java b/main/src/main/java/io/split/android/client/validators/EventValidator.java
deleted file mode 100644
index a1bd81220..000000000
--- a/main/src/main/java/io/split/android/client/validators/EventValidator.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package io.split.android.client.validators;
-
-import io.split.android.client.dtos.Event;
-
-/**
- * Interface to implement by Track Events validators
- */
-public interface EventValidator {
-
- /**
- * Checks that a Track event is valid
- * @param event: Event instance
- * @return true when the key is valid, false when it is not
- */
- ValidationErrorInfo validate(Event event, boolean validateTrafficType);
-
-}
diff --git a/main/src/main/java/io/split/android/client/validators/EventValidatorImpl.java b/main/src/main/java/io/split/android/client/validators/EventValidatorImpl.java
deleted file mode 100644
index a189a3a02..000000000
--- a/main/src/main/java/io/split/android/client/validators/EventValidatorImpl.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package io.split.android.client.validators;
-
-import io.split.android.client.dtos.Event;
-import io.split.android.client.storage.splits.SplitsStorage;
-import io.split.android.client.utils.Utils;
-
-/**
- * Contains func an instance of Event class.
- */
-public class EventValidatorImpl implements EventValidator {
-
- private final String TYPE_REGEX = ValidationConfig.getInstance().getTrackEventNamePattern();
- private KeyValidator mKeyValidator;
- private final SplitsStorage mSplitsStorage;
-
- public EventValidatorImpl(KeyValidator keyValidator, SplitsStorage splitsStorage) {
- mKeyValidator = keyValidator;
- mSplitsStorage = splitsStorage;
- }
-
- @Override
- public ValidationErrorInfo validate(Event event, boolean validateTrafficType) {
-
- if(event == null) {
- return new ValidationErrorInfo(ValidationErrorInfo.ERROR_SOME, "Event could not be null");
- }
-
- ValidationErrorInfo errorInfo = mKeyValidator.validate(event.key, null);
- if(errorInfo != null){
- return errorInfo;
- }
-
- if (event.trafficTypeName == null) {
- return new ValidationErrorInfo(ValidationErrorInfo.ERROR_SOME, "you passed a null or undefined traffic_type_name, traffic_type_name must be a non-empty string");
- }
-
- if (Utils.isNullOrEmpty(event.trafficTypeName.trim())) {
- return new ValidationErrorInfo(ValidationErrorInfo.ERROR_SOME, "you passed an empty traffic_type_name, traffic_type_name must be a non-empty string");
- }
-
- if (event.eventTypeId == null) {
- return new ValidationErrorInfo(ValidationErrorInfo.ERROR_SOME, "you passed a null or undefined event_type, event_type must be a non-empty String");
- }
-
- if (Utils.isNullOrEmpty(event.eventTypeId.trim())) {
- return new ValidationErrorInfo(ValidationErrorInfo.ERROR_SOME, "you passed an empty event_type, event_type must be a non-empty String");
- }
-
- if (!event.eventTypeId.matches(TYPE_REGEX)) {
- return new ValidationErrorInfo(ValidationErrorInfo.ERROR_SOME, "you passed " + event.eventTypeId
- + ", event name must adhere to the regular expression " + TYPE_REGEX
- + ". This means an event name must be alphanumeric, cannot be more than 80 characters long, and can only include a dash, "
- + " underscore, period, or colon as separators of alphanumeric characters.");
- }
-
- if(!event.trafficTypeName.toLowerCase().equals(event.trafficTypeName)) {
- errorInfo = new ValidationErrorInfo(ValidationErrorInfo.WARNING_TRAFFIC_TYPE_HAS_UPPERCASE_CHARS, "traffic_type_name should be all lowercase - converting string to lowercase", true);
- }
-
- if (validateTrafficType && !mSplitsStorage.isValidTrafficType(event.trafficTypeName)) {
- String message = "Traffic Type " + event.trafficTypeName + " does not have any corresponding feature flags in this environment, "
- + "make sure you’re tracking your events to a valid traffic type defined in the Split user interface";
- if(errorInfo == null) {
- errorInfo = new ValidationErrorInfo(ValidationErrorInfo.WARNING_TRAFFIC_TYPE_WITHOUT_SPLIT_IN_ENVIRONMENT, message, true);
- } else {
- errorInfo.addWarning(ValidationErrorInfo.WARNING_TRAFFIC_TYPE_WITHOUT_SPLIT_IN_ENVIRONMENT, message);
- }
- }
-
- return errorInfo;
- }
-}
diff --git a/main/src/main/java/io/split/android/client/validators/PropertyValidatorAdapter.java b/main/src/main/java/io/split/android/client/validators/PropertyValidatorAdapter.java
new file mode 100644
index 000000000..4406cd4ee
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/validators/PropertyValidatorAdapter.java
@@ -0,0 +1,32 @@
+package io.split.android.client.validators;
+
+import java.util.Map;
+
+import io.split.android.client.tracker.TrackerLogger;
+import io.split.android.client.tracker.TrackerPropertyValidator;
+
+/**
+ * Adapter that bridges the main module's PropertyValidator interface with
+ * the tracker module's TrackerPropertyValidator implementation.
+ */
+public class PropertyValidatorAdapter implements PropertyValidator {
+
+ private final TrackerPropertyValidator mDelegate;
+
+ public PropertyValidatorAdapter(TrackerPropertyValidator delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ public Result validate(Map properties, String validationTag) {
+ // Call the tracker validator with initialSizeInBytes=0 since we're not tracking
+ TrackerPropertyValidator.TrackerPropertyResult trackerResult =
+ mDelegate.validate(properties, 0, validationTag);
+
+ if (trackerResult.isValid()) {
+ return Result.valid(trackerResult.getProperties(), trackerResult.getSizeInBytes());
+ } else {
+ return Result.invalid(trackerResult.getErrorMessage(), trackerResult.getSizeInBytes());
+ }
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/validators/TrafficTypeValidatorImpl.java b/main/src/main/java/io/split/android/client/validators/TrafficTypeValidatorImpl.java
new file mode 100644
index 000000000..a46d998b8
--- /dev/null
+++ b/main/src/main/java/io/split/android/client/validators/TrafficTypeValidatorImpl.java
@@ -0,0 +1,23 @@
+package io.split.android.client.validators;
+
+import io.split.android.client.storage.splits.SplitsStorage;
+import io.split.android.client.tracker.TrafficTypeValidator;
+
+/**
+ * Implementation of {@link TrafficTypeValidator} that delegates to {@link SplitsStorage}.
+ *
+ * This implementation validates traffic type names by checking if they exist in the
+ * Split storage. It bridges the tracker module's abstraction with the SDK's storage layer.
+ */
+public class TrafficTypeValidatorImpl implements TrafficTypeValidator {
+ private final SplitsStorage mSplitsStorage;
+
+ public TrafficTypeValidatorImpl(SplitsStorage splitsStorage) {
+ mSplitsStorage = splitsStorage;
+ }
+
+ @Override
+ public boolean isValid(String trafficTypeName) {
+ return mSplitsStorage.isValidTrafficType(trafficTypeName);
+ }
+}
diff --git a/main/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java b/main/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java
index 287fb94b4..28d54a578 100644
--- a/main/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java
+++ b/main/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java
@@ -8,7 +8,7 @@
import io.split.android.client.Evaluator;
import io.split.android.client.EvaluatorImpl;
import io.split.android.client.FlagSetsFilter;
-import io.split.android.client.PropertyValidatorImpl;
+import io.split.android.client.validators.PropertyValidatorImpl;
import io.split.android.client.api.Key;
import io.split.android.client.attributes.AttributesManager;
import io.split.android.client.attributes.AttributesMerger;
@@ -65,7 +65,8 @@ public TreatmentManagerFactoryImpl(@NonNull KeyValidator keyValidator,
mSplitsStorage = checkNotNull(splitsStorage);
mValidationMessageLogger = new ValidationMessageLoggerImpl();
mFlagSetsValidator = new FlagSetsValidatorImpl();
- mPropertyValidator = new PropertyValidatorImpl();
+ mPropertyValidator = new PropertyValidatorAdapter(
+ new PropertyValidatorImpl(new ValidationMessageLoggerImpl()));
}
@Override
diff --git a/main/src/main/java/io/split/android/client/validators/ValidationMessageLoggerImpl.java b/main/src/main/java/io/split/android/client/validators/ValidationMessageLoggerImpl.java
index c6678b276..a56866a28 100644
--- a/main/src/main/java/io/split/android/client/validators/ValidationMessageLoggerImpl.java
+++ b/main/src/main/java/io/split/android/client/validators/ValidationMessageLoggerImpl.java
@@ -3,12 +3,14 @@
import java.util.ArrayList;
import java.util.List;
+import io.split.android.client.tracker.TrackerLogger;
+import io.split.android.client.tracker.TrackerValidationError;
import io.split.android.client.utils.logger.Logger;
/**
* Default implementation of ValidationMessageLogger interface
*/
-public class ValidationMessageLoggerImpl implements ValidationMessageLogger {
+public class ValidationMessageLoggerImpl implements ValidationMessageLogger, TrackerLogger {
@Override
public void log(ValidationErrorInfo errorInfo, String tag) {
@@ -52,4 +54,22 @@ private String sanitizeTag(String tag) {
return (tag != null ? tag : "");
}
+ // TrackerLogger implementation
+
+ @Override
+ public void log(TrackerValidationError errorInfo, String tag) {
+ if (errorInfo.isError()) {
+ logError(errorInfo.getMessage(), tag);
+ } else {
+ for (String warning : errorInfo.getWarnings()) {
+ logWarning(warning, tag);
+ }
+ }
+ }
+
+ @Override
+ public void v(String message) {
+ Logger.v(message);
+ }
+
}
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/SplitClientImplBaseTest.java b/main/src/test/java/io/split/android/client/SplitClientImplBaseTest.java
index 88cd686ee..b89a4f6e4 100644
--- a/main/src/test/java/io/split/android/client/SplitClientImplBaseTest.java
+++ b/main/src/test/java/io/split/android/client/SplitClientImplBaseTest.java
@@ -14,6 +14,7 @@
import io.split.android.client.storage.mysegments.MySegmentsStorageContainer;
import io.split.android.client.storage.rbs.RuleBasedSegmentStorage;
import io.split.android.client.storage.splits.SplitsStorage;
+import io.split.android.client.tracker.Tracker;
import io.split.android.client.validators.SplitValidator;
import io.split.android.client.validators.TreatmentManager;
import io.split.android.engine.experiments.ParserCommons;
@@ -41,7 +42,7 @@ public abstract class SplitClientImplBaseTest {
@Mock
protected SplitsStorage splitsStorage;
@Mock
- protected EventsTracker eventsTracker;
+ protected Tracker eventsTracker;
@Mock
protected SyncManager syncManager;
@Mock
diff --git a/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java b/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java
index 16d40a060..539982fc0 100644
--- a/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java
+++ b/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java
@@ -22,6 +22,7 @@
import io.split.android.client.events.SplitEventsManager;
import io.split.android.client.impressions.ImpressionListener;
import io.split.android.client.shared.SplitClientContainer;
+import io.split.android.client.tracker.Tracker;
import io.split.android.client.utils.logger.Logger;
import io.split.android.client.validators.SplitValidator;
import io.split.android.client.validators.TreatmentManager;
@@ -38,7 +39,7 @@ public class SplitClientImplEventRegistrationTest {
@Mock
private ImpressionListener impressionListener;
@Mock
- private EventsTracker eventsTracker;
+ private Tracker eventsTracker;
@Mock
private AttributesManager attributesManager;
@Mock
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/SplitFactoryImplEventsTrackerProviderTest.java b/main/src/test/java/io/split/android/client/SplitFactoryImplEventsTrackerProviderTest.java
new file mode 100644
index 000000000..456580004
--- /dev/null
+++ b/main/src/test/java/io/split/android/client/SplitFactoryImplEventsTrackerProviderTest.java
@@ -0,0 +1,141 @@
+package io.split.android.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyDouble;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.split.android.client.dtos.Event;
+import io.split.android.client.service.synchronizer.SyncManager;
+import io.split.android.client.storage.splits.SplitsStorage;
+import io.split.android.client.telemetry.model.Method;
+import io.split.android.client.telemetry.storage.TelemetryStorage;
+import io.split.android.client.tracker.Tracker;
+
+public class SplitFactoryImplEventsTrackerProviderTest {
+
+ private SplitsStorage mSplitsStorage;
+ private TelemetryStorage mTelemetryStorage;
+ private SyncManager mSyncManager;
+ private SplitFactoryImpl.EventsTrackerProvider mProvider;
+
+ @Before
+ public void setUp() {
+ mSplitsStorage = mock(SplitsStorage.class);
+ mTelemetryStorage = mock(TelemetryStorage.class);
+ mSyncManager = mock(SyncManager.class);
+ mProvider = new SplitFactoryImpl.EventsTrackerProvider(
+ mSplitsStorage,
+ mTelemetryStorage,
+ mSyncManager);
+
+ // Set up default behavior for traffic type validation
+ when(mSplitsStorage.isValidTrafficType(anyString())).thenReturn(true);
+ }
+
+ @Test
+ public void getEventsTrackerReturnsNonNullTracker() {
+ Tracker tracker = mProvider.getEventsTracker();
+
+ assertNotNull(tracker);
+ }
+
+ @Test
+ public void getEventsTrackerReturnsSameInstanceOnSubsequentCalls() {
+ Tracker tracker1 = mProvider.getEventsTracker();
+ Tracker tracker2 = mProvider.getEventsTracker();
+
+ assertSame(tracker1, tracker2);
+ }
+
+ @Test
+ public void trackerCallbackInvokesSyncManagerPushEvent() {
+ Tracker tracker = mProvider.getEventsTracker();
+
+ Map properties = new HashMap<>();
+ properties.put("key1", "value1");
+ boolean result = tracker.track("user-key", "user", "purchase", 10.5, properties, true);
+
+ assertTrue(result);
+ verify(mSyncManager).pushEvent(any(Event.class));
+ }
+
+ @Test
+ public void trackerCallbackCreatesEventWithCorrectFields() {
+ Tracker tracker = mProvider.getEventsTracker();
+
+ Map properties = new HashMap<>();
+ properties.put("product", "widget");
+ properties.put("quantity", 3);
+
+ long beforeTrack = System.currentTimeMillis();
+ tracker.track("test-key", "account", "conversion", 25.99, properties, true);
+ long afterTrack = System.currentTimeMillis();
+
+ ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class);
+ verify(mSyncManager).pushEvent(eventCaptor.capture());
+
+ Event capturedEvent = eventCaptor.getValue();
+ assertNotNull(capturedEvent);
+ assertEquals("conversion", capturedEvent.eventTypeId);
+ assertEquals("account", capturedEvent.trafficTypeName);
+ assertEquals("test-key", capturedEvent.key);
+ assertEquals(25.99, capturedEvent.value, 0.0001);
+ assertTrue(capturedEvent.timestamp >= beforeTrack && capturedEvent.timestamp <= afterTrack);
+ assertNotNull(capturedEvent.properties);
+ assertEquals("widget", capturedEvent.properties.get("product"));
+ assertEquals(3, capturedEvent.properties.get("quantity"));
+ assertTrue(capturedEvent.getSizeInBytes() > 0);
+ }
+
+ @Test
+ public void trackerCallbackRecordsLatencyInTelemetry() {
+ Tracker tracker = mProvider.getEventsTracker();
+
+ tracker.track("key", "user", "event", 1.0, null, true);
+
+ ArgumentCaptor latencyCaptor = ArgumentCaptor.forClass(Long.class);
+ verify(mTelemetryStorage).recordLatency(any(Method.class), latencyCaptor.capture());
+
+ Long latency = latencyCaptor.getValue();
+ assertNotNull(latency);
+ assertTrue(latency >= 0);
+ }
+
+ @Test
+ public void trackerCallbackRecordsExceptionInTelemetry() {
+ // Create a SyncManager that throws when pushEvent is called
+ SyncManager throwingSyncManager = mock(SyncManager.class);
+ doThrow(new RuntimeException("Push failed"))
+ .when(throwingSyncManager).pushEvent(any(Event.class));
+
+ SplitFactoryImpl.EventsTrackerProvider provider = new SplitFactoryImpl.EventsTrackerProvider(
+ mSplitsStorage,
+ mTelemetryStorage,
+ throwingSyncManager);
+ when(mSplitsStorage.isValidTrafficType(anyString())).thenReturn(true);
+
+ Tracker tracker = provider.getEventsTracker();
+
+ boolean result = tracker.track("key", "user", "event", 1.0, null, true);
+
+ // Track should return false due to exception
+ assertEquals(false, result);
+ verify(mTelemetryStorage).recordException(Method.TRACK);
+ }
+}
diff --git a/main/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java b/main/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java
index 4f8432e18..3d06f2f89 100644
--- a/main/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java
+++ b/main/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java
@@ -1,5 +1,7 @@
package io.split.android.client;
+import io.split.android.client.validators.PropertyValidatorAdapter;
+import io.split.android.client.validators.PropertyValidatorImpl;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
@@ -85,7 +87,7 @@ public void setUp() {
mSplitsStorage,
new ValidationMessageLoggerImpl(),
mFlagSetsValidator,
- new PropertyValidatorImpl(),
+ new PropertyValidatorAdapter(new PropertyValidatorImpl(new ValidationMessageLoggerImpl())),
new FallbackTreatmentsCalculatorImpl(FallbackTreatmentsConfiguration.builder().build()));
when(evaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenReturn(new EvaluationResult("test", "label"));
diff --git a/main/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java b/main/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java
index 222de7750..c7a3e0ec6 100644
--- a/main/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java
+++ b/main/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java
@@ -1,5 +1,7 @@
package io.split.android.client;
+import io.split.android.client.validators.PropertyValidatorAdapter;
+import io.split.android.client.validators.PropertyValidatorImpl;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
@@ -78,7 +80,7 @@ public void setUp() {
mFlagSetsFilter,
mSplitsStorage, new ValidationMessageLoggerImpl(),
new FlagSetsValidatorImpl(),
- new PropertyValidatorImpl(),
+ new PropertyValidatorAdapter(new PropertyValidatorImpl(new ValidationMessageLoggerImpl())),
new FallbackTreatmentsCalculatorImpl(FallbackTreatmentsConfiguration.builder().build()));
when(evaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenReturn(new EvaluationResult("test", "label"));
diff --git a/main/src/test/java/io/split/android/client/TreatmentManagerTest.java b/main/src/test/java/io/split/android/client/TreatmentManagerTest.java
index ce889d69d..6a2dd7988 100644
--- a/main/src/test/java/io/split/android/client/TreatmentManagerTest.java
+++ b/main/src/test/java/io/split/android/client/TreatmentManagerTest.java
@@ -1,5 +1,7 @@
package io.split.android.client;
+import io.split.android.client.validators.PropertyValidatorAdapter;
+import io.split.android.client.validators.PropertyValidatorImpl;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.argThat;
@@ -372,7 +374,7 @@ private TreatmentManager createTreatmentManager(String matchingKey, String bucke
new KeyValidatorImpl(), splitValidator,
mock(ImpressionListener.FederatedImpressionListener.class), config.labelsEnabled(), eventsManager,
mock(AttributesManager.class), mock(AttributesMerger.class),
- mock(TelemetryStorageProducer.class), mFlagSetsFilter, mSplitsStorage, validationLogger, new FlagSetsValidatorImpl(), new PropertyValidatorImpl(),
+ mock(TelemetryStorageProducer.class), mFlagSetsFilter, mSplitsStorage, validationLogger, new FlagSetsValidatorImpl(), new PropertyValidatorAdapter(new PropertyValidatorImpl(new ValidationMessageLoggerImpl())),
new FallbackTreatmentsCalculatorImpl(FallbackTreatmentsConfiguration.builder().build()));
}
@@ -403,7 +405,7 @@ private TreatmentManagerImpl initializeTreatmentManager(Evaluator evaluator) {
telemetryStorageProducer,
mFlagSetsFilter,
mSplitsStorage,
- new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl(), new PropertyValidatorImpl(),
+ new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl(), new PropertyValidatorAdapter(new PropertyValidatorImpl(new ValidationMessageLoggerImpl())),
new FallbackTreatmentsCalculatorImpl(FallbackTreatmentsConfiguration.builder().build()));
}
diff --git a/main/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java b/main/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java
index aa12c3d5e..8d51f2263 100644
--- a/main/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java
+++ b/main/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java
@@ -1,5 +1,7 @@
package io.split.android.client;
+import io.split.android.client.validators.PropertyValidatorAdapter;
+import io.split.android.client.validators.PropertyValidatorImpl;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -157,7 +159,7 @@ private void initializeTreatmentManager() {
mAttributesMerger,
mTelemetryStorageProducer,
mFlagSetsFilter,
- mSplitsStorage, new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl(), new PropertyValidatorImpl(),
+ mSplitsStorage, new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl(), new PropertyValidatorAdapter(new PropertyValidatorImpl(new ValidationMessageLoggerImpl())),
new FallbackTreatmentsCalculatorImpl(FallbackTreatmentsConfiguration.builder().build()));
}
diff --git a/main/src/test/java/io/split/android/client/UserConsentManagerTest.java b/main/src/test/java/io/split/android/client/UserConsentManagerTest.java
index 8dc3a2194..0d133342f 100644
--- a/main/src/test/java/io/split/android/client/UserConsentManagerTest.java
+++ b/main/src/test/java/io/split/android/client/UserConsentManagerTest.java
@@ -17,6 +17,7 @@
import io.split.android.client.shared.UserConsent;
import io.split.android.client.storage.events.EventsStorage;
import io.split.android.client.storage.impressions.ImpressionsStorage;
+import io.split.android.client.tracker.Tracker;
import io.split.android.fake.SplitTaskExecutorStub;
public class UserConsentManagerTest {
@@ -30,7 +31,7 @@ public class UserConsentManagerTest {
@Mock
private SyncManager mSyncManager;
@Mock
- private EventsTracker mEventsTracker;
+ private Tracker mEventsTracker;
@Mock
private SplitFactoryImpl.EventsTrackerProvider mEventsTrackerProvider;
@Mock
diff --git a/main/src/test/java/io/split/android/client/localhost/LocalhostTrafficTypeValidatorTest.java b/main/src/test/java/io/split/android/client/localhost/LocalhostTrafficTypeValidatorTest.java
new file mode 100644
index 000000000..6a0b04777
--- /dev/null
+++ b/main/src/test/java/io/split/android/client/localhost/LocalhostTrafficTypeValidatorTest.java
@@ -0,0 +1,43 @@
+package io.split.android.client.localhost;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class LocalhostTrafficTypeValidatorTest {
+
+ private LocalhostTrafficTypeValidator mValidator;
+
+ @Before
+ public void setUp() {
+ mValidator = new LocalhostTrafficTypeValidator();
+ }
+
+ @Test
+ public void isValidReturnsTrueForAnyTrafficType() {
+ assertTrue(mValidator.isValid("user"));
+ assertTrue(mValidator.isValid("account"));
+ assertTrue(mValidator.isValid("random_traffic_type"));
+ }
+
+ @Test
+ public void isValidReturnsTrueForNull() {
+ assertTrue(mValidator.isValid(null));
+ }
+
+ @Test
+ public void isValidReturnsTrueForEmptyString() {
+ assertTrue(mValidator.isValid(""));
+ }
+
+ @Test
+ public void isValidReturnsTrueForWhitespace() {
+ assertTrue(mValidator.isValid(" "));
+ }
+
+ @Test
+ public void isValidReturnsTrueForSpecialCharacters() {
+ assertTrue(mValidator.isValid("!@#$%^&*()"));
+ }
+}
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