diff --git a/.github/workflows/android_build.yml b/.github/workflows/android_build.yml
index 033ed4ba94..e0e7028bce 100644
--- a/.github/workflows/android_build.yml
+++ b/.github/workflows/android_build.yml
@@ -100,7 +100,7 @@ jobs:
java-package: jdk
architecture: x64
- name: 'Install dependencies'
- run: ./ci/mac_ci_setup.sh --android
+ run: ./ci/mac_ci_setup.sh
- name: 'Download aar'
uses: actions/cache@v2
id: check-cache
@@ -128,3 +128,89 @@ jobs:
adb shell am start -n io.envoyproxy.envoymobile.helloenvoykotlin/.MainActivity
- name: 'Check connectivity'
run: adb logcat -e "received headers with status 200" -m 1
+ kotlinbaselineapp:
+ name: kotlin_baseline_app
+ needs: androidbuild
+ runs-on: macos-11
+ timeout-minutes: 25
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ submodules: true
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '8'
+ java-package: jdk
+ architecture: x64
+ - name: 'Install dependencies'
+ run: ./ci/mac_ci_setup.sh --android
+ - name: 'Download aar'
+ uses: actions/cache@v2
+ id: check-cache
+ with:
+ key: aar-${{ github.sha }}
+ path: dist/envoy.aar
+ - name: 'Short-circuit'
+ if: steps.check-cache.outputs.cache-hit != 'true'
+ run: exit 1
+ - name: 'Start simulator'
+ run: ./ci/mac_start_emulator.sh
+ # Return to using:
+ # ./bazelw mobile-install --fat_apk_cpu=x86 --start_app //examples/kotlin/hello_world:hello_envoy_kt
+ # When https://github.com/envoyproxy/envoy-mobile/issues/853 is fixed.
+ - name: 'Start kotlin app'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ ./bazelw build \
+ --config=remote-ci-macos \
+ --remote_header="Authorization=Bearer $GITHUB_TOKEN" \
+ --fat_apk_cpu=x86 \
+ //test/kotlin/apps/baseline:hello_envoy_kt
+ adb install -r --no-incremental bazel-bin/test/kotlin/apps/baseline/hello_envoy_kt.apk
+ adb shell am start -n io.envoyproxy.envoymobile.helloenvoykotlin/.MainActivity
+ - name: 'Check connectivity'
+ run: adb logcat -e "received headers with status 200" -m 1
+ kotlinexperimentalapp:
+ name: kotlin_experimental_app
+ needs: androidbuild
+ runs-on: macos-11
+ timeout-minutes: 25
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ submodules: true
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '8'
+ java-package: jdk
+ architecture: x64
+ - name: 'Install dependencies'
+ run: ./ci/mac_ci_setup.sh
+ - name: 'Download aar'
+ uses: actions/cache@v2
+ id: check-cache
+ with:
+ key: aar-${{ github.sha }}
+ path: dist/envoy.aar
+ - name: 'Short-circuit'
+ if: steps.check-cache.outputs.cache-hit != 'true'
+ run: exit 1
+ - name: 'Start simulator'
+ run: ./ci/mac_start_emulator.sh
+ # Return to using:
+ # ./bazelw mobile-install --fat_apk_cpu=x86 --start_app //examples/kotlin/hello_world:hello_envoy_kt
+ # When https://github.com/envoyproxy/envoy-mobile/issues/853 is fixed.
+ - name: 'Start kotlin app'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ ./bazelw build \
+ --config=remote-ci-macos \
+ --remote_header="Authorization=Bearer $GITHUB_TOKEN" \
+ --fat_apk_cpu=x86 \
+ //test/kotlin/apps/experimental:hello_envoy_kt
+ adb install -r --no-incremental bazel-bin/test/kotlin/apps/experimental/hello_envoy_kt.apk
+ adb shell am start -n io.envoyproxy.envoymobile.helloenvoykotlin/.MainActivity
+ - name: 'Check connectivity'
+ run: adb logcat -e "received headers with status 200" -m 1
diff --git a/.github/workflows/ios_build.yml b/.github/workflows/ios_build.yml
index 5ec62bb7d4..c52f81c717 100644
--- a/.github/workflows/ios_build.yml
+++ b/.github/workflows/ios_build.yml
@@ -68,6 +68,74 @@ jobs:
- run: cat /tmp/envoy.log
if: ${{ failure() || cancelled() }}
name: 'Log app run'
+ swiftbaselineapp:
+ name: swift_baseline_app
+ needs: iosbuild
+ runs-on: macos-11
+ timeout-minutes: 25
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ submodules: true
+ - run: ./ci/mac_ci_setup.sh
+ name: 'Install dependencies'
+ - uses: actions/cache@v2
+ id: check-cache
+ with:
+ key: framework-${{ github.sha }}
+ path: dist/Envoy.framework
+ name: 'Download framework'
+ - run: exit 1
+ if: steps.check-cache.outputs.cache-hit != 'true'
+ name: 'Short-circuit'
+ - env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: ./bazelw build --config=ios --config=remote-ci-macos --remote_header="Authorization=Bearer $GITHUB_TOKEN" //test/swift/apps/baseline:app
+ name: 'Build swift app'
+ # Run the app in the background and redirect logs.
+ - env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: ./bazelw run --config=ios --config=remote-ci-macos --remote_header="Authorization=Bearer $GITHUB_TOKEN" //test/swift/apps/baseline:app &> /tmp/envoy.log &
+ name: 'Run swift app'
+ - run: sed '/received headers with status 200/q' <(touch /tmp/envoy.log && tail -F /tmp/envoy.log)
+ name: 'Check connectivity'
+ - run: cat /tmp/envoy.log
+ if: ${{ failure() || cancelled() }}
+ name: 'Log app run'
+ swiftexperimentalapp:
+ name: swift_experimental_app
+ needs: iosbuild
+ runs-on: macos-11
+ timeout-minutes: 25
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ submodules: true
+ - run: ./ci/mac_ci_setup.sh
+ name: 'Install dependencies'
+ - uses: actions/cache@v2
+ id: check-cache
+ with:
+ key: framework-${{ github.sha }}
+ path: dist/Envoy.framework
+ name: 'Download framework'
+ - run: exit 1
+ if: steps.check-cache.outputs.cache-hit != 'true'
+ name: 'Short-circuit'
+ - env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: ./bazelw build --config=ios --config=remote-ci-macos --remote_header="Authorization=Bearer $GITHUB_TOKEN" //test/swift/apps/experimental:app
+ name: 'Build swift app'
+ # Run the app in the background and redirect logs.
+ - env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: ./bazelw run --config=ios --config=remote-ci-macos --remote_header="Authorization=Bearer $GITHUB_TOKEN" //test/swift/apps/experimental:app &> /tmp/envoy.log &
+ name: 'Run swift app'
+ - run: sed '/received headers with status 200/q' <(touch /tmp/envoy.log && tail -F /tmp/envoy.log)
+ name: 'Check connectivity'
+ - run: cat /tmp/envoy.log
+ if: ${{ failure() || cancelled() }}
+ name: 'Log app run'
swiftasyncawait:
name: swift_async_await
needs: iosbuild
diff --git a/test/kotlin/apps/baseline/.bazelproject b/test/kotlin/apps/baseline/.bazelproject
new file mode 100644
index 0000000000..651096b4a0
--- /dev/null
+++ b/test/kotlin/apps/baseline/.bazelproject
@@ -0,0 +1,22 @@
+workspace_type: android
+
+directories:
+ -bazel-bin
+ -bazel-instant-android
+ -bazel-out
+ -bazel-testlogs
+ -buck-out
+ -build
+ examples/kotlin/hello_world
+
+import_run_configurations:
+ examples/kotlin/hello_world/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml
+
+targets:
+ //examples/kotlin/hello_world:hello_envoy_kt
+
+additional_languages:
+ kotlin
+ java
+ android
+ c
diff --git a/test/kotlin/apps/baseline/AndroidManifest.xml b/test/kotlin/apps/baseline/AndroidManifest.xml
new file mode 100644
index 0000000000..89427cc582
--- /dev/null
+++ b/test/kotlin/apps/baseline/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/kotlin/apps/baseline/AsyncDemoFilter.kt b/test/kotlin/apps/baseline/AsyncDemoFilter.kt
new file mode 100644
index 0000000000..a45ec5f7af
--- /dev/null
+++ b/test/kotlin/apps/baseline/AsyncDemoFilter.kt
@@ -0,0 +1,95 @@
+package io.envoyproxy.envoymobile.helloenvoykotlin
+
+import io.envoyproxy.envoymobile.AsyncResponseFilter
+import io.envoyproxy.envoymobile.EnvoyError
+import io.envoyproxy.envoymobile.FilterDataStatus
+import io.envoyproxy.envoymobile.FilterHeadersStatus
+import io.envoyproxy.envoymobile.FilterResumeStatus
+import io.envoyproxy.envoymobile.FilterTrailersStatus
+import io.envoyproxy.envoymobile.FinalStreamIntel
+import io.envoyproxy.envoymobile.ResponseFilterCallbacks
+import io.envoyproxy.envoymobile.ResponseHeaders
+import io.envoyproxy.envoymobile.ResponseTrailers
+import io.envoyproxy.envoymobile.StreamIntel
+import java.nio.ByteBuffer
+import java.util.Timer
+import kotlin.concurrent.schedule
+
+/**
+ * Example of a more complex HTTP filter that pauses processing on the response filter chain,
+ * buffers until the response is complete, then asynchronously triggers filter chain resumption
+ * while setting a new header. Also demonstrates safety of re-entrancy in async callbacks.
+ */
+class AsyncDemoFilter : AsyncResponseFilter {
+ private lateinit var callbacks: ResponseFilterCallbacks
+
+ override fun onResponseHeaders(
+ headers: ResponseHeaders,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterHeadersStatus {
+ // If this is the end of the stream, asynchronously resume response processing via callback.
+ if (endStream) {
+ Timer("AsyncResume", false).schedule(100) {
+ callbacks.resumeResponse()
+ }
+ }
+ return FilterHeadersStatus.StopIteration()
+ }
+
+ override fun onResponseData(
+ body: ByteBuffer,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterDataStatus {
+ // If this is the end of the stream, asynchronously resume response processing via callback.
+ if (endStream) {
+ Timer("AsyncResume", false).schedule(100) {
+ callbacks.resumeResponse()
+ }
+ }
+ return FilterDataStatus.StopIterationAndBuffer()
+ }
+
+ override fun onResponseTrailers(
+ trailers: ResponseTrailers,
+ streamIntel: StreamIntel
+ ): FilterTrailersStatus {
+ // Trailers imply end of stream, so asynchronously resume response processing via callbacka
+ Timer("AsyncResume", false).schedule(100) {
+ callbacks.resumeResponse()
+ }
+ return FilterTrailersStatus.StopIteration()
+ }
+
+ override fun setResponseFilterCallbacks(callbacks: ResponseFilterCallbacks) {
+ this.callbacks = callbacks
+ }
+
+ override fun onResumeResponse(
+ headers: ResponseHeaders?,
+ data: ByteBuffer?,
+ trailers: ResponseTrailers?,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterResumeStatus {
+ val builder = headers!!.toResponseHeadersBuilder()
+ .add("async-filter-demo", "1")
+ return FilterResumeStatus.ResumeIteration(builder.build(), data, trailers)
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onError(
+ error: EnvoyError,
+ finalStreamIntel: FinalStreamIntel
+ ) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onCancel(finalStreamIntel: FinalStreamIntel) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onComplete(finalStreamIntel: FinalStreamIntel) {
+ }
+}
diff --git a/test/kotlin/apps/baseline/BUILD b/test/kotlin/apps/baseline/BUILD
new file mode 100644
index 0000000000..7ef9cb46c2
--- /dev/null
+++ b/test/kotlin/apps/baseline/BUILD
@@ -0,0 +1,45 @@
+load("@build_bazel_rules_android//android:rules.bzl", "android_binary")
+load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
+load("@rules_detekt//detekt:defs.bzl", "detekt")
+load("@rules_jvm_external//:defs.bzl", "artifact")
+
+licenses(["notice"]) # Apache 2
+
+android_binary(
+ name = "hello_envoy_kt",
+ custom_package = "io.envoyproxy.envoymobile.helloenvoykotlin",
+ manifest = "AndroidManifest.xml",
+ proguard_specs = ["//library:proguard_rules"],
+ deps = [
+ "hello_envoy_kt_lib",
+ ],
+)
+
+kt_android_library(
+ name = "hello_envoy_kt_lib",
+ srcs = [
+ "AsyncDemoFilter.kt",
+ "BufferDemoFilter.kt",
+ "DemoFilter.kt",
+ "MainActivity.kt",
+ ],
+ custom_package = "io.envoyproxy.envoymobile.helloenvoykotlin",
+ manifest = "AndroidManifest.xml",
+ resource_files = [
+ "res/layout/activity_main.xml",
+ ],
+ deps = [
+ "//dist:envoy_mobile_android",
+ "//examples/kotlin/shared:hello_envoy_shared_lib",
+ artifact("androidx.recyclerview:recyclerview"),
+ artifact("androidx.annotation:annotation"),
+ artifact("com.google.code.findbugs:jsr305"),
+ ],
+)
+
+detekt(
+ name = "hello_envoy_kt_lint",
+ srcs = glob(["*.kt"]),
+ build_upon_default_config = True,
+ cfgs = ["//:kotlin_lint_config"],
+)
diff --git a/test/kotlin/apps/baseline/BufferDemoFilter.kt b/test/kotlin/apps/baseline/BufferDemoFilter.kt
new file mode 100644
index 0000000000..f158d1b1e6
--- /dev/null
+++ b/test/kotlin/apps/baseline/BufferDemoFilter.kt
@@ -0,0 +1,73 @@
+package io.envoyproxy.envoymobile.helloenvoykotlin
+
+import io.envoyproxy.envoymobile.EnvoyError
+import io.envoyproxy.envoymobile.FilterDataStatus
+import io.envoyproxy.envoymobile.FilterHeadersStatus
+import io.envoyproxy.envoymobile.FilterTrailersStatus
+import io.envoyproxy.envoymobile.FinalStreamIntel
+import io.envoyproxy.envoymobile.ResponseFilter
+import io.envoyproxy.envoymobile.ResponseHeaders
+import io.envoyproxy.envoymobile.ResponseTrailers
+import io.envoyproxy.envoymobile.StreamIntel
+import java.nio.ByteBuffer
+
+/**
+ * Example of a more complex HTTP filter that pauses processing on the response filter chain,
+ * buffers until the response is complete, then resumes filter iteration while setting a new
+ * header.
+ */
+class BufferDemoFilter : ResponseFilter {
+ private lateinit var headers: ResponseHeaders
+ private lateinit var body: ByteBuffer
+
+ override fun onResponseHeaders(
+ headers: ResponseHeaders,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterHeadersStatus {
+ this.headers = headers
+ return FilterHeadersStatus.StopIteration()
+ }
+
+ override fun onResponseData(
+ body: ByteBuffer,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterDataStatus {
+ // Since we request buffering, each invocation will include all data buffered so far.
+ this.body = body
+
+ // If this is the end of the stream, resume processing of the (now fully-buffered) response.
+ if (endStream) {
+ val builder = headers.toResponseHeadersBuilder()
+ .add("buffer-filter-demo", "1")
+ return FilterDataStatus.ResumeIteration(builder.build(), body)
+ }
+ return FilterDataStatus.StopIterationAndBuffer()
+ }
+
+ override fun onResponseTrailers(
+ trailers: ResponseTrailers,
+ streamIntel: StreamIntel
+ ): FilterTrailersStatus {
+ // Trailers imply end of stream; resume processing of the (now fully-buffered) response.
+ val builder = headers.toResponseHeadersBuilder()
+ .add("buffer-filter-demo", "1")
+ return FilterTrailersStatus.ResumeIteration(builder.build(), this.body, trailers)
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onError(
+ error: EnvoyError,
+ finalStreamIntel: FinalStreamIntel
+ ) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onCancel(finalStreamIntel: FinalStreamIntel) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onComplete(finalStreamIntel: FinalStreamIntel) {
+ }
+}
diff --git a/test/kotlin/apps/baseline/DemoFilter.kt b/test/kotlin/apps/baseline/DemoFilter.kt
new file mode 100644
index 0000000000..2f231fd130
--- /dev/null
+++ b/test/kotlin/apps/baseline/DemoFilter.kt
@@ -0,0 +1,58 @@
+package io.envoyproxy.envoymobile.helloenvoykotlin
+
+import android.util.Log
+import io.envoyproxy.envoymobile.EnvoyError
+import io.envoyproxy.envoymobile.FilterDataStatus
+import io.envoyproxy.envoymobile.FilterHeadersStatus
+import io.envoyproxy.envoymobile.FilterTrailersStatus
+import io.envoyproxy.envoymobile.FinalStreamIntel
+import io.envoyproxy.envoymobile.ResponseFilter
+import io.envoyproxy.envoymobile.ResponseHeaders
+import io.envoyproxy.envoymobile.ResponseTrailers
+import io.envoyproxy.envoymobile.StreamIntel
+import java.nio.ByteBuffer
+
+class DemoFilter : ResponseFilter {
+ override fun onResponseHeaders(
+ headers: ResponseHeaders,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterHeadersStatus {
+ Log.d("DemoFilter", "On headers!")
+ val builder = headers.toResponseHeadersBuilder()
+ builder.add("filter-demo", "1")
+ return FilterHeadersStatus.Continue(builder.build())
+ }
+
+ override fun onResponseData(
+ body: ByteBuffer,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterDataStatus {
+ Log.d("DemoFilter", "On data!")
+ return FilterDataStatus.Continue(body)
+ }
+
+ override fun onResponseTrailers(
+ trailers: ResponseTrailers,
+ streamIntel: StreamIntel
+ ): FilterTrailersStatus {
+ Log.d("DemoFilter", "On trailers!")
+ return FilterTrailersStatus.Continue(trailers)
+ }
+
+ override fun onError(
+ error: EnvoyError,
+ finalStreamIntel: FinalStreamIntel
+ ) {
+ Log.d("DemoFilter", "On error!")
+ }
+
+ override fun onCancel(finalStreamIntel: FinalStreamIntel) {
+ Log.d("DemoFilter", "On cancel!")
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onComplete(finalStreamIntel: FinalStreamIntel) {
+ }
+}
diff --git a/test/kotlin/apps/baseline/MainActivity.kt b/test/kotlin/apps/baseline/MainActivity.kt
new file mode 100644
index 0000000000..e66aab3793
--- /dev/null
+++ b/test/kotlin/apps/baseline/MainActivity.kt
@@ -0,0 +1,164 @@
+package io.envoyproxy.envoymobile.helloenvoykotlin
+
+import android.app.Activity
+import android.os.Bundle
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Log
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.envoyproxy.envoymobile.AndroidEngineBuilder
+import io.envoyproxy.envoymobile.Element
+import io.envoyproxy.envoymobile.Engine
+import io.envoyproxy.envoymobile.LogLevel
+import io.envoyproxy.envoymobile.RequestHeadersBuilder
+import io.envoyproxy.envoymobile.RequestMethod
+import io.envoyproxy.envoymobile.UpstreamHttpProtocol
+import io.envoyproxy.envoymobile.shared.Failure
+import io.envoyproxy.envoymobile.shared.ResponseRecyclerViewAdapter
+import io.envoyproxy.envoymobile.shared.Success
+import java.io.IOException
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+private const val REQUEST_HANDLER_THREAD_NAME = "hello_envoy_kt"
+private const val REQUEST_AUTHORITY = "api.lyft.com"
+private const val REQUEST_PATH = "/ping"
+private const val REQUEST_SCHEME = "https"
+private val FILTERED_HEADERS = setOf(
+ "server",
+ "filter-demo",
+ "buffer-filter-demo",
+ "async-filter-demo",
+ "x-envoy-upstream-service-time"
+)
+
+class MainActivity : Activity() {
+ private val thread = HandlerThread(REQUEST_HANDLER_THREAD_NAME)
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var viewAdapter: ResponseRecyclerViewAdapter
+ private lateinit var engine: Engine
+
+ @Suppress("MaxLineLength")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ engine = AndroidEngineBuilder(application)
+ .addLogLevel(LogLevel.DEBUG)
+ .addPlatformFilter(::DemoFilter)
+ .addPlatformFilter(::BufferDemoFilter)
+ .addPlatformFilter(::AsyncDemoFilter)
+ .addNativeFilter("envoy.filters.http.buffer", "{\"@type\":\"type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer\",\"max_request_bytes\":5242880}")
+ .addStringAccessor("demo-accessor", { "PlatformString" })
+ .setOnEngineRunning { Log.d("MainActivity", "Envoy async internal setup completed") }
+ .setEventTracker({
+ for (entry in it.entries) {
+ Log.d("MainActivity", "Event emitted: ${entry.key}, ${entry.value}")
+ }
+ })
+ .setLogger {
+ Log.d("MainActivity", it)
+ }
+ .build()
+
+ recyclerView = findViewById(R.id.recycler_view) as RecyclerView
+ recyclerView.layoutManager = LinearLayoutManager(this)
+
+ viewAdapter = ResponseRecyclerViewAdapter()
+ recyclerView.adapter = viewAdapter
+ val dividerItemDecoration = DividerItemDecoration(
+ recyclerView.context, DividerItemDecoration.VERTICAL
+ )
+ recyclerView.addItemDecoration(dividerItemDecoration)
+ thread.start()
+ val handler = Handler(thread.looper)
+
+ // Run a request loop and record stats until the application exits.
+ handler.postDelayed(
+ object : Runnable {
+ override fun run() {
+ try {
+ makeRequest()
+ recordStats()
+ } catch (e: IOException) {
+ Log.d("MainActivity", "exception making request or recording stats", e)
+ }
+
+ // Make a call and report stats again
+ handler.postDelayed(this, TimeUnit.SECONDS.toMillis(1))
+ }
+ },
+ TimeUnit.SECONDS.toMillis(1)
+ )
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ thread.quit()
+ }
+
+ private fun makeRequest() {
+ // Note: this request will use an h2 stream for the upstream request.
+ // The Java example uses http/1.1. This is done on purpose to test both paths in end-to-end
+ // tests in CI.
+ val requestHeaders = RequestHeadersBuilder(
+ RequestMethod.GET, REQUEST_SCHEME, REQUEST_AUTHORITY, REQUEST_PATH
+ )
+ .addUpstreamHttpProtocol(UpstreamHttpProtocol.HTTP2)
+ .build()
+ engine
+ .streamClient()
+ .newStreamPrototype()
+ .setOnResponseHeaders { responseHeaders, _, _ ->
+ val status = responseHeaders.httpStatus ?: 0L
+ val message = "received headers with status $status"
+
+ val sb = StringBuilder()
+ for ((name, value) in responseHeaders.headers) {
+ if (name in FILTERED_HEADERS) {
+ sb.append(name).append(": ").append(value.joinToString()).append("\n")
+ }
+ }
+ val headerText = sb.toString()
+
+ Log.d("MainActivity", message)
+ responseHeaders.value("filter-demo")?.first()?.let { filterDemoValue ->
+ Log.d("MainActivity", "filter-demo: $filterDemoValue")
+ }
+
+ if (status == 200) {
+ recyclerView.post { viewAdapter.add(Success(message, headerText)) }
+ } else {
+ recyclerView.post { viewAdapter.add(Failure(message)) }
+ }
+ }
+ .setOnError { error, _ ->
+ val attemptCount = error.attemptCount ?: -1
+ val message = "failed with error after $attemptCount attempts: ${error.message}"
+ Log.d("MainActivity", message)
+ recyclerView.post { viewAdapter.add(Failure(message)) }
+ }
+ .start(Executors.newSingleThreadExecutor())
+ .sendHeaders(requestHeaders, true)
+ }
+
+ private fun recordStats() {
+ val counter = engine.pulseClient().counter(Element("foo"), Element("bar"), Element("counter"))
+ val gauge = engine.pulseClient().gauge(Element("foo"), Element("bar"), Element("gauge"))
+ val timer = engine.pulseClient().timer(Element("foo"), Element("bar"), Element("timer"))
+ val distribution =
+ engine.pulseClient().distribution(Element("foo"), Element("bar"), Element("distribution"))
+
+ counter.increment()
+ counter.increment(5)
+
+ gauge.set(5)
+ gauge.add(10)
+ gauge.sub(1)
+
+ timer.recordDuration(15)
+ distribution.recordValue(15)
+ }
+}
diff --git a/test/kotlin/apps/baseline/README.md b/test/kotlin/apps/baseline/README.md
new file mode 100644
index 0000000000..53e8270fb7
--- /dev/null
+++ b/test/kotlin/apps/baseline/README.md
@@ -0,0 +1 @@
+For instructions on how to use this demo, please head over to our [docs](https://envoy-mobile.github.io/docs/envoy-mobile/latest/start/examples/hello_world.html).
diff --git a/test/kotlin/apps/baseline/res/layout/activity_main.xml b/test/kotlin/apps/baseline/res/layout/activity_main.xml
new file mode 100644
index 0000000000..0d8c4ff3fd
--- /dev/null
+++ b/test/kotlin/apps/baseline/res/layout/activity_main.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/test/kotlin/apps/baseline/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml b/test/kotlin/apps/baseline/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml
new file mode 100644
index 0000000000..ab975a532c
--- /dev/null
+++ b/test/kotlin/apps/baseline/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml
@@ -0,0 +1,38 @@
+
+
+ --fat_apk_cpu=x86
+ //examples/kotlin/hello_world:hello_envoy_kt
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/kotlin/apps/experimental/.bazelproject b/test/kotlin/apps/experimental/.bazelproject
new file mode 100644
index 0000000000..651096b4a0
--- /dev/null
+++ b/test/kotlin/apps/experimental/.bazelproject
@@ -0,0 +1,22 @@
+workspace_type: android
+
+directories:
+ -bazel-bin
+ -bazel-instant-android
+ -bazel-out
+ -bazel-testlogs
+ -buck-out
+ -build
+ examples/kotlin/hello_world
+
+import_run_configurations:
+ examples/kotlin/hello_world/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml
+
+targets:
+ //examples/kotlin/hello_world:hello_envoy_kt
+
+additional_languages:
+ kotlin
+ java
+ android
+ c
diff --git a/test/kotlin/apps/experimental/AndroidManifest.xml b/test/kotlin/apps/experimental/AndroidManifest.xml
new file mode 100644
index 0000000000..89427cc582
--- /dev/null
+++ b/test/kotlin/apps/experimental/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/kotlin/apps/experimental/AsyncDemoFilter.kt b/test/kotlin/apps/experimental/AsyncDemoFilter.kt
new file mode 100644
index 0000000000..a45ec5f7af
--- /dev/null
+++ b/test/kotlin/apps/experimental/AsyncDemoFilter.kt
@@ -0,0 +1,95 @@
+package io.envoyproxy.envoymobile.helloenvoykotlin
+
+import io.envoyproxy.envoymobile.AsyncResponseFilter
+import io.envoyproxy.envoymobile.EnvoyError
+import io.envoyproxy.envoymobile.FilterDataStatus
+import io.envoyproxy.envoymobile.FilterHeadersStatus
+import io.envoyproxy.envoymobile.FilterResumeStatus
+import io.envoyproxy.envoymobile.FilterTrailersStatus
+import io.envoyproxy.envoymobile.FinalStreamIntel
+import io.envoyproxy.envoymobile.ResponseFilterCallbacks
+import io.envoyproxy.envoymobile.ResponseHeaders
+import io.envoyproxy.envoymobile.ResponseTrailers
+import io.envoyproxy.envoymobile.StreamIntel
+import java.nio.ByteBuffer
+import java.util.Timer
+import kotlin.concurrent.schedule
+
+/**
+ * Example of a more complex HTTP filter that pauses processing on the response filter chain,
+ * buffers until the response is complete, then asynchronously triggers filter chain resumption
+ * while setting a new header. Also demonstrates safety of re-entrancy in async callbacks.
+ */
+class AsyncDemoFilter : AsyncResponseFilter {
+ private lateinit var callbacks: ResponseFilterCallbacks
+
+ override fun onResponseHeaders(
+ headers: ResponseHeaders,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterHeadersStatus {
+ // If this is the end of the stream, asynchronously resume response processing via callback.
+ if (endStream) {
+ Timer("AsyncResume", false).schedule(100) {
+ callbacks.resumeResponse()
+ }
+ }
+ return FilterHeadersStatus.StopIteration()
+ }
+
+ override fun onResponseData(
+ body: ByteBuffer,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterDataStatus {
+ // If this is the end of the stream, asynchronously resume response processing via callback.
+ if (endStream) {
+ Timer("AsyncResume", false).schedule(100) {
+ callbacks.resumeResponse()
+ }
+ }
+ return FilterDataStatus.StopIterationAndBuffer()
+ }
+
+ override fun onResponseTrailers(
+ trailers: ResponseTrailers,
+ streamIntel: StreamIntel
+ ): FilterTrailersStatus {
+ // Trailers imply end of stream, so asynchronously resume response processing via callbacka
+ Timer("AsyncResume", false).schedule(100) {
+ callbacks.resumeResponse()
+ }
+ return FilterTrailersStatus.StopIteration()
+ }
+
+ override fun setResponseFilterCallbacks(callbacks: ResponseFilterCallbacks) {
+ this.callbacks = callbacks
+ }
+
+ override fun onResumeResponse(
+ headers: ResponseHeaders?,
+ data: ByteBuffer?,
+ trailers: ResponseTrailers?,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterResumeStatus {
+ val builder = headers!!.toResponseHeadersBuilder()
+ .add("async-filter-demo", "1")
+ return FilterResumeStatus.ResumeIteration(builder.build(), data, trailers)
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onError(
+ error: EnvoyError,
+ finalStreamIntel: FinalStreamIntel
+ ) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onCancel(finalStreamIntel: FinalStreamIntel) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onComplete(finalStreamIntel: FinalStreamIntel) {
+ }
+}
diff --git a/test/kotlin/apps/experimental/BUILD b/test/kotlin/apps/experimental/BUILD
new file mode 100644
index 0000000000..7ef9cb46c2
--- /dev/null
+++ b/test/kotlin/apps/experimental/BUILD
@@ -0,0 +1,45 @@
+load("@build_bazel_rules_android//android:rules.bzl", "android_binary")
+load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
+load("@rules_detekt//detekt:defs.bzl", "detekt")
+load("@rules_jvm_external//:defs.bzl", "artifact")
+
+licenses(["notice"]) # Apache 2
+
+android_binary(
+ name = "hello_envoy_kt",
+ custom_package = "io.envoyproxy.envoymobile.helloenvoykotlin",
+ manifest = "AndroidManifest.xml",
+ proguard_specs = ["//library:proguard_rules"],
+ deps = [
+ "hello_envoy_kt_lib",
+ ],
+)
+
+kt_android_library(
+ name = "hello_envoy_kt_lib",
+ srcs = [
+ "AsyncDemoFilter.kt",
+ "BufferDemoFilter.kt",
+ "DemoFilter.kt",
+ "MainActivity.kt",
+ ],
+ custom_package = "io.envoyproxy.envoymobile.helloenvoykotlin",
+ manifest = "AndroidManifest.xml",
+ resource_files = [
+ "res/layout/activity_main.xml",
+ ],
+ deps = [
+ "//dist:envoy_mobile_android",
+ "//examples/kotlin/shared:hello_envoy_shared_lib",
+ artifact("androidx.recyclerview:recyclerview"),
+ artifact("androidx.annotation:annotation"),
+ artifact("com.google.code.findbugs:jsr305"),
+ ],
+)
+
+detekt(
+ name = "hello_envoy_kt_lint",
+ srcs = glob(["*.kt"]),
+ build_upon_default_config = True,
+ cfgs = ["//:kotlin_lint_config"],
+)
diff --git a/test/kotlin/apps/experimental/BufferDemoFilter.kt b/test/kotlin/apps/experimental/BufferDemoFilter.kt
new file mode 100644
index 0000000000..f158d1b1e6
--- /dev/null
+++ b/test/kotlin/apps/experimental/BufferDemoFilter.kt
@@ -0,0 +1,73 @@
+package io.envoyproxy.envoymobile.helloenvoykotlin
+
+import io.envoyproxy.envoymobile.EnvoyError
+import io.envoyproxy.envoymobile.FilterDataStatus
+import io.envoyproxy.envoymobile.FilterHeadersStatus
+import io.envoyproxy.envoymobile.FilterTrailersStatus
+import io.envoyproxy.envoymobile.FinalStreamIntel
+import io.envoyproxy.envoymobile.ResponseFilter
+import io.envoyproxy.envoymobile.ResponseHeaders
+import io.envoyproxy.envoymobile.ResponseTrailers
+import io.envoyproxy.envoymobile.StreamIntel
+import java.nio.ByteBuffer
+
+/**
+ * Example of a more complex HTTP filter that pauses processing on the response filter chain,
+ * buffers until the response is complete, then resumes filter iteration while setting a new
+ * header.
+ */
+class BufferDemoFilter : ResponseFilter {
+ private lateinit var headers: ResponseHeaders
+ private lateinit var body: ByteBuffer
+
+ override fun onResponseHeaders(
+ headers: ResponseHeaders,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterHeadersStatus {
+ this.headers = headers
+ return FilterHeadersStatus.StopIteration()
+ }
+
+ override fun onResponseData(
+ body: ByteBuffer,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterDataStatus {
+ // Since we request buffering, each invocation will include all data buffered so far.
+ this.body = body
+
+ // If this is the end of the stream, resume processing of the (now fully-buffered) response.
+ if (endStream) {
+ val builder = headers.toResponseHeadersBuilder()
+ .add("buffer-filter-demo", "1")
+ return FilterDataStatus.ResumeIteration(builder.build(), body)
+ }
+ return FilterDataStatus.StopIterationAndBuffer()
+ }
+
+ override fun onResponseTrailers(
+ trailers: ResponseTrailers,
+ streamIntel: StreamIntel
+ ): FilterTrailersStatus {
+ // Trailers imply end of stream; resume processing of the (now fully-buffered) response.
+ val builder = headers.toResponseHeadersBuilder()
+ .add("buffer-filter-demo", "1")
+ return FilterTrailersStatus.ResumeIteration(builder.build(), this.body, trailers)
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onError(
+ error: EnvoyError,
+ finalStreamIntel: FinalStreamIntel
+ ) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onCancel(finalStreamIntel: FinalStreamIntel) {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onComplete(finalStreamIntel: FinalStreamIntel) {
+ }
+}
diff --git a/test/kotlin/apps/experimental/DemoFilter.kt b/test/kotlin/apps/experimental/DemoFilter.kt
new file mode 100644
index 0000000000..2f231fd130
--- /dev/null
+++ b/test/kotlin/apps/experimental/DemoFilter.kt
@@ -0,0 +1,58 @@
+package io.envoyproxy.envoymobile.helloenvoykotlin
+
+import android.util.Log
+import io.envoyproxy.envoymobile.EnvoyError
+import io.envoyproxy.envoymobile.FilterDataStatus
+import io.envoyproxy.envoymobile.FilterHeadersStatus
+import io.envoyproxy.envoymobile.FilterTrailersStatus
+import io.envoyproxy.envoymobile.FinalStreamIntel
+import io.envoyproxy.envoymobile.ResponseFilter
+import io.envoyproxy.envoymobile.ResponseHeaders
+import io.envoyproxy.envoymobile.ResponseTrailers
+import io.envoyproxy.envoymobile.StreamIntel
+import java.nio.ByteBuffer
+
+class DemoFilter : ResponseFilter {
+ override fun onResponseHeaders(
+ headers: ResponseHeaders,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterHeadersStatus {
+ Log.d("DemoFilter", "On headers!")
+ val builder = headers.toResponseHeadersBuilder()
+ builder.add("filter-demo", "1")
+ return FilterHeadersStatus.Continue(builder.build())
+ }
+
+ override fun onResponseData(
+ body: ByteBuffer,
+ endStream: Boolean,
+ streamIntel: StreamIntel
+ ): FilterDataStatus {
+ Log.d("DemoFilter", "On data!")
+ return FilterDataStatus.Continue(body)
+ }
+
+ override fun onResponseTrailers(
+ trailers: ResponseTrailers,
+ streamIntel: StreamIntel
+ ): FilterTrailersStatus {
+ Log.d("DemoFilter", "On trailers!")
+ return FilterTrailersStatus.Continue(trailers)
+ }
+
+ override fun onError(
+ error: EnvoyError,
+ finalStreamIntel: FinalStreamIntel
+ ) {
+ Log.d("DemoFilter", "On error!")
+ }
+
+ override fun onCancel(finalStreamIntel: FinalStreamIntel) {
+ Log.d("DemoFilter", "On cancel!")
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onComplete(finalStreamIntel: FinalStreamIntel) {
+ }
+}
diff --git a/test/kotlin/apps/experimental/MainActivity.kt b/test/kotlin/apps/experimental/MainActivity.kt
new file mode 100644
index 0000000000..e99b7295ca
--- /dev/null
+++ b/test/kotlin/apps/experimental/MainActivity.kt
@@ -0,0 +1,166 @@
+package io.envoyproxy.envoymobile.helloenvoykotlin
+
+import android.app.Activity
+import android.os.Bundle
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Log
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.envoyproxy.envoymobile.AndroidEngineBuilder
+import io.envoyproxy.envoymobile.Element
+import io.envoyproxy.envoymobile.Engine
+import io.envoyproxy.envoymobile.LogLevel
+import io.envoyproxy.envoymobile.RequestHeadersBuilder
+import io.envoyproxy.envoymobile.RequestMethod
+import io.envoyproxy.envoymobile.UpstreamHttpProtocol
+import io.envoyproxy.envoymobile.shared.Failure
+import io.envoyproxy.envoymobile.shared.ResponseRecyclerViewAdapter
+import io.envoyproxy.envoymobile.shared.Success
+import java.io.IOException
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+private const val REQUEST_HANDLER_THREAD_NAME = "hello_envoy_kt"
+private const val REQUEST_AUTHORITY = "api.lyft.com"
+private const val REQUEST_PATH = "/ping"
+private const val REQUEST_SCHEME = "https"
+private val FILTERED_HEADERS = setOf(
+ "server",
+ "filter-demo",
+ "buffer-filter-demo",
+ "async-filter-demo",
+ "x-envoy-upstream-service-time"
+)
+
+class MainActivity : Activity() {
+ private val thread = HandlerThread(REQUEST_HANDLER_THREAD_NAME)
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var viewAdapter: ResponseRecyclerViewAdapter
+ private lateinit var engine: Engine
+
+ @Suppress("MaxLineLength")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ engine = AndroidEngineBuilder(application)
+ .addLogLevel(LogLevel.DEBUG)
+ .addPlatformFilter(::DemoFilter)
+ .addPlatformFilter(::BufferDemoFilter)
+ .addPlatformFilter(::AsyncDemoFilter)
+ .enableHappyEyeballs(true)
+ .enableInterfaceBinding(true)
+ .addNativeFilter("envoy.filters.http.buffer", "{\"@type\":\"type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer\",\"max_request_bytes\":5242880}")
+ .addStringAccessor("demo-accessor", { "PlatformString" })
+ .setOnEngineRunning { Log.d("MainActivity", "Envoy async internal setup completed") }
+ .setEventTracker({
+ for (entry in it.entries) {
+ Log.d("MainActivity", "Event emitted: ${entry.key}, ${entry.value}")
+ }
+ })
+ .setLogger {
+ Log.d("MainActivity", it)
+ }
+ .build()
+
+ recyclerView = findViewById(R.id.recycler_view) as RecyclerView
+ recyclerView.layoutManager = LinearLayoutManager(this)
+
+ viewAdapter = ResponseRecyclerViewAdapter()
+ recyclerView.adapter = viewAdapter
+ val dividerItemDecoration = DividerItemDecoration(
+ recyclerView.context, DividerItemDecoration.VERTICAL
+ )
+ recyclerView.addItemDecoration(dividerItemDecoration)
+ thread.start()
+ val handler = Handler(thread.looper)
+
+ // Run a request loop and record stats until the application exits.
+ handler.postDelayed(
+ object : Runnable {
+ override fun run() {
+ try {
+ makeRequest()
+ recordStats()
+ } catch (e: IOException) {
+ Log.d("MainActivity", "exception making request or recording stats", e)
+ }
+
+ // Make a call and report stats again
+ handler.postDelayed(this, TimeUnit.SECONDS.toMillis(1))
+ }
+ },
+ TimeUnit.SECONDS.toMillis(1)
+ )
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ thread.quit()
+ }
+
+ private fun makeRequest() {
+ // Note: this request will use an h2 stream for the upstream request.
+ // The Java example uses http/1.1. This is done on purpose to test both paths in end-to-end
+ // tests in CI.
+ val requestHeaders = RequestHeadersBuilder(
+ RequestMethod.GET, REQUEST_SCHEME, REQUEST_AUTHORITY, REQUEST_PATH
+ )
+ .addUpstreamHttpProtocol(UpstreamHttpProtocol.HTTP2)
+ .build()
+ engine
+ .streamClient()
+ .newStreamPrototype()
+ .setOnResponseHeaders { responseHeaders, _, _ ->
+ val status = responseHeaders.httpStatus ?: 0L
+ val message = "received headers with status $status"
+
+ val sb = StringBuilder()
+ for ((name, value) in responseHeaders.headers) {
+ if (name in FILTERED_HEADERS) {
+ sb.append(name).append(": ").append(value.joinToString()).append("\n")
+ }
+ }
+ val headerText = sb.toString()
+
+ Log.d("MainActivity", message)
+ responseHeaders.value("filter-demo")?.first()?.let { filterDemoValue ->
+ Log.d("MainActivity", "filter-demo: $filterDemoValue")
+ }
+
+ if (status == 200) {
+ recyclerView.post { viewAdapter.add(Success(message, headerText)) }
+ } else {
+ recyclerView.post { viewAdapter.add(Failure(message)) }
+ }
+ }
+ .setOnError { error, _ ->
+ val attemptCount = error.attemptCount ?: -1
+ val message = "failed with error after $attemptCount attempts: ${error.message}"
+ Log.d("MainActivity", message)
+ recyclerView.post { viewAdapter.add(Failure(message)) }
+ }
+ .start(Executors.newSingleThreadExecutor())
+ .sendHeaders(requestHeaders, true)
+ }
+
+ private fun recordStats() {
+ val counter = engine.pulseClient().counter(Element("foo"), Element("bar"), Element("counter"))
+ val gauge = engine.pulseClient().gauge(Element("foo"), Element("bar"), Element("gauge"))
+ val timer = engine.pulseClient().timer(Element("foo"), Element("bar"), Element("timer"))
+ val distribution =
+ engine.pulseClient().distribution(Element("foo"), Element("bar"), Element("distribution"))
+
+ counter.increment()
+ counter.increment(5)
+
+ gauge.set(5)
+ gauge.add(10)
+ gauge.sub(1)
+
+ timer.recordDuration(15)
+ distribution.recordValue(15)
+ }
+}
diff --git a/test/kotlin/apps/experimental/README.md b/test/kotlin/apps/experimental/README.md
new file mode 100644
index 0000000000..53e8270fb7
--- /dev/null
+++ b/test/kotlin/apps/experimental/README.md
@@ -0,0 +1 @@
+For instructions on how to use this demo, please head over to our [docs](https://envoy-mobile.github.io/docs/envoy-mobile/latest/start/examples/hello_world.html).
diff --git a/test/kotlin/apps/experimental/res/layout/activity_main.xml b/test/kotlin/apps/experimental/res/layout/activity_main.xml
new file mode 100644
index 0000000000..0d8c4ff3fd
--- /dev/null
+++ b/test/kotlin/apps/experimental/res/layout/activity_main.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/test/kotlin/apps/experimental/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml b/test/kotlin/apps/experimental/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml
new file mode 100644
index 0000000000..ab975a532c
--- /dev/null
+++ b/test/kotlin/apps/experimental/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml
@@ -0,0 +1,38 @@
+
+
+ --fat_apk_cpu=x86
+ //examples/kotlin/hello_world:hello_envoy_kt
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/swift/apps/baseline/AppDelegate.swift b/test/swift/apps/baseline/AppDelegate.swift
new file mode 100644
index 0000000000..2f161c9b3e
--- /dev/null
+++ b/test/swift/apps/baseline/AppDelegate.swift
@@ -0,0 +1,19 @@
+import UIKit
+
+@UIApplicationMain
+final class AppDelegate: UIResponder, UIApplicationDelegate {
+ var window: UIWindow?
+
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
+ {
+ let window = UIWindow(frame: UIScreen.main.bounds)
+ window.rootViewController = ViewController()
+ window.makeKeyAndVisible()
+ self.window = window
+
+ NSLog("Finished launching!")
+ return true
+ }
+}
diff --git a/test/swift/apps/baseline/AsyncDemoFilter.swift b/test/swift/apps/baseline/AsyncDemoFilter.swift
new file mode 100644
index 0000000000..c25e10ef43
--- /dev/null
+++ b/test/swift/apps/baseline/AsyncDemoFilter.swift
@@ -0,0 +1,67 @@
+import Envoy
+import Foundation
+
+/// Example of a more complex HTTP filter that pauses processing on the response filter chain,
+/// buffers until the response is complete, then asynchronously triggers filter chain resumption
+/// while setting a new header. Also demonstrates safety of re-entrancy of async callbacks.
+final class AsyncDemoFilter: AsyncResponseFilter {
+ private var callbacks: ResponseFilterCallbacks!
+
+ func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterHeadersStatus
+ {
+ if endStream {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
+ self?.callbacks.resumeResponse()
+ }
+ }
+ return .stopIteration
+ }
+
+ func onResponseData(_ body: Data, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterDataStatus
+ {
+ if endStream {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
+ self?.callbacks.resumeResponse()
+ }
+ }
+ return .stopIterationAndBuffer
+ }
+
+ func onResponseTrailers(
+ _ trailers: ResponseTrailers,
+ streamIntel: StreamIntel
+ ) -> FilterTrailersStatus {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
+ self?.callbacks.resumeResponse()
+ }
+ return .stopIteration
+ }
+
+ func setResponseFilterCallbacks(_ callbacks: ResponseFilterCallbacks) {
+ self.callbacks = callbacks
+ }
+
+ func onResumeResponse(
+ headers: ResponseHeaders?,
+ data: Data?,
+ trailers: ResponseTrailers?,
+ endStream: Bool,
+ streamIntel: StreamIntel
+ ) -> FilterResumeStatus {
+ guard let headers = headers else {
+ // Iteration was stopped on headers, so headers must be present.
+ fatalError("Filter behavior violation!")
+ }
+ let builder = headers.toResponseHeadersBuilder()
+ .add(name: "async-filter-demo", value: "1")
+ return .resumeIteration(headers: builder.build(), data: data, trailers: trailers)
+ }
+
+ func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {}
+
+ func onCancel(streamIntel: FinalStreamIntel) {}
+
+ func onComplete(streamIntel: FinalStreamIntel) {}
+}
diff --git a/test/swift/apps/baseline/BUILD b/test/swift/apps/baseline/BUILD
new file mode 100644
index 0000000000..c4f17ed7e4
--- /dev/null
+++ b/test/swift/apps/baseline/BUILD
@@ -0,0 +1,19 @@
+load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application")
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+licenses(["notice"]) # Apache 2
+
+swift_library(
+ name = "appmain",
+ srcs = glob(["*.swift"]),
+ deps = ["//dist:envoy_mobile_ios"],
+)
+
+ios_application(
+ name = "app",
+ bundle_id = "io.envoyproxy.envoymobile.helloworld",
+ families = ["iphone"],
+ infoplists = ["Info.plist"],
+ minimum_os_version = "11.0",
+ deps = ["appmain"],
+)
diff --git a/test/swift/apps/baseline/Base.lproj/LaunchScreen.storyboard b/test/swift/apps/baseline/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000000..bfa3612941
--- /dev/null
+++ b/test/swift/apps/baseline/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/swift/apps/baseline/BufferDemoFilter.swift b/test/swift/apps/baseline/BufferDemoFilter.swift
new file mode 100644
index 0000000000..1def3fb677
--- /dev/null
+++ b/test/swift/apps/baseline/BufferDemoFilter.swift
@@ -0,0 +1,48 @@
+import Envoy
+import Foundation
+
+/// Example of a more complex HTTP filter that pauses processing on the response filter chain,
+/// buffers until the response is complete, then resumes filter iteration while setting a new
+/// header.
+final class BufferDemoFilter: ResponseFilter {
+ private var headers: ResponseHeaders!
+ private var body: Data?
+
+ func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterHeadersStatus
+ {
+ self.headers = headers
+ return .stopIteration
+ }
+
+ func onResponseData(_ data: Data, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterDataStatus
+ {
+ // Since we request buffering, each invocation will include all data buffered so far.
+ self.body = data
+
+ // If this is the end of the stream, resume processing of the (now fully-buffered) response.
+ if endStream {
+ let builder = self.headers.toResponseHeadersBuilder()
+ .add(name: "buffer-filter-demo", value: "1")
+ return .resumeIteration(headers: builder.build(), data: data)
+ }
+ return .stopIterationAndBuffer
+ }
+
+ func onResponseTrailers(
+ _ trailers: ResponseTrailers,
+ streamIntel: StreamIntel
+ ) -> FilterTrailersStatus {
+ // Trailers imply end of stream; resume processing of the (now fully-buffered) response.
+ let builder = self.headers.toResponseHeadersBuilder()
+ .add(name: "buffer-filter-demo", value: "1")
+ return .resumeIteration(headers: builder.build(), data: self.body, trailers: trailers)
+ }
+
+ func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {}
+
+ func onCancel(streamIntel: FinalStreamIntel) {}
+
+ func onComplete(streamIntel: FinalStreamIntel) {}
+}
diff --git a/test/swift/apps/baseline/DemoFilter.swift b/test/swift/apps/baseline/DemoFilter.swift
new file mode 100644
index 0000000000..27e79c3e6b
--- /dev/null
+++ b/test/swift/apps/baseline/DemoFilter.swift
@@ -0,0 +1,33 @@
+import Envoy
+import Foundation
+
+/// Example of a simple HTTP filter that adds a response header.
+struct DemoFilter: ResponseFilter {
+ func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterHeadersStatus
+ {
+ let builder = headers.toResponseHeadersBuilder()
+ builder.add(name: "filter-demo", value: "1")
+ return .continue(headers: builder.build())
+ }
+
+ func setResponseFilterCallbacks(_ callbacks: ResponseFilterCallbacks, streamIntel: StreamIntel) {}
+
+ func onResponseData(_ body: Data, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterDataStatus
+ {
+ return .continue(data: body)
+ }
+
+ func onResponseTrailers(_ trailers: ResponseTrailers, streamIntel: StreamIntel)
+ -> FilterTrailersStatus
+ {
+ return .continue(trailers: trailers)
+ }
+
+ func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {}
+
+ func onCancel(streamIntel: FinalStreamIntel) {}
+
+ func onComplete(streamIntel: FinalStreamIntel) {}
+}
diff --git a/test/swift/apps/baseline/Info.plist b/test/swift/apps/baseline/Info.plist
new file mode 100644
index 0000000000..83af5464ce
--- /dev/null
+++ b/test/swift/apps/baseline/Info.plist
@@ -0,0 +1,47 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 0.1
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 0.1
+ ITSAppUsesNonExemptEncryption
+
+ NSAppTransportSecurity
+
+ NSExceptionDomains
+
+ localhost
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UIStatusBarStyle
+ UIStatusBarStyleLightContent
+ UILaunchStoryboardName
+ LaunchScreen
+
+
diff --git a/test/swift/apps/baseline/README.md b/test/swift/apps/baseline/README.md
new file mode 100644
index 0000000000..53e8270fb7
--- /dev/null
+++ b/test/swift/apps/baseline/README.md
@@ -0,0 +1 @@
+For instructions on how to use this demo, please head over to our [docs](https://envoy-mobile.github.io/docs/envoy-mobile/latest/start/examples/hello_world.html).
diff --git a/test/swift/apps/baseline/ResponseModels.swift b/test/swift/apps/baseline/ResponseModels.swift
new file mode 100644
index 0000000000..c83e555320
--- /dev/null
+++ b/test/swift/apps/baseline/ResponseModels.swift
@@ -0,0 +1,10 @@
+/// Represents a response from the server.
+struct Response {
+ let message: String
+ let headerMessage: String
+}
+
+/// Error that was encountered when executing a request.
+struct RequestError: Error {
+ let message: String
+}
diff --git a/test/swift/apps/baseline/ViewController.swift b/test/swift/apps/baseline/ViewController.swift
new file mode 100644
index 0000000000..8676c01002
--- /dev/null
+++ b/test/swift/apps/baseline/ViewController.swift
@@ -0,0 +1,168 @@
+import Envoy
+import UIKit
+
+private let kCellID = "cell-id"
+private let kRequestAuthority = "api.lyft.com"
+private let kRequestPath = "/ping"
+private let kRequestScheme = "https"
+private let kFilteredHeaders =
+ ["server", "filter-demo", "async-filter-demo", "x-envoy-upstream-service-time"]
+
+final class ViewController: UITableViewController {
+ private var results = [Result]()
+ private var timer: Foundation.Timer?
+ private var streamClient: StreamClient?
+ private var pulseClient: PulseClient?
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ let engine = EngineBuilder()
+ .addLogLevel(.debug)
+ .addPlatformFilter(DemoFilter.init)
+ .addPlatformFilter(BufferDemoFilter.init)
+ .addPlatformFilter(AsyncDemoFilter.init)
+ // swiftlint:disable:next line_length
+ .addNativeFilter(name: "envoy.filters.http.buffer", typedConfig: "{\"@type\":\"type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer\",\"max_request_bytes\":5242880}")
+ .setOnEngineRunning { NSLog("Envoy async internal setup completed") }
+ .addStringAccessor(name: "demo-accessor", accessor: { return "PlatformString" })
+ .setEventTracker { NSLog("Envoy event emitted: \($0)") }
+ .build()
+ self.streamClient = engine.streamClient()
+ self.pulseClient = engine.pulseClient()
+
+ NSLog("started Envoy, beginning requests...")
+ self.startRequests()
+ }
+
+ deinit {
+ self.timer?.invalidate()
+ }
+
+ // MARK: - Requests
+
+ private func startRequests() {
+ self.timer = .scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
+ self?.performRequest()
+ self?.recordStats()
+ }
+ }
+
+ private func performRequest() {
+ guard let streamClient = self.streamClient else {
+ NSLog("failed to start request - Envoy is not running")
+ return
+ }
+
+ NSLog("starting request to '\(kRequestPath)'")
+
+ // Note: this request will use an h2 stream for the upstream request.
+ // The Objective-C example uses http/1.1. This is done on purpose to test both paths in
+ // end-to-end tests in CI.
+ let headers = RequestHeadersBuilder(method: .get, scheme: kRequestScheme,
+ authority: kRequestAuthority, path: kRequestPath)
+ .addUpstreamHttpProtocol(.http2)
+ .build()
+
+ streamClient
+ .newStreamPrototype()
+ .setOnResponseHeaders { [weak self] headers, _, _ in
+ let statusCode = headers.httpStatus.map(String.init) ?? "nil"
+ let message = "received headers with status \(statusCode)"
+
+ let headerMessage = headers.allHeaders()
+ .filter { kFilteredHeaders.contains($0.key) }
+ .map { "\($0.key): \($0.value.joined(separator: ", "))" }
+ .joined(separator: "\n")
+
+ NSLog(message)
+ if let filterDemoValue = headers.value(forName: "filter-demo")?.first {
+ NSLog("filter-demo: \(filterDemoValue)")
+ }
+ if let asyncFilterDemoValue = headers.value(forName: "async-filter-demo")?.first {
+ NSLog("async-filter-demo: \(asyncFilterDemoValue)")
+ }
+
+ let response = Response(message: message,
+ headerMessage: headerMessage)
+ self?.add(result: .success(response))
+ }
+ .setOnError { [weak self] error, _ in
+ let message: String
+ if let attemptCount = error.attemptCount {
+ message = "failed within Envoy library after \(attemptCount) attempts: \(error.message)"
+ } else {
+ message = "failed within Envoy library: \(error.message)"
+ }
+
+ NSLog(message)
+ self?.add(result: .failure(RequestError(message: message)))
+ }
+ .start()
+ .sendHeaders(headers, endStream: true)
+ }
+
+ private func add(result: Result) {
+ self.results.insert(result, at: 0)
+ self.tableView.reloadData()
+ }
+
+ private func recordStats() {
+ guard let pulseClient = self.pulseClient else {
+ NSLog("failed to send stats - Envoy is not running")
+ return
+ }
+
+ let counter = pulseClient.counter(elements: ["foo", "bar", "counter"])
+ counter.increment()
+ counter.increment(count: 5)
+
+ let gauge = pulseClient.gauge(elements: ["foo", "bar", "gauge"])
+ gauge.set(value: 5)
+ gauge.add(amount: 10)
+ gauge.sub(amount: 1)
+
+ let timer = pulseClient.timer(elements: ["foo", "bar", "timer"])
+ let distribution = pulseClient.distribution(elements: ["foo", "bar", "distribution"])
+ timer.recordDuration(durationMs: 15)
+ distribution.recordValue(value: 15)
+ }
+ // MARK: - UITableView
+
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return 1
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return self.results.count
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
+ -> UITableViewCell
+ {
+ let cell = tableView.dequeueReusableCell(withIdentifier: kCellID) ??
+ UITableViewCell(style: .subtitle, reuseIdentifier: kCellID)
+
+ let result = self.results[indexPath.row]
+ switch result {
+ case .success(let response):
+ cell.textLabel?.text = response.message
+ cell.detailTextLabel?.text = response.headerMessage
+
+ cell.textLabel?.textColor = .black
+ cell.detailTextLabel?.lineBreakMode = .byWordWrapping
+ cell.detailTextLabel?.numberOfLines = 0
+ cell.detailTextLabel?.textColor = .black
+ cell.contentView.backgroundColor = .white
+ case .failure(let error):
+ cell.textLabel?.text = error.message
+ cell.detailTextLabel?.text = nil
+
+ cell.textLabel?.textColor = .white
+ cell.detailTextLabel?.textColor = .white
+ cell.contentView.backgroundColor = .red
+ }
+
+ return cell
+ }
+}
diff --git a/test/swift/apps/experimental/AppDelegate.swift b/test/swift/apps/experimental/AppDelegate.swift
new file mode 100644
index 0000000000..2f161c9b3e
--- /dev/null
+++ b/test/swift/apps/experimental/AppDelegate.swift
@@ -0,0 +1,19 @@
+import UIKit
+
+@UIApplicationMain
+final class AppDelegate: UIResponder, UIApplicationDelegate {
+ var window: UIWindow?
+
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
+ {
+ let window = UIWindow(frame: UIScreen.main.bounds)
+ window.rootViewController = ViewController()
+ window.makeKeyAndVisible()
+ self.window = window
+
+ NSLog("Finished launching!")
+ return true
+ }
+}
diff --git a/test/swift/apps/experimental/AsyncDemoFilter.swift b/test/swift/apps/experimental/AsyncDemoFilter.swift
new file mode 100644
index 0000000000..c25e10ef43
--- /dev/null
+++ b/test/swift/apps/experimental/AsyncDemoFilter.swift
@@ -0,0 +1,67 @@
+import Envoy
+import Foundation
+
+/// Example of a more complex HTTP filter that pauses processing on the response filter chain,
+/// buffers until the response is complete, then asynchronously triggers filter chain resumption
+/// while setting a new header. Also demonstrates safety of re-entrancy of async callbacks.
+final class AsyncDemoFilter: AsyncResponseFilter {
+ private var callbacks: ResponseFilterCallbacks!
+
+ func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterHeadersStatus
+ {
+ if endStream {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
+ self?.callbacks.resumeResponse()
+ }
+ }
+ return .stopIteration
+ }
+
+ func onResponseData(_ body: Data, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterDataStatus
+ {
+ if endStream {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
+ self?.callbacks.resumeResponse()
+ }
+ }
+ return .stopIterationAndBuffer
+ }
+
+ func onResponseTrailers(
+ _ trailers: ResponseTrailers,
+ streamIntel: StreamIntel
+ ) -> FilterTrailersStatus {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
+ self?.callbacks.resumeResponse()
+ }
+ return .stopIteration
+ }
+
+ func setResponseFilterCallbacks(_ callbacks: ResponseFilterCallbacks) {
+ self.callbacks = callbacks
+ }
+
+ func onResumeResponse(
+ headers: ResponseHeaders?,
+ data: Data?,
+ trailers: ResponseTrailers?,
+ endStream: Bool,
+ streamIntel: StreamIntel
+ ) -> FilterResumeStatus {
+ guard let headers = headers else {
+ // Iteration was stopped on headers, so headers must be present.
+ fatalError("Filter behavior violation!")
+ }
+ let builder = headers.toResponseHeadersBuilder()
+ .add(name: "async-filter-demo", value: "1")
+ return .resumeIteration(headers: builder.build(), data: data, trailers: trailers)
+ }
+
+ func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {}
+
+ func onCancel(streamIntel: FinalStreamIntel) {}
+
+ func onComplete(streamIntel: FinalStreamIntel) {}
+}
diff --git a/test/swift/apps/experimental/BUILD b/test/swift/apps/experimental/BUILD
new file mode 100644
index 0000000000..c4f17ed7e4
--- /dev/null
+++ b/test/swift/apps/experimental/BUILD
@@ -0,0 +1,19 @@
+load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application")
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+licenses(["notice"]) # Apache 2
+
+swift_library(
+ name = "appmain",
+ srcs = glob(["*.swift"]),
+ deps = ["//dist:envoy_mobile_ios"],
+)
+
+ios_application(
+ name = "app",
+ bundle_id = "io.envoyproxy.envoymobile.helloworld",
+ families = ["iphone"],
+ infoplists = ["Info.plist"],
+ minimum_os_version = "11.0",
+ deps = ["appmain"],
+)
diff --git a/test/swift/apps/experimental/Base.lproj/LaunchScreen.storyboard b/test/swift/apps/experimental/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000000..bfa3612941
--- /dev/null
+++ b/test/swift/apps/experimental/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/swift/apps/experimental/BufferDemoFilter.swift b/test/swift/apps/experimental/BufferDemoFilter.swift
new file mode 100644
index 0000000000..1def3fb677
--- /dev/null
+++ b/test/swift/apps/experimental/BufferDemoFilter.swift
@@ -0,0 +1,48 @@
+import Envoy
+import Foundation
+
+/// Example of a more complex HTTP filter that pauses processing on the response filter chain,
+/// buffers until the response is complete, then resumes filter iteration while setting a new
+/// header.
+final class BufferDemoFilter: ResponseFilter {
+ private var headers: ResponseHeaders!
+ private var body: Data?
+
+ func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterHeadersStatus
+ {
+ self.headers = headers
+ return .stopIteration
+ }
+
+ func onResponseData(_ data: Data, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterDataStatus
+ {
+ // Since we request buffering, each invocation will include all data buffered so far.
+ self.body = data
+
+ // If this is the end of the stream, resume processing of the (now fully-buffered) response.
+ if endStream {
+ let builder = self.headers.toResponseHeadersBuilder()
+ .add(name: "buffer-filter-demo", value: "1")
+ return .resumeIteration(headers: builder.build(), data: data)
+ }
+ return .stopIterationAndBuffer
+ }
+
+ func onResponseTrailers(
+ _ trailers: ResponseTrailers,
+ streamIntel: StreamIntel
+ ) -> FilterTrailersStatus {
+ // Trailers imply end of stream; resume processing of the (now fully-buffered) response.
+ let builder = self.headers.toResponseHeadersBuilder()
+ .add(name: "buffer-filter-demo", value: "1")
+ return .resumeIteration(headers: builder.build(), data: self.body, trailers: trailers)
+ }
+
+ func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {}
+
+ func onCancel(streamIntel: FinalStreamIntel) {}
+
+ func onComplete(streamIntel: FinalStreamIntel) {}
+}
diff --git a/test/swift/apps/experimental/DemoFilter.swift b/test/swift/apps/experimental/DemoFilter.swift
new file mode 100644
index 0000000000..27e79c3e6b
--- /dev/null
+++ b/test/swift/apps/experimental/DemoFilter.swift
@@ -0,0 +1,33 @@
+import Envoy
+import Foundation
+
+/// Example of a simple HTTP filter that adds a response header.
+struct DemoFilter: ResponseFilter {
+ func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterHeadersStatus
+ {
+ let builder = headers.toResponseHeadersBuilder()
+ builder.add(name: "filter-demo", value: "1")
+ return .continue(headers: builder.build())
+ }
+
+ func setResponseFilterCallbacks(_ callbacks: ResponseFilterCallbacks, streamIntel: StreamIntel) {}
+
+ func onResponseData(_ body: Data, endStream: Bool, streamIntel: StreamIntel)
+ -> FilterDataStatus
+ {
+ return .continue(data: body)
+ }
+
+ func onResponseTrailers(_ trailers: ResponseTrailers, streamIntel: StreamIntel)
+ -> FilterTrailersStatus
+ {
+ return .continue(trailers: trailers)
+ }
+
+ func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {}
+
+ func onCancel(streamIntel: FinalStreamIntel) {}
+
+ func onComplete(streamIntel: FinalStreamIntel) {}
+}
diff --git a/test/swift/apps/experimental/Info.plist b/test/swift/apps/experimental/Info.plist
new file mode 100644
index 0000000000..83af5464ce
--- /dev/null
+++ b/test/swift/apps/experimental/Info.plist
@@ -0,0 +1,47 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 0.1
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 0.1
+ ITSAppUsesNonExemptEncryption
+
+ NSAppTransportSecurity
+
+ NSExceptionDomains
+
+ localhost
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UIStatusBarStyle
+ UIStatusBarStyleLightContent
+ UILaunchStoryboardName
+ LaunchScreen
+
+
diff --git a/test/swift/apps/experimental/README.md b/test/swift/apps/experimental/README.md
new file mode 100644
index 0000000000..53e8270fb7
--- /dev/null
+++ b/test/swift/apps/experimental/README.md
@@ -0,0 +1 @@
+For instructions on how to use this demo, please head over to our [docs](https://envoy-mobile.github.io/docs/envoy-mobile/latest/start/examples/hello_world.html).
diff --git a/test/swift/apps/experimental/ResponseModels.swift b/test/swift/apps/experimental/ResponseModels.swift
new file mode 100644
index 0000000000..c83e555320
--- /dev/null
+++ b/test/swift/apps/experimental/ResponseModels.swift
@@ -0,0 +1,10 @@
+/// Represents a response from the server.
+struct Response {
+ let message: String
+ let headerMessage: String
+}
+
+/// Error that was encountered when executing a request.
+struct RequestError: Error {
+ let message: String
+}
diff --git a/test/swift/apps/experimental/ViewController.swift b/test/swift/apps/experimental/ViewController.swift
new file mode 100644
index 0000000000..34dc43e322
--- /dev/null
+++ b/test/swift/apps/experimental/ViewController.swift
@@ -0,0 +1,170 @@
+import Envoy
+import UIKit
+
+private let kCellID = "cell-id"
+private let kRequestAuthority = "api.lyft.com"
+private let kRequestPath = "/ping"
+private let kRequestScheme = "https"
+private let kFilteredHeaders =
+ ["server", "filter-demo", "async-filter-demo", "x-envoy-upstream-service-time"]
+
+final class ViewController: UITableViewController {
+ private var results = [Result]()
+ private var timer: Foundation.Timer?
+ private var streamClient: StreamClient?
+ private var pulseClient: PulseClient?
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ let engine = EngineBuilder()
+ .addLogLevel(.debug)
+ .addPlatformFilter(DemoFilter.init)
+ .addPlatformFilter(BufferDemoFilter.init)
+ .addPlatformFilter(AsyncDemoFilter.init)
+ .enableHappyEyeballs(true)
+ .enableInterfaceBinding(true)
+ // swiftlint:disable:next line_length
+ .addNativeFilter(name: "envoy.filters.http.buffer", typedConfig: "{\"@type\":\"type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer\",\"max_request_bytes\":5242880}")
+ .setOnEngineRunning { NSLog("Envoy async internal setup completed") }
+ .addStringAccessor(name: "demo-accessor", accessor: { return "PlatformString" })
+ .setEventTracker { NSLog("Envoy event emitted: \($0)") }
+ .build()
+ self.streamClient = engine.streamClient()
+ self.pulseClient = engine.pulseClient()
+
+ NSLog("started Envoy, beginning requests...")
+ self.startRequests()
+ }
+
+ deinit {
+ self.timer?.invalidate()
+ }
+
+ // MARK: - Requests
+
+ private func startRequests() {
+ self.timer = .scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
+ self?.performRequest()
+ self?.recordStats()
+ }
+ }
+
+ private func performRequest() {
+ guard let streamClient = self.streamClient else {
+ NSLog("failed to start request - Envoy is not running")
+ return
+ }
+
+ NSLog("starting request to '\(kRequestPath)'")
+
+ // Note: this request will use an h2 stream for the upstream request.
+ // The Objective-C example uses http/1.1. This is done on purpose to test both paths in
+ // end-to-end tests in CI.
+ let headers = RequestHeadersBuilder(method: .get, scheme: kRequestScheme,
+ authority: kRequestAuthority, path: kRequestPath)
+ .addUpstreamHttpProtocol(.http2)
+ .build()
+
+ streamClient
+ .newStreamPrototype()
+ .setOnResponseHeaders { [weak self] headers, _, _ in
+ let statusCode = headers.httpStatus.map(String.init) ?? "nil"
+ let message = "received headers with status \(statusCode)"
+
+ let headerMessage = headers.allHeaders()
+ .filter { kFilteredHeaders.contains($0.key) }
+ .map { "\($0.key): \($0.value.joined(separator: ", "))" }
+ .joined(separator: "\n")
+
+ NSLog(message)
+ if let filterDemoValue = headers.value(forName: "filter-demo")?.first {
+ NSLog("filter-demo: \(filterDemoValue)")
+ }
+ if let asyncFilterDemoValue = headers.value(forName: "async-filter-demo")?.first {
+ NSLog("async-filter-demo: \(asyncFilterDemoValue)")
+ }
+
+ let response = Response(message: message,
+ headerMessage: headerMessage)
+ self?.add(result: .success(response))
+ }
+ .setOnError { [weak self] error, _ in
+ let message: String
+ if let attemptCount = error.attemptCount {
+ message = "failed within Envoy library after \(attemptCount) attempts: \(error.message)"
+ } else {
+ message = "failed within Envoy library: \(error.message)"
+ }
+
+ NSLog(message)
+ self?.add(result: .failure(RequestError(message: message)))
+ }
+ .start()
+ .sendHeaders(headers, endStream: true)
+ }
+
+ private func add(result: Result) {
+ self.results.insert(result, at: 0)
+ self.tableView.reloadData()
+ }
+
+ private func recordStats() {
+ guard let pulseClient = self.pulseClient else {
+ NSLog("failed to send stats - Envoy is not running")
+ return
+ }
+
+ let counter = pulseClient.counter(elements: ["foo", "bar", "counter"])
+ counter.increment()
+ counter.increment(count: 5)
+
+ let gauge = pulseClient.gauge(elements: ["foo", "bar", "gauge"])
+ gauge.set(value: 5)
+ gauge.add(amount: 10)
+ gauge.sub(amount: 1)
+
+ let timer = pulseClient.timer(elements: ["foo", "bar", "timer"])
+ let distribution = pulseClient.distribution(elements: ["foo", "bar", "distribution"])
+ timer.recordDuration(durationMs: 15)
+ distribution.recordValue(value: 15)
+ }
+ // MARK: - UITableView
+
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return 1
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return self.results.count
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
+ -> UITableViewCell
+ {
+ let cell = tableView.dequeueReusableCell(withIdentifier: kCellID) ??
+ UITableViewCell(style: .subtitle, reuseIdentifier: kCellID)
+
+ let result = self.results[indexPath.row]
+ switch result {
+ case .success(let response):
+ cell.textLabel?.text = response.message
+ cell.detailTextLabel?.text = response.headerMessage
+
+ cell.textLabel?.textColor = .black
+ cell.detailTextLabel?.lineBreakMode = .byWordWrapping
+ cell.detailTextLabel?.numberOfLines = 0
+ cell.detailTextLabel?.textColor = .black
+ cell.contentView.backgroundColor = .white
+ case .failure(let error):
+ cell.textLabel?.text = error.message
+ cell.detailTextLabel?.text = nil
+
+ cell.textLabel?.textColor = .white
+ cell.detailTextLabel?.textColor = .white
+ cell.contentView.backgroundColor = .red
+ }
+
+ return cell
+ }
+}