Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 25 additions & 5 deletions .github/workflows/android-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ on:
push:
branches: [master]
pull_request:
workflow_call:
inputs:
sdk_version:
description: Published Maven Central SDK version for a release check
type: string
sdk_ref:
description: SDK source ref for development checks (ignored when sdk_version is set)
type: string
samples_ref:
description: Examples revision containing this workflow
required: true
type: string
workflow_dispatch:
inputs:
sdk_version:
Expand All @@ -14,7 +26,7 @@ on:

env:
# Baseline for source integration checks; release checks use sdk_version.
SDK_REF: 49a72dc936db4e27b65673100680677680c736ee
SDK_REF: f629c29ac7e523abf982a984ca089ba939b93c9e
SAMPLES_SDK_VERSION: ${{ inputs.sdk_version || '0.0.0-sdk-check' }}
SAMPLES_SDK_REPOSITORY: ${{ inputs.sdk_version && 'https://repo.maven.apache.org/maven2' || format('{0}/engine/android/build/sdk-check/repository', github.workspace) }}

Expand All @@ -23,7 +35,7 @@ permissions:

concurrency:
# SDK releases and source revisions have independent compatibility results.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ inputs.sdk_version || inputs.sdk_ref || 'baseline' }}
group: ${{ github.workflow }}-samples-${{ github.event.pull_request.number || github.ref }}-${{ inputs.sdk_version || inputs.sdk_ref || 'baseline' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
Expand All @@ -35,6 +47,8 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
repository: organicmaps/sdk-android
ref: ${{ inputs.samples_ref || github.sha }}
path: samples
- uses: actions/checkout@v6
if: ${{ !inputs.sdk_version }}
Expand Down Expand Up @@ -66,22 +80,28 @@ jobs:
- name: Build, lint and check Kotlin style for all samples
working-directory: samples
run: ./gradlew -PsdkRepository="$SAMPLES_SDK_REPOSITORY" -PsdkVersion="$SAMPLES_SDK_VERSION" checkSamples
- name: Verify packaged SDK runtime
if: ${{ !inputs.sdk_version }}
working-directory: engine/android
run: bash ../.github/scripts/check-sdk-aar.sh sdk/build/outputs/aar/sdk-release.aar
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Exercise sample lifecycle and result-list gestures
- name: Exercise sample lifecycle and the minified release consumer
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
target: default
profile: pixel_2
working-directory: samples
script: ./gradlew --continue -PsdkRepository="$SAMPLES_SDK_REPOSITORY" -PsdkVersion="$SAMPLES_SDK_VERSION" connectedSamples
script: ./gradlew --continue -PsdkRepository="$SAMPLES_SDK_REPOSITORY" -PsdkVersion="$SAMPLES_SDK_VERSION" connectedSamples verifyConnectedConsumer
- uses: actions/upload-artifact@v7
if: always()
with:
name: sample-reports
path: samples/app/build/reports
path: |
samples/app/build/reports
samples/runtime-checks/build/reports
12 changes: 9 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

## Project Structure & Module Organization

This repository demonstrates the Organic Maps Android SDK in one `app` module with independently installable `basic`, `offline`, and `routing` flavors.
The `app` module demonstrates the Organic Maps Android SDK with independently installable `basic`, `offline`, and `routing` tutorial flavors. The `consumerNative`, `consumerGoogle`, and `consumerFoss` flavors validate the published module combinations. The separate `runtime-checks` test module probes the minified Native consumer using only Android platform APIs.

- `app/src/main/java/app/organicmaps/example/`: Kotlin activities and `ExampleApplication`, which owns SDK initialization and readiness.
- `app/src/main/res/`: layouts, strings, themes, and drawable assets; the manifest is in `app/src/main/`.
- `app/src/consumer/`: shared consumer activity, resources, and navigation widget previews.
- `runtime-checks/src/main/`: platform-only release instrumentation runner, with no SDK or AndroidX dependencies.
- `gradle/sdk-consumer-checks.gradle.kts`: final-APK JNI checks, verifier regressions, and resolved SDK module assertions.
- `gradle/libs.versions.toml`: dependency versions; `.github/workflows/android-check.yaml`: CI.

## Build, Test, and Development Commands
Expand All @@ -19,19 +22,22 @@ Build fresh SDK artifacts from a sibling Organic Maps checkout:
./gradlew -PsdkRepository=../organicmaps/android/build/sdk-check/repository -PsdkVersion=0.0.0-sdk-check checkSamples
```

`checkSamples` checks Kotlin style, builds debug and minified release APKs for all flavors, and runs Android lint. Keep the same repository/version flags when running other sample tasks:
`checkSamples` checks Kotlin style, builds and lints all six debug and minified release variants, builds the tutorial instrumentation APKs and release probe, checks optional-module isolation, and verifies HTTP/JNI contracts in every final release APK with the Android build tools' `dexdump`. Keep the same repository/version flags when running other sample tasks:

- `./gradlew :app:assembleBasicDebug`: build the basic sample.
- `./gradlew :app:installOfflineDebug`: install offline search; use `installRoutingDebug` for routing.
- `./gradlew ktlintCheck` / `./gradlew ktlintFormat`: check or fix Kotlin formatting.
- `./gradlew :app:verifyReleaseJniContracts`: inspect the final release DEX types, method descriptors, and staticness.
- `./gradlew :app:verifySdkConsumerDependencies`: check SDK module isolation in all debug and release configurations.
- `./gradlew verifyConnectedConsumer`: run the platform-only probe against `consumerNativeRelease` on a matching device.

## Coding Style & Naming Conventions

Follow `.editorconfig`: four-space indentation for Kotlin and Gradle Kotlin scripts, 120-character lines, UTF-8, LF, and a final newline. Use ktlint's `android_studio` style and trailing commas. Name classes/files in PascalCase, functions/properties in camelCase, and Android resources in snake_case. Keep UI text in `res/values/strings.xml`.

## Testing Guidelines

AndroidX instrumentation tests live in `app/src/androidTest/`, `app/src/androidTestOffline/`, and `app/src/androidTestRouting/`. Run `checkSamples` before submitting changes and `connectedSamples` on a matching device or emulator. Verify rotation, light/dark themes, location permissions, cancelled downloads, offline search, missing-map routing, and background/foreground transitions. Repeat relevant checks with minified release APKs. See `README.md` for sample walkthroughs.
AndroidX tutorial instrumentation tests live in `app/src/androidTest/`, `app/src/androidTestOffline/`, and `app/src/androidTestRouting/`; the platform-only release probe lives in `runtime-checks/src/main/`. Run `checkSamples` before submitting changes and `connectedSamples verifyConnectedConsumer` on a matching device or emulator. The Google/FOSS consumers receive build and lint coverage; actual provider behavior requires an appropriate Google/microG environment. Car is included for dependency/build coverage. Verify rotation, light/dark themes, location permissions, cancelled downloads, offline search, missing-map routing, and background/foreground transitions. Repeat relevant checks with minified offline/routing APKs; the Native probe does not cover those feature scenarios. See `README.md` for sample walkthroughs.

## Commit & Pull Request Guidelines

Expand Down
61 changes: 57 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ Downloads continue independently of the screen and offer cancellation and retry.
The SDK is a side product of the main Organic Maps application. Its application
and SDK implementation are the source of truth; API stability is not guaranteed.
These examples must adapt when SDK changes break integration. See the
[SDK integration guide](https://github.com/organicmaps/organicmaps/blob/49a72dc936db4e27b65673100680677680c736ee/android/sdk/README.md)
[SDK integration guide](https://github.com/organicmaps/organicmaps/blob/f629c29ac7e523abf982a984ca089ba939b93c9e/android/sdk/README.md)
for initialization, lifecycle, permissions, packaging, and API contracts.

## Build and run

Use **JDK 21**, **Android SDK Platform 37**, and an **API 21+** device or emulator.
The examples consume native libraries from SDK AARs and do not need an NDK.

For development with SDK source, publish its artifacts locally. The merged revision
For development with SDK source, publish its artifacts locally. The pinned revision
below provides `publishSdkForChecks`, documented in
[organicmaps/organicmaps#14025](https://github.com/organicmaps/organicmaps/pull/14025).
Building that checkout requires the upstream
[native build environment](https://github.com/organicmaps/organicmaps/blob/49a72dc936db4e27b65673100680677680c736ee/docs/INSTALL.md).
[native build environment](https://github.com/organicmaps/organicmaps/blob/f629c29ac7e523abf982a984ca089ba939b93c9e/docs/INSTALL.md).
With an Organic Maps checkout in the sibling `organicmaps` directory:

```sh
git -C ../organicmaps fetch origin 49a72dc936db4e27b65673100680677680c736ee
git -C ../organicmaps fetch origin f629c29ac7e523abf982a984ca089ba939b93c9e
git -C ../organicmaps checkout --detach FETCH_HEAD
git -C ../organicmaps submodule update --init --recursive --depth 1
(cd ../organicmaps/android && ./gradlew -Parm64 -PsdkVersion=0.0.0-sdk-check publishSdkForChecks)
Expand Down Expand Up @@ -87,6 +87,59 @@ Keep the baseline SDK version/source pin current after adopting a release and
exercise minified release APKs on a device for JNI/R8 checks. Resource shrinking
is disabled here and needs separate validation of resources resolved by name.

## Published SDK consumer checks

Together, the `consumerNative`, `consumerGoogle`, and `consumerFoss` variants resolve,
build, and lint all published SDK modules. Car and lane/speed-limit widget dependencies
are confined to these consumers; the three tutorial apps use only the core SDK and
bundled World maps, including the transitive `location-core` module.
[ConsumerActivity](app/src/consumer/java/app/organicmaps/example/ConsumerActivity.kt)
constructs navigation widget previews through ordinary host references.

`checkSamples` checks the resolved SDK module set for all debug/release configurations
to prevent optional dependencies from leaking into the tutorials or mixing providers.
It also uses the Android build tools' `dexdump` to inspect every final release APK,
including all DEX files, for the HTTP/JNI types and exact static `runAsync` and concrete
instance `cancel` method descriptors. The R8 mapping locates the obfuscated concrete
Call class; method signatures are verified in DEX. Focused verifier regressions reject
removed/renamed methods, changed descriptors and incorrect staticness.

`checkSamples` also builds the platform-only release probe. Run that probe against
`consumerNativeRelease` on a device matching the published ABI:

```sh
./gradlew -PsdkRepository=../organicmaps/android/build/sdk-check/repository -PsdkVersion=0.0.0-sdk-check verifyConnectedConsumer
```

The [probe module](runtime-checks/) has no SDK or AndroidX dependencies and adds no
keep rules to the target. It checks the HTTP descriptor, native renderer readiness,
JNI-populated region data and host-created widgets, and fails if no test executes.
The Google/FOSS variants receive build/lint coverage; actual Google Play Services or
microG provider behavior requires an appropriate environment. Car receives dependency
and build coverage. Minified offline/routing feature scenarios remain separate from
this Native smoke test, as tracked in [issue #2](https://github.com/organicmaps/sdk-android/issues/2).

The reusable workflow accepts `sdk_version` for Maven Central release checks,
`sdk_ref` for optional source integration, and `samples_ref` for the examples revision.
For a dedicated manual or post-release workflow, call it at the same examples commit
that you pass as `samples_ref`:

```yaml
jobs:
check-sdk-consumers:
uses: organicmaps/sdk-android/.github/workflows/android-check.yaml@SAMPLES_COMMIT_SHA
permissions:
contents: read
with:
samples_ref: SAMPLES_COMMIT_SHA
sdk_version: PUBLISHED_SDK_VERSION
```

Replace both `SAMPLES_COMMIT_SHA` values with the same commit containing the reusable
workflow, and `PUBLISHED_SDK_VERSION` with a version available on Maven Central.
To check SDK source instead, omit `sdk_version` and optionally supply `sdk_ref`.
Consumer compatibility runs in this repository and does not gate SDK changes.

## License and support

See [LICENSE](LICENSE). Report sample issues in
Expand Down
25 changes: 25 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ plugins {
alias(libs.plugins.android.application)
}

val consumerFlavors = listOf("consumerNative", "consumerGoogle", "consumerFoss")

android {
namespace = "app.organicmaps.example"
compileSdk = providers.gradleProperty("propCompileSdkVersion").get().toInt()
Expand Down Expand Up @@ -45,6 +47,19 @@ android {
}
}

consumerFlavors.forEach { sample ->
productFlavors.create(sample) {
dimension = "sample"
applicationIdSuffix = ".$sample"
manifestPlaceholders["sampleActivity"] = "app.organicmaps.example.ConsumerActivity"
resValue("string", "app_name", "OM SDK Consumer (${sample.removePrefix("consumer")})")
}
sourceSets.named(sample) {
kotlin.directories += "src/consumer/java"
res.srcDir("src/consumer/res")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
Expand Down Expand Up @@ -77,6 +92,14 @@ dependencies {
// Bundles overview maps; detailed offline search and routing still require region downloads.
implementation("app.organicmaps.sdk:maps-world:$sdkVersion")

consumerFlavors.forEach { sample ->
listOf("car", "widgets-lanes", "widgets-speedlimit").forEach { artifact ->
add("${sample}Implementation", "app.organicmaps.sdk:$artifact:$sdkVersion")
}
}
"consumerGoogleImplementation"("app.organicmaps.sdk:location-google:$sdkVersion")
"consumerFossImplementation"("app.organicmaps.sdk:location-foss:$sdkVersion")

coreLibraryDesugaring(libs.android.tools.desugaring)

implementation(libs.androidx.appcompat)
Expand All @@ -86,3 +109,5 @@ dependencies {
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.junit)
}

apply(from = rootProject.file("gradle/sdk-consumer-checks.gradle.kts"))
39 changes: 39 additions & 0 deletions app/src/consumer/java/app/organicmaps/example/ConsumerActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package app.organicmaps.example

import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.LinearLayout
import android.widget.TextView
import app.organicmaps.sdk.routing.LaneInfo
import app.organicmaps.sdk.routing.LaneWay
import app.organicmaps.sdk.widgets.lanes.LanesView
import app.organicmaps.sdk.widgets.speedlimit.SpeedLimitView

/** A standalone host for the published SDK modules and navigation widget previews. */
class ConsumerActivity : MainActivity() {
override fun onCoreInitialized() {
super.onCoreInitialized()
val panel = samplePanel()
panel.addView(TextView(this).apply { setText(R.string.consumer_widgets) })
val widgetSize = (80 * resources.displayMetrics.density).toInt()
panel.addView(
LanesView(this, null).apply {
id = R.id.consumer_lanes
setLanes(arrayOf(LaneInfo(arrayOf(LaneWay.Left, LaneWay.Through), LaneWay.Through)))
},
LinearLayout.LayoutParams(MATCH_PARENT, widgetSize),
)
panel.addView(
SpeedLimitView(this).apply {
id = R.id.consumer_speed_limit
setSpeedLimit(50, false)
},
LinearLayout.LayoutParams(widgetSize, widgetSize),
)
}

override fun onRenderingInitializationFinished() {
// The external probe observes the native callback through a platform View, without naming host methods.
findViewById<View>(R.id.map).tag = true
}
}
6 changes: 6 additions & 0 deletions app/src/consumer/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="consumer_widgets">Navigation widget previews</string>
<item name="consumer_lanes" type="id" />
<item name="consumer_speed_limit" type="id" />
</resources>
38 changes: 33 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.test) apply false
}

val ktlint = configurations.create("ktlint")
Expand All @@ -15,27 +16,54 @@ tasks.register<JavaExec>("ktlintCheck") {
group = "verification"
classpath = ktlint
mainClass.set("com.pinterest.ktlint.Main")
args("--editorconfig=.editorconfig", "app/src/**/*.kt", "*.gradle.kts", "app/*.gradle.kts")
args(
"--editorconfig=.editorconfig",
"app/src/**/*.kt",
"*.gradle.kts",
"*/build.gradle.kts",
"gradle/**/*.gradle.kts",
)
}
tasks.register<JavaExec>("ktlintFormat") {
group = "formatting"
classpath = ktlint
mainClass.set("com.pinterest.ktlint.Main")
args("--format", "--editorconfig=.editorconfig", "app/src/**/*.kt", "*.gradle.kts", "app/*.gradle.kts")
args(
"--format",
"--editorconfig=.editorconfig",
"app/src/**/*.kt",
"*.gradle.kts",
"*/build.gradle.kts",
"gradle/**/*.gradle.kts",
)
}
val sampleFlavors = listOf("Basic", "Offline", "Routing")
val consumerFlavors = listOf("ConsumerNative", "ConsumerGoogle", "ConsumerFoss")

tasks.register("checkSamples") {
group = "verification"
description = "Build and lint all independently installable samples, including minified releases."
dependsOn("ktlintCheck", ":app:assembleDebug", ":app:assembleRelease")
sampleFlavors.forEach { sample ->
dependsOn(":app:lint${sample}Debug", ":app:lint${sample}Release", ":app:assemble${sample}DebugAndroidTest")
dependsOn(
"ktlintCheck",
":app:assembleDebug",
":app:verifyReleaseJniContracts",
":app:verifySdkConsumerDependencies",
":runtime-checks:assembleRelease",
)
(sampleFlavors + consumerFlavors).forEach { sample ->
dependsOn(":app:lint${sample}Debug", ":app:lint${sample}Release")
}
sampleFlavors.forEach { sample -> dependsOn(":app:assemble${sample}DebugAndroidTest") }
}

tasks.register("connectedSamples") {
group = "verification"
description = "Run each sample's instrumentation tests on a connected device or emulator."
sampleFlavors.forEach { sample -> dependsOn(":app:connected${sample}DebugAndroidTest") }
}

tasks.register("verifyConnectedConsumer") {
group = "verification"
description = "Exercise a minified consumer without adding test dependencies or keep rules to its APK."
dependsOn(":runtime-checks:verifyConnectedConsumer")
}
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
org.gradle.jvmargs=-Xmx2048m -Xms512m
org.gradle.caching=true
# Bound resource and R8 concurrency for the consumer matrix within the configured heap.
org.gradle.workers.max=2

android.useAndroidX=true

Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "andro

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-test = { id = "com.android.test", version.ref = "agp" }
Loading
Loading