diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 64db71862..107a9a999 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -49,3 +49,32 @@ jobs: with: name: java-build-reports-realtime path: java/build/reports/ + check-rest-okhttp: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Set up the JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - run: ./gradlew :java:testRestSuite -Pokhttp + + check-realtime-okhttp: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Set up the JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - run: ./gradlew :java:testRealtimeSuite -Pokhttp diff --git a/README.md b/README.md index 69ea2b260..449ec77ea 100644 --- a/README.md +++ b/README.md @@ -500,6 +500,67 @@ realtime.setAndroidContext(context); realtime.push.activate(); ``` +## Using Ably SDK Under a Proxy + +When working in environments where outbound internet access is restricted, such as behind a corporate proxy, the Ably SDK allows you to configure a proxy server for HTTP and WebSocket connections. + +### Add the Required Dependency + +You need to use **OkHttp** library for making HTTP calls and WebSocket connections in the Ably SDK to get proxy support both for your Rest and Realtime clients. + +Add the following dependency to your `build.gradle` file: + +```groovy +dependencies { + runtimeOnly("io.ably:network-client-okhttp:1.2.43") +} +``` + +### Configure Proxy Settings + +After adding the required OkHttp dependency, you need to configure the proxy settings for your Ably client. This can be done by setting the proxy options in the `ClientOptions` object when you instantiate the Ably SDK. + +Here’s an example of how to configure and use a proxy: + +#### Java Example + +```java +import io.ably.lib.realtime.AblyRealtime; +import io.ably.lib.rest.AblyRest; +import io.ably.lib.transport.Defaults; +import io.ably.lib.types.ClientOptions; +import io.ably.lib.types.ProxyOptions; +import io.ably.lib.http.HttpAuth; + +public class AblyWithProxy { + public static void main(String[] args) throws Exception { + // Configure Ably Client options + ClientOptions options = new ClientOptions(); + + // Setup proxy settings + ProxyOptions proxy = new ProxyOptions(); + proxy.host = "your-proxy-host"; // Replace with your proxy host + proxy.port = 8080; // Replace with your proxy port + + // Optional: If the proxy requires authentication + proxy.username = "your-username"; // Replace with proxy username + proxy.password = "your-password"; // Replace with proxy password + proxy.prefAuthType = HttpAuth.Type.BASIC; // Choose your preferred authentication type (e.g., BASIC or DIGEST) + + // Attach the proxy settings to the client options + options.proxy = proxy; + + // Create an instance of Ably using the configured options + AblyRest ably = new AblyRest(options); + + // Alternatively, for real-time connections + AblyRealtime ablyRealtime = new AblyRealtime(options); + + // Use the Ably client as usual + } +} +``` + ## Resources Visit https://www.ably.com/docs for a complete API reference and more examples. diff --git a/build.gradle.kts b/build.gradle.kts index 9452386ca..c20fc7ead 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.android.library) apply false alias(libs.plugins.maven.publish) apply false alias(libs.plugins.lombok) apply false + alias(libs.plugins.test.retry) apply false } subprojects { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3545e89ea..542072c78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,8 @@ dexmaker = "1.4" android-retrostreams = "1.7.4" maven-publish = "0.29.0" lombok = "8.10" +okhttp = "4.12.0" +test-retry = "1.6.0" [libraries] gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } @@ -38,6 +40,7 @@ dexmaker = { group = "com.crittercism.dexmaker", name = "dexmaker", version.ref dexmaker-dx = { group = "com.crittercism.dexmaker", name = "dexmaker-dx", version.ref = "dexmaker" } dexmaker-mockito = { group = "com.crittercism.dexmaker", name = "dexmaker-mockito", version.ref = "dexmaker" } android-retrostreams = { group = "net.sourceforge.streamsupport", name = "android-retrostreams", version.ref = "android-retrostreams" } +okhttp = { group ="com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } [bundles] common = ["msgpack", "vcdiff-core"] @@ -49,3 +52,4 @@ android-library = { id = "com.android.library", version.ref = "agp" } build-config = { id = "com.github.gmazzo.buildconfig", version.ref = "build-config" } maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } lombok = { id = "io.freefair.lombok", version.ref = "lombok" } +test-retry = { id = "org.gradle.test-retry", version.ref = "test-retry" } diff --git a/java/build.gradle.kts b/java/build.gradle.kts index e537e6cfa..45b0c4e39 100644 --- a/java/build.gradle.kts +++ b/java/build.gradle.kts @@ -3,6 +3,7 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat plugins { alias(libs.plugins.build.config) alias(libs.plugins.maven.publish) + alias(libs.plugins.test.retry) checkstyle `java-library` } @@ -20,7 +21,11 @@ dependencies { api(libs.gson) implementation(libs.bundles.common) implementation(project(":network-client-core")) - runtimeOnly(project(":network-client-default")) + if (findProperty("okhttp") == null) { + runtimeOnly(project(":network-client-default")) + } else { + runtimeOnly(project(":network-client-okhttp")) + } testImplementation(libs.bundles.tests) } @@ -59,6 +64,12 @@ tasks.register("testRealtimeSuite") { testLogging { exceptionFormat = TestExceptionFormat.FULL } + retry { + maxRetries.set(3) + maxFailures.set(8) + failOnPassedAfterRetry.set(false) + failOnSkippedAfterRetry.set(false) + } } tasks.register("testRestSuite") { @@ -72,6 +83,12 @@ tasks.register("testRestSuite") { testLogging { exceptionFormat = TestExceptionFormat.FULL } + retry { + maxRetries.set(3) + maxFailures.set(8) + failOnPassedAfterRetry.set(false) + failOnSkippedAfterRetry.set(false) + } } /* diff --git a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java index cbcf58b5f..a44ee0194 100644 --- a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java +++ b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java @@ -1,12 +1,13 @@ package io.ably.lib.transport; import io.ably.lib.http.HttpUtils; +import io.ably.lib.network.EngineType; +import io.ably.lib.network.NotConnectedException; import io.ably.lib.network.WebSocketClient; import io.ably.lib.network.WebSocketEngine; import io.ably.lib.network.WebSocketEngineConfig; import io.ably.lib.network.WebSocketEngineFactory; import io.ably.lib.network.WebSocketListener; -import io.ably.lib.network.NotConnectedException; import io.ably.lib.types.AblyException; import io.ably.lib.types.ErrorInfo; import io.ably.lib.types.Param; @@ -17,6 +18,8 @@ import javax.net.ssl.SSLContext; import java.nio.ByteBuffer; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.util.Timer; import java.util.TimerTask; @@ -48,16 +51,43 @@ public class WebSocketTransport implements ITransport { private String wsUri; private ConnectListener connectListener; private WebSocketClient webSocketClient; + private final WebSocketEngine webSocketEngine; + private boolean activityCheckTurnedOff = false; + /****************** * protected constructor ******************/ - protected WebSocketTransport(TransportParams params, ConnectionManager connectionManager) { this.params = params; this.connectionManager = connectionManager; this.channelBinaryMode = params.options.useBinaryProtocol; - /* We do not require Ably heartbeats, as we can use WebSocket pings instead. */ - params.heartbeats = false; + this.webSocketEngine = createWebSocketEngine(params); + params.heartbeats = !this.webSocketEngine.isPingListenerSupported(); + + } + + private static WebSocketEngine createWebSocketEngine(TransportParams params) { + WebSocketEngineFactory engineFactory = WebSocketEngineFactory.getFirstAvailable(); + Log.v(TAG, String.format("Using %s WebSocket Engine", engineFactory.getEngineType().name())); + WebSocketEngineConfig.WebSocketEngineConfigBuilder configBuilder = WebSocketEngineConfig.builder(); + configBuilder + .tls(params.options.tls) + .host(params.host) + .proxy(ClientOptionsUtils.convertToProxyConfig(params.getClientOptions())); + + // OkHttp supports modern TLS algorithms by default + if (params.options.tls && engineFactory.getEngineType() != EngineType.OKHTTP) { + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, null, null); + SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory()); + configBuilder.sslSocketFactory(factory); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new IllegalStateException("Can't get safe tls algorithms", e); + } + } + + return engineFactory.create(configBuilder.build()); } /****************** @@ -78,24 +108,7 @@ public void connect(ConnectListener connectListener) { Log.d(TAG, "connect(); wsUri = " + wsUri); synchronized (this) { - WebSocketEngineFactory engineFactory = WebSocketEngineFactory.getFirstAvailable(); - Log.v(TAG, String.format("Using %s WebSocket Engine", engineFactory.getEngineType().name())); - - WebSocketEngineConfig.WebSocketEngineConfigBuilder configBuilder = WebSocketEngineConfig.builder(); - configBuilder - .tls(isTls) - .host(params.host) - .proxy(ClientOptionsUtils.convertToProxyConfig(params.getClientOptions())); - - if (isTls) { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, null, null); - SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory()); - configBuilder.sslSocketFactory(factory); - } - - WebSocketEngine engine = engineFactory.create(configBuilder.build()); - webSocketClient = engine.create(wsUri, new WebSocketHandler(this::receive)); + webSocketClient = this.webSocketEngine.create(wsUri, new WebSocketHandler(this::receive)); } webSocketClient.connect(); } catch (AblyException e) { @@ -161,6 +174,16 @@ protected void preProcessReceivedMessage(ProtocolMessage message) { //Gives the chance to child classes to do message pre-processing } + /** + * Visible For Testing + *

+ * We need to turn off activity check for some tests (e.g. io.ably.lib.test.realtime.RealtimeConnectFailTest.disconnect_retry_channel_timeout_jitter_after_consistent_detach[binary_protocol]) + * Those tests expects that activity checks are passing, but protocol messages are not coming + */ + protected void turnOffActivityCheckIfPingListenerIsNotSupported() { + if (!webSocketEngine.isPingListenerSupported()) activityCheckTurnedOff = true; + } + public String toString() { return WebSocketTransport.class.getName() + " {" + getURL() + "}"; } @@ -307,7 +330,7 @@ private synchronized void dispose() { private synchronized void flagActivity() { lastActivityTime = System.currentTimeMillis(); connectionManager.setLastActivity(lastActivityTime); - if (activityTimerTask == null && connectionManager.maxIdleInterval != 0) { + if (activityTimerTask == null && connectionManager.maxIdleInterval != 0 && !activityCheckTurnedOff) { /* No timer currently running because previously there was no * maxIdleInterval configured, but now there is a * maxIdleInterval configured. Call checkActivity so a timer diff --git a/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java b/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java index bbf3ba0d0..15a4cbfad 100644 --- a/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java +++ b/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java @@ -155,6 +155,7 @@ private MockWebsocketTransport(TransportParams givenTransportParams, TransportPa super(transformedTransportParams, connectionManager); this.givenTransportParams = givenTransportParams; this.transformedTransportParams = transformedTransportParams; + turnOffActivityCheckIfPingListenerIsNotSupported(); } public List getSentMessages() { diff --git a/network-client-core/build.gradle.kts b/network-client-core/build.gradle.kts index 9b3ba996a..f7bb62dd6 100644 --- a/network-client-core/build.gradle.kts +++ b/network-client-core/build.gradle.kts @@ -1,6 +1,7 @@ plugins { `java-library` alias(libs.plugins.lombok) + alias(libs.plugins.maven.publish) } java { diff --git a/network-client-core/gradle.properties b/network-client-core/gradle.properties new file mode 100644 index 000000000..f37ee24fe --- /dev/null +++ b/network-client-core/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=network-client-core +POM_NAME=Core HTTP client abstraction +POM_DESCRIPTION=Core HTTP client abstraction +POM_PACKAGING=jar diff --git a/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngine.java b/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngine.java index a4a236757..af29dce14 100644 --- a/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngine.java +++ b/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngine.java @@ -5,4 +5,5 @@ */ public interface WebSocketEngine { WebSocketClient create(String url, WebSocketListener listener); + boolean isPingListenerSupported(); } diff --git a/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineFactory.java b/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineFactory.java index ce22567b3..c5693d655 100644 --- a/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineFactory.java +++ b/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineFactory.java @@ -19,7 +19,7 @@ static WebSocketEngineFactory getFirstAvailable() { static WebSocketEngineFactory tryGetOkWebSocketFactory() { try { - Class okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkWebSocketEngineFactory"); + Class okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkHttpWebSocketEngineFactory"); return (WebSocketEngineFactory) okWebSocketFactoryClass.getDeclaredConstructor().newInstance(); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { diff --git a/network-client-default/build.gradle.kts b/network-client-default/build.gradle.kts index 4cf238353..9b19b174f 100644 --- a/network-client-default/build.gradle.kts +++ b/network-client-default/build.gradle.kts @@ -10,6 +10,6 @@ java { } dependencies { - api(project(":network-client-core")) + implementation(project(":network-client-core")) implementation(libs.java.websocket) } diff --git a/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngine.java b/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngine.java index e8c5ae00e..a73f9f580 100644 --- a/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngine.java +++ b/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngine.java @@ -17,4 +17,9 @@ public WebSocketClient create(String url, WebSocketListener listener) { } return client; } + + @Override + public boolean isPingListenerSupported() { + return true; + } } diff --git a/network-client-okhttp/build.gradle.kts b/network-client-okhttp/build.gradle.kts new file mode 100644 index 000000000..7e3118764 --- /dev/null +++ b/network-client-okhttp/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `java-library` + alias(libs.plugins.lombok) + alias(libs.plugins.maven.publish) +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation(project(":network-client-core")) + implementation(libs.okhttp) +} diff --git a/network-client-okhttp/gradle.properties b/network-client-okhttp/gradle.properties new file mode 100644 index 000000000..4b648381c --- /dev/null +++ b/network-client-okhttp/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=network-client-okhttp +POM_NAME=Default HTTP client +POM_DESCRIPTION=Default implementation for HTTP client +POM_PACKAGING=jar diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpCall.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpCall.java new file mode 100644 index 000000000..643697391 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpCall.java @@ -0,0 +1,45 @@ +package io.ably.lib.network; + +import okhttp3.Call; +import okhttp3.Response; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; + +public class OkHttpCall implements HttpCall { + private final Call call; + + public OkHttpCall(Call call) { + this.call = call; + } + + @Override + public HttpResponse execute() { + try (Response response = call.execute()) { + return HttpResponse.builder() + .headers(response.headers().toMultimap()) + .code(response.code()) + .message(response.message()) + .body( + response.body() != null && response.body().contentType() != null + ? new HttpBody(response.body().contentType().toString(), response.body().bytes()) + : null + ) + .build(); + + } catch (ConnectException | SocketTimeoutException | UnknownHostException | NoRouteToHostException fce) { + throw new FailedConnectionException(fce); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + + } + + @Override + public void cancel() { + call.cancel(); + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngine.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngine.java new file mode 100644 index 000000000..50faa3610 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngine.java @@ -0,0 +1,32 @@ +package io.ably.lib.network; + +import okhttp3.Call; +import okhttp3.OkHttpClient; + +import java.util.concurrent.TimeUnit; + +public class OkHttpEngine implements HttpEngine { + + private final OkHttpClient client; + private final HttpEngineConfig config; + + public OkHttpEngine(OkHttpClient client, HttpEngineConfig config) { + this.client = client; + this.config = config; + } + + @Override + public HttpCall call(HttpRequest request) { + Call call = client.newBuilder() + .connectTimeout(request.getHttpOpenTimeout(), TimeUnit.MILLISECONDS) + .readTimeout(request.getHttpReadTimeout(), TimeUnit.MILLISECONDS) + .build() + .newCall(OkHttpUtils.toOkhttpRequest(request)); + return new OkHttpCall(call); + } + + @Override + public boolean isUsingProxy() { + return config.getProxy() != null; + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngineFactory.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngineFactory.java new file mode 100644 index 000000000..2cf65a9a8 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngineFactory.java @@ -0,0 +1,17 @@ +package io.ably.lib.network; + +import okhttp3.OkHttpClient; + +public class OkHttpEngineFactory implements HttpEngineFactory { + @Override + public HttpEngine create(HttpEngineConfig config) { + OkHttpClient.Builder connectionBuilder = new OkHttpClient.Builder(); + OkHttpUtils.injectProxySetting(config.getProxy(), connectionBuilder); + return new OkHttpEngine(connectionBuilder.build(), config); + } + + @Override + public EngineType getEngineType() { + return EngineType.OKHTTP; + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpUtils.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpUtils.java new file mode 100644 index 000000000..2bd566153 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpUtils.java @@ -0,0 +1,51 @@ +package io.ably.lib.network; + +import okhttp3.Credentials; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.List; +import java.util.Map; + +public class OkHttpUtils { + public static void injectProxySetting(ProxyConfig proxyConfig, OkHttpClient.Builder connectionBuilder) { + if (proxyConfig == null) return; + connectionBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyConfig.getHost(), proxyConfig.getPort()))); + if (proxyConfig.getUsername() == null || proxyConfig.getAuthType() != ProxyAuthType.BASIC) return; + String username = proxyConfig.getUsername(); + String password = proxyConfig.getPassword(); + connectionBuilder.proxyAuthenticator((route, response) -> { + String credential = Credentials.basic(username, password); + return response.request().newBuilder() + .header("Proxy-Authorization", credential) + .build(); + }); + } + + public static Request toOkhttpRequest(HttpRequest request) { + Request.Builder builder = new Request.Builder() + .url(request.getUrl()); + + RequestBody body = null; + + if (request.getBody() != null) { + body = RequestBody.create(request.getBody().getContent(), MediaType.parse(request.getBody().getContentType())); + } + + builder.method(request.getMethod(), body); + for (Map.Entry> entry : request.getHeaders().entrySet()) { + String headerName = entry.getKey(); + List values = entry.getValue(); + for (String headerValue : values) { + builder.addHeader(headerName, headerValue); + } + } + + return builder.build(); + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketClient.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketClient.java new file mode 100644 index 000000000..7341eb71a --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketClient.java @@ -0,0 +1,87 @@ +package io.ably.lib.network; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okio.ByteString; + +import java.nio.ByteBuffer; + +public class OkHttpWebSocketClient implements WebSocketClient { + private final OkHttpClient connection; + private final Request request; + private final WebSocketListener listener; + private WebSocket webSocket; + + public OkHttpWebSocketClient(OkHttpClient connection, Request request, WebSocketListener listener) { + this.connection = connection; + this.request = request; + this.listener = listener; + } + + @Override + public void connect() { + webSocket = connection.newWebSocket(request, new WebSocketHandler(listener)); + } + + @Override + public void close() { + webSocket.close(1000, "Close"); + } + + @Override + public void close(int code, String reason) { + webSocket.close(code, reason); + } + + @Override + public void cancel(int code, String reason) { + webSocket.cancel(); + listener.onClose(code, reason); + } + + @Override + public void send(byte[] bytes) { + webSocket.send(ByteString.of(bytes)); + } + + @Override + public void send(String message) { + webSocket.send(message); + } + + private static class WebSocketHandler extends okhttp3.WebSocketListener { + private final WebSocketListener listener; + + private WebSocketHandler(WebSocketListener listener) { + super(); + this.listener = listener; + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + listener.onClose(code, reason); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + listener.onError(t); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + listener.onMessage(text); + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + listener.onMessage(ByteBuffer.wrap(bytes.toByteArray())); + } + + @Override + public void onOpen(WebSocket webSocket, Response response) { + listener.onOpen(); + } + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngine.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngine.java new file mode 100644 index 000000000..7715501ab --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngine.java @@ -0,0 +1,32 @@ +package io.ably.lib.network; + +import okhttp3.OkHttpClient; +import okhttp3.Request; + +public class OkHttpWebSocketEngine implements WebSocketEngine { + private final WebSocketEngineConfig config; + + public OkHttpWebSocketEngine(WebSocketEngineConfig config) { + this.config = config; + } + + @Override + public WebSocketClient create(String url, WebSocketListener listener) { + OkHttpClient.Builder connectionBuilder = new OkHttpClient.Builder(); + + Request.Builder requestBuilder = new Request.Builder().url(url); + + OkHttpUtils.injectProxySetting(config.getProxy(), connectionBuilder); + + if (config.getSslSocketFactory() != null) { + connectionBuilder.sslSocketFactory(config.getSslSocketFactory()); + } + + return new OkHttpWebSocketClient(connectionBuilder.build(), requestBuilder.build(), listener); + } + + @Override + public boolean isPingListenerSupported() { + return false; + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngineFactory.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngineFactory.java new file mode 100644 index 000000000..24b7dcf20 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngineFactory.java @@ -0,0 +1,13 @@ +package io.ably.lib.network; + +public class OkHttpWebSocketEngineFactory implements WebSocketEngineFactory { + @Override + public WebSocketEngine create(WebSocketEngineConfig config) { + return new OkHttpWebSocketEngine(config); + } + + @Override + public EngineType getEngineType() { + return EngineType.OKHTTP; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e905e3922..136b798ca 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,3 +13,4 @@ include("android") include("gradle-lint") include("network-client-core") include("network-client-default") +include("network-client-okhttp")