Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c0b0336
[NT-3070] Wire ios-sdk into the pnpm implementation runner with build…
akfreas May 5, 2026
cba3e7c
[NT-3070] Add iOS UI test build and matrix run jobs to the main pipeline
akfreas May 5, 2026
b219953
Update runs-on to namespace-profile-macos-apple-silicon-arm64-6-cpu-1…
akfreas May 6, 2026
632876f
Add android-zipline-bridge TypeScript package mirroring ios-jsc-bridge
akfreas May 6, 2026
4c3c4f1
Add Android Gradle module structure with polyfill assets
akfreas May 6, 2026
7dc08fd
Port core data models to Kotlin (config, state, errors, preview DTOs,…
akfreas May 6, 2026
77bd81b
Port bridge layer to Kotlin (ZiplineContextManager, callbacks, polyfi…
akfreas May 6, 2026
af35171
Port storage layer to Kotlin (PersistentStore, SharedPreferencesStore)
akfreas May 6, 2026
539638f
Port handler layer to Kotlin (AppLifecycleHandler, NetworkMonitor)
akfreas May 6, 2026
e26f0da
Port OptimizationClient to Kotlin with StateFlow, suspend functions, …
akfreas May 6, 2026
1edd9e7
Update Android SDK AGENTS.md and README to reflect implemented packag…
akfreas May 6, 2026
ee1cf13
Update runs-on to namespace-profile-macos-apple-silicon-arm64-6-cpu-1…
akfreas May 7, 2026
05860c2
Port tracking layer to Kotlin (TrackingMetadata, ViewTrackingController)
akfreas May 7, 2026
3d91bcf
Port Compose UI layer (OptimizationRoot, OptimizedEntry, LazyColumn, …
akfreas May 7, 2026
c82d713
Port preview panel to Compose (theme, components, overlay, ViewModel,…
akfreas May 7, 2026
50646fc
Update Android SDK docs to reflect Compose UI layer and preview panel
akfreas May 7, 2026
d683ddc
Fix Android SDK compilation errors for Kotlin 2.3 compatibility
akfreas May 7, 2026
4af7ece
Add Android reference implementation Gradle project scaffold
akfreas May 7, 2026
3f50770
Add shared utilities for Android reference implementation (AppConfig,…
akfreas May 7, 2026
ce9be1f
Add UI components for Android reference implementation (ContentEntryV…
akfreas May 7, 2026
a5eb5d6
Add screens for Android reference implementation (MainScreen, Navigat…
akfreas May 7, 2026
52fb0c1
Add MainActivity entry point for Android reference implementation
akfreas May 7, 2026
9b40342
Add bootstrap script and monorepo integration for Android reference i…
akfreas May 7, 2026
b62b571
Add UI Automator 2 test module Gradle scaffold for Android reference …
akfreas May 7, 2026
20c647d
Add test support layer for Android UI tests (AppLauncher, TestHelpers…
akfreas May 7, 2026
31ddf92
Add 11 UI Automator 2 E2E test files mirroring iOS XCUITest suite
akfreas May 7, 2026
831be37
Update monorepo integration for Android UI test module
akfreas May 7, 2026
b941f8a
Migrate Android SDK from quickjs-android to quickjs-kt
akfreas May 7, 2026
8a0d9f3
Fix preview panel not showing definitions by refreshing state after load
akfreas May 7, 2026
2e23812
Add mock CDA client and testTag support for UI test integration
akfreas May 7, 2026
61483f7
Fix UI test suite to pass all 64 tests (preview panel, overrides, ext…
akfreas May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions .github/workflows/main-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
e2e_web_sdk_react: ${{ steps.filter.outputs.e2e_web_sdk_react }}
e2e_react_web_sdk: ${{ steps.filter.outputs.e2e_react_web_sdk }}
e2e_react_native_android: ${{ steps.filter.outputs.e2e_react_native_android }}
e2e_ios: ${{ steps.filter.outputs.e2e_ios }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1

Expand Down Expand Up @@ -125,6 +126,14 @@ jobs:
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'
# iOS native implementation E2E coverage scope.
e2e_ios:
- 'implementations/ios-sdk/**'
- 'lib/mocks/**'
- 'packages/ios/**'
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'

setup:
name: 🛠️ pnpm install
Expand Down Expand Up @@ -650,6 +659,57 @@ jobs:
if-no-files-found: error
retention-days: 1

e2e-ios-sdk-build:
name: 🍎 Build iOS UI Test Bundles
runs-on: namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb
timeout-minutes: 30
needs: [setup, changes]
if: needs.changes.outputs.e2e_ios == 'true'
env:
DERIVED_DATA: /tmp/optimization-ios-derived-data
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
package-manager-cache: false

- uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3

- name: Install XcodeGen and xcbeautify
run: brew install xcodegen xcbeautify

- name: Set up caches (Namespace)
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3
with:
cache: pnpm
path: |
~/Library/Caches/org.swift.swiftpm

- name: Show toolchain
run: |
xcodebuild -version
xcrun simctl list runtimes | head

- run: pnpm install --prefer-offline --frozen-lockfile

- name: Build iOS UI test bundles (SwiftUI + UIKit)
run: pnpm run implementation:ios-sdk -- test:e2e:ios:build:release

- name: Stage Build/Products for artifact
run: |
mkdir -p /tmp/ios-artifact
cp -R "$DERIVED_DATA/Build/Products" /tmp/ios-artifact/Products
ls /tmp/ios-artifact/Products/*.xctestrun

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ios-uitest-bundles
path: /tmp/ios-artifact/
if-no-files-found: error
retention-days: 1

e2e-react-native-android:
name: 📱 E2E React Native Android (shard ${{ matrix.shard }}/2)
runs-on: namespace-profile-linux-16-vcpu-32-gb-ram-optimal
Expand Down Expand Up @@ -808,3 +868,94 @@ jobs:
implementations/react-native-sdk/.detox/
/tmp/mock-server.log
retention-days: 7

e2e-ios-sdk:
name: 🍎 E2E iOS UI (${{ matrix.scheme }})
runs-on: namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb
timeout-minutes: 45
needs: [setup, changes, e2e-ios-sdk-build]
if: needs.changes.outputs.e2e_ios == 'true'
strategy:
fail-fast: false
matrix:
include:
- scheme: SwiftUI
- scheme: UIKit
env:
DERIVED_DATA: /tmp/optimization-ios-derived-data
IOS_SCHEME: ${{ matrix.scheme }}
IOS_SIM_NAME: 'iPhone 16'
IOS_SIM_OS: 'latest'
# Smoke mode for the first PR — restrict to one test class per scheme.
# Remove this env var in a follow-up PR to enable the full suite.
IOS_ONLY_TESTING: OptimizationAppUITests${{ matrix.scheme }}/PreviewPanelTests
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
package-manager-cache: false

- uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3

- name: Install xcbeautify
run: brew install xcbeautify

- uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3
with:
cache: pnpm

- run: pnpm install --prefer-offline --frozen-lockfile

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ios-uitest-bundles
path: /tmp/ios-artifact/

- name: Reconstruct DerivedData layout at stable path
run: |
mkdir -p "$DERIVED_DATA/Build"
mv /tmp/ios-artifact/Products "$DERIVED_DATA/Build/Products"
ls "$DERIVED_DATA/Build/Products"

- name: Boot iOS Simulator
run: |
DEVICE_UDID=$(xcrun simctl create "ci-${{ matrix.scheme }}" "$IOS_SIM_NAME")
echo "DEVICE_UDID=$DEVICE_UDID" >> "$GITHUB_ENV"
xcrun simctl boot "$DEVICE_UDID"
xcrun simctl bootstatus "$DEVICE_UDID" -b

- name: Start Mock Server
run: |
pnpm --dir lib/mocks serve > /tmp/mock-server.log 2>&1 &
echo $! > /tmp/mock-server.pid
for i in {1..60}; do
if nc -z localhost 8000 2>/dev/null; then
echo "Mock server is ready"
break
fi
echo "Waiting for mock server... ($i/60)"
sleep 1
done
if ! nc -z localhost 8000 2>/dev/null; then
echo "Mock server failed to start:"
cat /tmp/mock-server.log
exit 1
fi

- name: Run iOS UI tests (${{ matrix.scheme }})
run: pnpm run implementation:ios-sdk -- test:e2e:ios:run:release

- name: Stop Mock Server
if: always()
run: kill $(cat /tmp/mock-server.pid) 2>/dev/null || true

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ !cancelled() }}
with:
name: ci-results-ios-${{ matrix.scheme }}
path: |
/tmp/optimization-ios-derived-data/Test-*.xcresult
/tmp/mock-server.log
retention-days: 7
5 changes: 5 additions & 0 deletions implementations/android-sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.gradle/
app/build/
uitests/build/
local.properties
logs/
69 changes: 69 additions & 0 deletions implementations/android-sdk/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# AGENTS.md

Read the repository root `AGENTS.md`, then `implementations/AGENTS.md`, before this file.

## Scope

This is the native Android reference implementation for bridge and preview-panel validation work. It
uses a Jetpack Compose app shell with the Android SDK library module included via Gradle composite
build.

## Key paths

- `app/src/main/kotlin/com/contentful/optimization/app/` — App source
- `app/src/main/kotlin/com/contentful/optimization/app/screens/` — Screen composables
- `app/src/main/kotlin/com/contentful/optimization/app/components/` — Reusable UI components
- `uitests/` — UI Automator 2 E2E test module (`com.android.test`)
- `uitests/src/main/kotlin/.../uitests/tests/` — Test files (1:1 mirror of iOS XCUITest suite)
- `uitests/src/main/kotlin/.../uitests/support/` — Shared test helpers, app launcher, device
extensions
- `scripts/` — Build and run scripts
- `build.gradle.kts` — Root build config (plugin versions)
- `settings.gradle.kts` — Project structure (includes SDK module + uitests via project.dir)
- `app/build.gradle.kts` — App module build config and dependencies

## Local rules

- Keep this app focused on validating native Android integration behavior. Reusable SDK behavior
belongs in `packages/android/ContentfulOptimization`, and TypeScript bridge behavior belongs in
`packages/android/android-zipline-bridge`.
- The mock server must be running at `http://localhost:8000` before running the app. Use
`adb reverse tcp:8000 tcp:8000` to forward the port to the emulator.
- The app references the SDK via Gradle `include` + `project.dir` in `settings.gradle.kts`. After
SDK source changes, rebuild via `./gradlew :app:assembleDebug` from this directory.
- Keep accessibility identifiers (testTags) aligned with the iOS SwiftUI implementation and
`implementations/PREVIEW_PANEL_SCENARIOS.md`.
- Use `Modifier.testTag()` for app-level test identifiers. The root composable sets
`testTagsAsResourceId = true` so UI Automator 2 can discover them as `resource-id`.
- The SDK uses `Modifier.semantics { contentDescription = ... }` for its own identifiers (e.g.,
`OptimizedEntry`'s `accessibilityIdentifier` parameter).
- Test launch arguments use intent extras: `--ez reset true` clears SDK SharedPreferences,
`--ez simulate_offline true` sets the client offline.

## Commands

- `pnpm serve:mocks` (from monorepo root)
- From `implementations/android-sdk/`: `./gradlew :app:assembleDebug`
- From `implementations/android-sdk/`: `./scripts/bootstrap.sh`
- Build bridge first: `pnpm --filter @contentful/optimization-android-bridge build`
- Build UI test APK: `./gradlew :uitests:assembleDebug`
- Run all UI tests: `./gradlew :uitests:connectedAndroidTest`
- Run single test class:
`./gradlew :uitests:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.contentful.optimization.uitests.tests.AnalyticsTests`

## UI tests

- The `uitests/` module is a `com.android.test` Gradle module — fully decoupled from app internals.
- Tests interact with the app purely through UI Automator 2's accessibility layer.
- Element discovery: `By.res("testTag")` for app `testTag` values, `By.desc("id")` for SDK
`contentDescription` elements (e.g., `content-entry-{id}`).
- Test names and accessibility identifiers match the iOS XCUITest suite at
`implementations/ios-sdk/uitests/Tests/` for cross-platform test parity.
- The mock server must be running and port-forwarded before running tests.

## Usually validate

- Run the app on emulator after changes to verify UI renders correctly.
- Verify accessibility identifiers match iOS counterparts when changing UI structure.
- Rebuild `@contentful/optimization-android-bridge` before testing when bridge source changed.
- After UI structure changes, run `./gradlew :uitests:assembleDebug` to verify test APK compiles.
90 changes: 90 additions & 0 deletions implementations/android-sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../../documentation/assets/contentful-logo-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="../../documentation/assets/contentful-logo-light.png" />
<img width="300" alt="Contentful" src="../../documentation/assets/contentful-logo-light.png" />
</picture>

### Contentful Personalization & Analytics

<h3>Android SDK Reference Implementation</h3>

[Readme](./README.md) · [Guides](https://contentful.github.io/optimization/documents/Guides.html) ·
[Reference](https://contentful.github.io/optimization/) · [Contributing](../../CONTRIBUTING.md)

</div>

---

> [!CAUTION] Pre-release. API surface is not yet stable.

This is the native Android reference implementation for the
[Contentful Optimization Android SDK](../../packages/android/README.md). It demonstrates the minimal
integration pattern using Jetpack Compose and serves as a test target for UI Automator 2 E2E tests.

## What this demonstrates

- `OptimizationRoot` initialization with mock server configuration
- `OptimizedEntry` personalization with view and click tracking
- Nested entry resolution and recursive rendering
- Navigation with screen tracking via `ScreenTrackingEffect`
- Live updates behavior: default (global), explicit live, and locked variants
- `PreviewPanelOverlay` with audience/variant override controls
- Analytics event display for debugging tracked events
- All accessibility identifiers aligned with the iOS SwiftUI implementation for cross-platform E2E
parity

## Prerequisites

- Android SDK with `ANDROID_HOME` set
- Android emulator or connected device
- `adb` in PATH
- pnpm dependencies installed at monorepo root (`pnpm install`)
- Android bridge built: `pnpm --filter @contentful/optimization-android-bridge build`

## Setup

From the monorepo root:

```sh
pnpm install
pnpm --filter @contentful/optimization-android-bridge build
```

## Running locally

The bootstrap script starts the mock server, builds the app, and launches it on an emulator:

```sh
cd implementations/android-sdk
./scripts/bootstrap.sh
```

Or manually:

```sh
# Terminal 1: Start mock server
pnpm serve:mocks

# Terminal 2: Build and install
cd implementations/android-sdk
adb reverse tcp:8000 tcp:8000
./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n com.contentful.optimization.app/.MainActivity
```

To launch with test arguments (clear state or simulate offline):

```sh
adb shell am start -n com.contentful.optimization.app/.MainActivity --ez reset true
adb shell am start -n com.contentful.optimization.app/.MainActivity --ez simulate_offline true
```

## Related

- [Android SDK](../../packages/android/README.md)
- [iOS SDK Reference Implementation](../ios-sdk/README.md)
- [React Native Reference Implementation](../react-native-sdk/README.md)
- [Preview Panel Scenarios](../PREVIEW_PANEL_SCENARIOS.md)
- [Mock Server](../../lib/mocks/README.md)
54 changes: 54 additions & 0 deletions implementations/android-sdk/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}

android {
namespace = "com.contentful.optimization.app"
compileSdk = 36

defaultConfig {
applicationId = "com.contentful.optimization.app"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
}

buildTypes {
release {
isMinifyEnabled = false
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

buildFeatures {
compose = true
}
}

kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}

dependencies {
implementation(project(":ContentfulOptimization"))

implementation(platform("androidx.compose:compose-bom:2024.12.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material3:material3")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.navigation:navigation-compose:2.8.5")

implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}
Loading
Loading