Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
59ea5b5
Add android-zipline-bridge TypeScript package mirroring ios-jsc-bridge
akfreas May 6, 2026
a6f5ded
Add Android Gradle module structure with polyfill assets
akfreas May 6, 2026
29f9c2c
Port core data models to Kotlin (config, state, errors, preview DTOs,…
akfreas May 6, 2026
ee57d72
Port bridge layer to Kotlin (ZiplineContextManager, callbacks, polyfi…
akfreas May 6, 2026
2a82a89
Port storage layer to Kotlin (PersistentStore, SharedPreferencesStore)
akfreas May 6, 2026
5d9b1bb
Port handler layer to Kotlin (AppLifecycleHandler, NetworkMonitor)
akfreas May 6, 2026
5853b0c
Port OptimizationClient to Kotlin with StateFlow, suspend functions, …
akfreas May 6, 2026
7da42ba
Update Android SDK AGENTS.md and README to reflect implemented packag…
akfreas May 6, 2026
1adc829
Port tracking layer to Kotlin (TrackingMetadata, ViewTrackingController)
akfreas May 7, 2026
0065549
Port Compose UI layer (OptimizationRoot, OptimizedEntry, LazyColumn, …
akfreas May 7, 2026
73f9b26
Port preview panel to Compose (theme, components, overlay, ViewModel,…
akfreas May 7, 2026
88a2ee2
Update Android SDK docs to reflect Compose UI layer and preview panel
akfreas May 7, 2026
b0551ed
Fix Android SDK compilation errors for Kotlin 2.3 compatibility
akfreas May 7, 2026
ae6e244
Add Android reference implementation Gradle project scaffold
akfreas May 7, 2026
9ead65d
Add shared utilities for Android reference implementation (AppConfig,…
akfreas May 7, 2026
fffa1c4
Add UI components for Android reference implementation (ContentEntryV…
akfreas May 7, 2026
e70985d
Add screens for Android reference implementation (MainScreen, Navigat…
akfreas May 7, 2026
3c226c1
Add MainActivity entry point for Android reference implementation
akfreas May 7, 2026
395fc43
Add bootstrap script and monorepo integration for Android reference i…
akfreas May 7, 2026
7913a81
Add UI Automator 2 test module Gradle scaffold for Android reference …
akfreas May 7, 2026
947f57f
Add test support layer for Android UI tests (AppLauncher, TestHelpers…
akfreas May 7, 2026
1983fd6
Add 11 UI Automator 2 E2E test files mirroring iOS XCUITest suite
akfreas May 7, 2026
c4b1618
Update monorepo integration for Android UI test module
akfreas May 7, 2026
a6e7dfd
Migrate Android SDK from quickjs-android to quickjs-kt
akfreas May 7, 2026
b7ea1a4
Fix preview panel not showing definitions by refreshing state after load
akfreas May 7, 2026
ef85bc0
Add mock CDA client and testTag support for UI test integration
akfreas May 7, 2026
b1afcf4
Fix UI test suite to pass all 64 tests (preview panel, overrides, ext…
akfreas May 7, 2026
00e64e4
Add more preflight steps to bootstrap, checking for device availabili…
akfreas May 11, 2026
85adfac
Update android bridge
akfreas May 11, 2026
6fafd97
Add Android native E2E job to CI pipeline
akfreas May 11, 2026
2de498b
Fix CI silent pass and 8 test failures on Pixel 7 emulator
akfreas May 11, 2026
600747a
Fix CI script syntax and remaining 3 test failures on CI emulator
akfreas May 11, 2026
2de5f25
Fix navigation close and panel scroll for CI emulator compatibility
akfreas May 11, 2026
42444ee
Add BackHandler for nav close, fix panel scroll with bounded swipes
akfreas May 11, 2026
58bceb9
Increase default element timeout to 20s and disable emulator animations
akfreas May 11, 2026
710552e
Replace UiObject2.click() with coordinate-based taps for CI emulator …
akfreas May 11, 2026
b2b7dc3
Fix forceStop to actually kill app process and add emulator connectiv…
akfreas May 11, 2026
de17765
Revert forceStop crash and add adb reverse connectivity retry loop
akfreas May 11, 2026
6e2785b
Simplify CI connectivity check to adb reverse list with stabilization…
akfreas May 11, 2026
f0507eb
Wait for emulator network before adb reverse, use pm clear instead of…
akfreas May 11, 2026
88414e3
Fix network readiness loop to single line for android-emulator-runner…
akfreas May 11, 2026
bae32be
Remove network ping check and broken adb reverse list that killed tes…
akfreas May 11, 2026
30ab156
Revert forceStop to home screen approach to avoid killing instrumenta…
akfreas May 11, 2026
9eb5cc3
Use accessibility performAction(ACTION_CLICK) to bypass SwiftShader t…
akfreas May 11, 2026
1f2f6ca
Add adb reverse tunnel verification and stabilization delay to CI
akfreas May 11, 2026
7054c49
Fix remaining 7 test failures: dual click, timeouts, scroll, and delays
akfreas May 11, 2026
a42a38a
Fix waitForElement race condition, revert dual-click, and add robust …
akfreas May 11, 2026
4c95a4f
Add missing UiObject2 import to PreviewPanelOverridesTests
akfreas May 11, 2026
59f6984
Restore dual-click strategy with StaleObjectException guard, fix navi…
akfreas May 11, 2026
f883324
Fix expand-all double-toggle by using single coordinate click with fa…
akfreas May 11, 2026
c37d8e4
Add singleClick option to tapElement to prevent double-toggle on expa…
akfreas May 11, 2026
e7785c4
Support text-only elements in accessibility click for expand-all button
akfreas May 11, 2026
3ed87e7
Use UiScrollable with descriptionContains fallback for variant picker…
akfreas May 12, 2026
d95b84b
Use direct accessibility tree search for variant picker instead of he…
akfreas May 12, 2026
6c1b2a3
Reduce panel scroll iterations from 30 to 5 and use descContains matc…
akfreas May 12, 2026
9876dc2
Add diagnostics to variant picker search and try panel-scoped findObject
akfreas May 12, 2026
c5c0d59
Remove scroll loop from variant picker search to prevent CI hangs
akfreas May 12, 2026
15a4a68
Simplify variant picker search to only use accessibility tree traversal
akfreas May 12, 2026
d06e985
Use UiScrollable to scroll panel to off-screen variant picker elements
akfreas May 12, 2026
c3e5f11
Use coordinate-based panel scrolling to find variant picker after exp…
akfreas May 12, 2026
a42d819
Use AccessibilityNodeInfo ACTION_SCROLL_FORWARD to scroll panel witho…
akfreas May 12, 2026
241047b
Add scrolling and extended timeouts for navigation tests on slow CI e…
akfreas May 12, 2026
eed6dae
Use search bar to filter audiences instead of scrolling panel
akfreas May 12, 2026
c3a3479
Revert to coordinate scroll for audience toggles, expand single audie…
akfreas May 12, 2026
c787c5e
Revert unnecessary scroll and sleep in navigation tests
akfreas May 12, 2026
439fef2
Add contentDescription to audience expand header for UI test accessib…
akfreas May 12, 2026
7e0a8fa
Use audience-expand semantic identifier to expand target audience in …
akfreas May 12, 2026
1638561
Add gentle panel scroll after audience expand to reveal variant picker
akfreas May 12, 2026
2ae1626
Auto-scroll preview panel on audience expand to ensure variant picker…
akfreas May 12, 2026
c37a381
Fix variant picker expand: use coordinate click and scroll panel on a…
akfreas May 12, 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
159 changes: 159 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_android: ${{ steps.filter.outputs.e2e_android }}
e2e_ios: ${{ steps.filter.outputs.e2e_ios }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
Expand Down Expand Up @@ -126,6 +127,14 @@ jobs:
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'
# Android native implementation E2E coverage scope.
e2e_android:
- 'implementations/android-sdk/**'
- 'lib/mocks/**'
- 'packages/android/**'
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'
# iOS native implementation E2E coverage scope.
e2e_ios:
- 'implementations/ios-sdk/**'
Expand Down Expand Up @@ -659,6 +668,156 @@ jobs:
if-no-files-found: error
retention-days: 1

e2e-android-sdk:
name: 🤖 E2E Android Native
runs-on: namespace-profile-linux-16-vcpu-32-gb-ram-optimal
timeout-minutes: 45
needs: [setup, changes]
if: needs.changes.outputs.e2e_android == 'true'
env:
CI: 'true'
GRADLE_OPTS: >-
-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs=-Xmx4g
-Dkotlin.daemon.jvm.options=-Xmx2g
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: Set Android SDK environment variables
run: |
echo "ANDROID_SDK_ROOT=$HOME/.android/sdk" >> "$GITHUB_ENV"
echo "ANDROID_HOME=$HOME/.android/sdk" >> "$GITHUB_ENV"

- name: Prepare cache directories
run: |
mkdir -p "$HOME/.android/sdk" "$HOME/.android/avd" "$HOME/.android/cache"

- name: Set up caches (Namespace)
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3
with:
cache: |
pnpm
gradle
path: |
~/.android/sdk
~/.android/avd
~/.android/cache

- name: Install system dependencies (Android emulator)
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ca-certificates curl unzip zip git \
netcat-openbsd cpu-checker \
libgl1 libnss3 libx11-6 libx11-xcb1 libxcomposite1 libxdamage1 libxrandr2 libxtst6 \
libxi6 libxrender1 libxkbcommon0 libgbm1 libdbus-1-3 libdrm2 libpulse0
sudo apt-get install -y --no-install-recommends libasound2 || sudo apt-get install -y --no-install-recommends libasound2t64

- name: Verify KVM is available
run: |
if [ ! -e /dev/kvm ]; then
echo "/dev/kvm not found; Android hardware acceleration will not work." >&2
exit 1
fi
ls -l /dev/kvm
sudo kvm-ok || true

- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Android SDK
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1

- name: Install JS dependencies
run: pnpm install --prefer-offline --frozen-lockfile

- name: Build Android bridge JS bundle
run: pnpm --filter @contentful/optimization-android-bridge build

- name: Build app and test APKs
working-directory: implementations/android-sdk
run: ./gradlew :app:assembleDebug :uitests:assembleDebug

- 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 Android E2E Tests (emulator)
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
with:
api-level: 35
arch: x86_64
target: google_apis
profile: pixel_7
avd-name: test
force-avd-creation: true
emulator-boot-timeout: 600
cores: 6
ram-size: 4096M
disk-size: 8G
disable-animations: true
emulator-options: -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect
script: |
echo "Disabling animations..."
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
echo "Installing APKs..."
adb install -r implementations/android-sdk/app/build/outputs/apk/debug/app-debug.apk
adb install -r implementations/android-sdk/uitests/build/outputs/apk/debug/uitests-debug.apk
echo "Setting up adb reverse port forwarding..."
adb reverse tcp:8000 tcp:8000
sleep 3
adb shell "for i in 1 2 3 4 5 6 7 8 9 10; do nc -z localhost 8000 2>/dev/null && echo 'Mock server tunnel verified' && exit 0; sleep 1; done; echo 'WARNING: tunnel verification timed out'"
echo "Running UI Automator 2 E2E tests..."
adb shell am instrument -w com.contentful.optimization.uitests/androidx.test.runner.AndroidJUnitRunner 2>&1 | tee /tmp/test-output.log
grep -q "FAILURES" /tmp/test-output.log && { echo "::error::Android UI tests failed"; exit 1; } || true
grep -q "Process crashed" /tmp/test-output.log && { echo "::error::Test process crashed"; exit 1; } || true

- name: Upload logs on failure
if: failure()
run: |
echo "=== Mock Server Logs ==="
cat /tmp/mock-server.log || echo "No mock server logs found"

- 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: always()
with:
name: ci-results-android-sdk
path: |
implementations/android-sdk/logs/
/tmp/mock-server.log
/tmp/test-output.log
retention-days: 7

e2e-ios-sdk-build:
name: 🍎 Build iOS UI Test Bundles
runs-on: namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb
Expand Down
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)
Loading
Loading