diff --git a/.dockleignore b/.dockleignore
new file mode 100644
index 0000000..71da3bf
--- /dev/null
+++ b/.dockleignore
@@ -0,0 +1,6 @@
+# [Enable Content trust for Docker] - We don't really need this much because only we both publish and pull
+# this image and pin the version so we can't accidentally pull unknown malicious image.
+CIS-DI-0005
+
+# [Add HEALTHCHECK instruction to the container image] - Does not make sense for our CI image.
+CIS-DI-0006
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 9a7a27f..d252920 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -21,6 +21,7 @@ jobs:
uses: ./.github/actions/login
with:
user-name: ${{ vars.DOCKER_HUB_TEST_USERNAME }}
+ # Test public repos read only token on a separate androidackee test account.
token: ${{ vars.DOCKER_HUB_TEST_TOKEN }}
- name: Preflight checks
diff --git a/CHANGELOG.MD b/CHANGELOG.MD
index 2434969..7faf4a9 100644
--- a/CHANGELOG.MD
+++ b/CHANGELOG.MD
@@ -1,9 +1,28 @@
# Changelog
All notable changes to this project will be documented in this file.
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [v3.0.0] - 2026-02-17
+### Changed
+- Complete rewrite of Dockerfile using multi-stage build architecture (separate stages for Java, Android SDK, Danger, Git LFS)
+- Danger JS installation now runs as `nonroot` user to reduce attack surface
+- Node.js installed from system apt packages instead of via nvm
+- Updated Android cmdline-tools to 14742923, platform to 36, build-tools to 36.1.0
+
+### Added
+- Checksum verification for all downloaded artifacts (Java, Kotlin compiler, danger-kotlin)
+- Integration of shai-hulud supply chain attack detector for npm packages
+- npm scripts disabled globally to prevent supply chain attacks
+- Final image runs as `nonroot` user
+
+### Removed
+- Flutter support
+- nvm and Node.js version management
+- Google Cloud CLI
+- Privilege escalation binaries from final image (`su`, `apt`, `apt-get`, `apt-cache`, `dpkg`, `unix_chkpwd`)
+
## [v2.8.0] - 2025-12-04
### Changed
- Update danger kotlin to 1.3.4
diff --git a/Dockerfile b/Dockerfile
index a150ac9..197866c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,128 +1,238 @@
-FROM debian:trixie
+# === Global args ===
-LABEL tag="ackee-gitlab" \
- author="Ackee 🦄" \
- description="This Docker image serves as an environment for running Android builds on Gitlab CI in Ackee workspace"
+# == Versions ==
+
+ARG ANDROID_CMDLINE_TOOLS_VERSION="14742923"
+ARG ANDROID_BUILD_TOOLS_VERSION="36.1.0"
+ARG ANDROID_PLATFORM_VERSION="36"
+
+ARG DANGER_JS_VERSION="12.3.4"
+ARG DANGER_KOTLIN_VERSION="1.3.4"
+ARG DANGER_KOTLIN_CHECKSUM="sha256:232b11680cdfe50c64a6cef1d96d3cd09a857422da1e2dd0464f80c8ddb1afac"
+
+# If you need to update major Java version, you will need to adjust the version in the download link as well
+ARG JAVA_VERSION="17.0.18_8"
+ARG JAVA_CHECKSUM="sha256:0c94cbb54325c40dcf026143eb621562017db5525727f2d9131a11250f72c450"
+
+# Needed for danger-kotlin
+ARG KOTLINC_VERSION="2.2.21"
+ARG KOTLINC_CHECKSUM="sha256:a623871f1cd9c938946948b70ef9170879f0758043885bbd30c32f024e511714"
+
+# == Others ==
+
+ARG CMDLINE_TOOLS_DIR="cmdline-tools"
+ARG CMDLINE_TOOLS_VERSION_DIR="latest"
+
+ARG DANGER_BASE_PATH="/usr/local"
+ARG KOTLINC_BASE_PATH="/usr/lib"
+
+
+
+# === Stages ===
+
+# == base ==
+
+# Base stage that all stages inherit from. It contains common setup needed for both build and final stages.
+FROM dhi.io/debian-base:trixie-debian13-dev AS base
+
+ARG CMDLINE_TOOLS_DIR
+ARG CMDLINE_TOOLS_VERSION_DIR
+ARG JAVA_VERSION
SHELL ["/bin/bash", "-c"]
-RUN apt update && apt install -y \
- curl \
- git \
- libgl1 \
- unzip \
- zip \
- python3 \
- wget \
- xz-utils \
- fontconfig \
- gnupg
-
-RUN curl -s "https://get.sdkman.io" | bash && \
- source "$HOME/.sdkman/bin/sdkman-init.sh" && \
- sdk install java 17.0.7-oracle && \
- sdk use java 17.0.7-oracle
-
-ENV JAVA_HOME=/root/.sdkman/candidates/java/current
-ENV ANDROID_HOME=/opt/android-sdk-linux
+ENV ANDROID_HOME="/opt/android-sdk-linux"
+ENV PATH="$PATH:$ANDROID_HOME/$CMDLINE_TOOLS_DIR/$CMDLINE_TOOLS_VERSION_DIR"
+ENV PATH="$PATH:$ANDROID_HOME/$CMDLINE_TOOLS_DIR/$CMDLINE_TOOLS_VERSION_DIR/bin"
+ENV PATH="$PATH:$ANDROID_HOME/platform-tools"
+
+ENV JAVA_HOME="/usr/lib/jdk/$JAVA_VERSION"
ENV PATH="$PATH:$JAVA_HOME/bin"
-# Download Android SDK command line tools into $ANDROID_HOME
-RUN cd /opt && wget -q https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip -O android-sdk-tools.zip && \
- unzip -q android-sdk-tools.zip && mkdir -p "$ANDROID_HOME/cmdline-tools/" && mv cmdline-tools latest && mv latest/ "$ANDROID_HOME"/cmdline-tools/ && \
- rm android-sdk-tools.zip
+RUN apt update && apt install -y --no-install-recommends \
+ git \
+ && rm -rf /var/lib/apt/lists/*
+
+
+
+# == build ==
+
+# Base stage for all build stages. These stages are used for building dependencies that are then
+# copied to the final image.
+FROM base AS build
+
+RUN apt update && apt install -y --no-install-recommends \
+ curl \
+ # Needed for danger-js installation
+ npm \
+ unzip
+
+# Disables npm preinstall, postintall and other scripts that might run when any npm package is installed,
+# which is usually exploited by supply chain attacks like shai-hulud
+RUN npm config set ignore-scripts true
+
+
+
+# == java-installation ==
+
+FROM build AS java-installation
+
+ARG JAVA_CHECKSUM
+ARG JAVA_VERSION
+
+ARG JAVA_TEMP_DIR="java"
+# Replace _ with + in version for download link
+ENV JAVA_VERSION_PLUS="${JAVA_VERSION/_/+}"
+ADD --checksum="$JAVA_CHECKSUM" --unpack=true \
+ "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-$JAVA_VERSION_PLUS/OpenJDK17U-jdk_x64_linux_hotspot_$JAVA_VERSION.tar.gz" \
+ "$JAVA_TEMP_DIR"
+RUN mkdir -p "$JAVA_HOME" && \
+ # Temp dir contains unpacked JDK folder, we want to move its contents to JAVA_HOME
+ mv "$JAVA_TEMP_DIR"/*/* "$JAVA_HOME"
+
+
+
+# == android-sdk-installation ==
+
+# Installs Android SDK. Requires Java to be already installed.
+FROM java-installation AS android-sdk-installation
+
+ARG ANDROID_BUILD_TOOLS_VERSION
+ARG ANDROID_CMDLINE_TOOLS_VERSION
+ARG ANDROID_PLATFORM_VERSION
+ARG CMDLINE_TOOLS_DIR
+ARG CMDLINE_TOOLS_VERSION_DIR
-ENV PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools"
+WORKDIR /opt
+
+ARG CMDLINE_TOOLS_ZIP="cmdline-tools.zip"
+ADD "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip" \
+ "./$CMDLINE_TOOLS_ZIP"
+
+ARG CMDLINE_TOOLS_PATH="$ANDROID_HOME/$CMDLINE_TOOLS_DIR"
+RUN mkdir -p "$CMDLINE_TOOLS_PATH" && \
+ unzip -q "$CMDLINE_TOOLS_ZIP" -d "$CMDLINE_TOOLS_PATH" && \
+ mv "$CMDLINE_TOOLS_PATH/$CMDLINE_TOOLS_DIR" "$CMDLINE_TOOLS_PATH/$CMDLINE_TOOLS_VERSION_DIR"
# Accept licenses before installing components
# License is valid for all the standard components in versions installed from this file
# Non-standard components: MIPS system images, preview versions, GDK (Google Glass) and Android Google TV require separate licenses, not accepted there
RUN yes | sdkmanager --licenses
-RUN sdkmanager "platform-tools"
-
-# list all platforms, sort them in descending order, take the newest 8 versions and install them
-RUN sdkmanager $(sdkmanager --list 2> /dev/null | grep platforms | awk -F' ' '{print $1}' | sort -nr -k2 -t- | head -8)
-# list all build-tools, sort them in descending order and install them
-RUN sdkmanager $(sdkmanager --list 2> /dev/null | grep build-tools | awk -F' ' '{print $1}' | sort -nr -k2 -t \; | uniq)
-
-# setup gcloud
-RUN echo "deb https://packages.cloud.google.com/apt cloud-sdk main" >> /etc/apt/sources.list.d/google-cloud-sdk.list && \
- curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor >> /etc/apt/trusted.gpg.d/cloud.google.gpg && \
- apt update && apt install -y google-cloud-cli && \
- gcloud config set component_manager/disable_update_check true
-
-# nvm environment variables
-ENV NVM_DIR=/usr/local/nvm \
- NODE_VERSION=23.5.0
-
-# install nvm
-# https://github.com/creationix/nvm#install-script
-RUN mkdir $NVM_DIR && \
- curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.7/install.sh | bash
-
-RUN source $NVM_DIR/nvm.sh && \
- nvm install $NODE_VERSION && \
- nvm alias default $NODE_VERSION && \
- nvm use default
-
-# add node and npm to path so the commands are available
-ENV NODE_PATH=$NVM_DIR/v$NODE_VERSION/lib/node_modules \
- PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
-
-# install make which is needed in danger-kotlin install phase
-RUN apt update && apt install -y \
- make
-
-# install danger-js which is needed for danger-kotlin to work
-RUN npm install -g danger@12.3.4
-
-# install kotlin compiler
-RUN curl -o kotlin-compiler.zip -L https://github.com/JetBrains/kotlin/releases/download/v2.2.21/kotlin-compiler-2.2.21.zip && \
- unzip -d /usr/local/ kotlin-compiler.zip && \
- rm -rf kotlin-compiler.zip
-
-# install danger-kotlin
-RUN git clone https://github.com/danger/kotlin.git _danger-kotlin && \
- cd _danger-kotlin && git checkout 1.3.4 && \
- make install && \
- cd .. && \
- rm -rf _danger-kotlin
-
-# setup environment variables
-ENV PATH=$PATH:/usr/local/kotlinc/bin
-
-# flutter
-ENV FLUTTER_CHANNEL="stable"
-ENV FLUTTER_VERSION="3.24.3"
-ENV FLUTTER_URL="https://storage.googleapis.com/flutter_infra_release/releases/$FLUTTER_CHANNEL/linux/flutter_linux_$FLUTTER_VERSION-$FLUTTER_CHANNEL.tar.xz"
-ENV FLUTTER_HOME="/opt/flutter"
-ENV FLUTTER_FILE="flutter.tar.xz"
-
-RUN curl -o $FLUTTER_FILE $FLUTTER_URL \
- && mkdir -p $FLUTTER_HOME \
- && tar xf $FLUTTER_FILE -C /opt \
- && git config --global --add safe.directory /opt/flutter \
- && rm $FLUTTER_FILE
-
-ENV PATH=$PATH:$FLUTTER_HOME/bin
-
-RUN flutter config --no-analytics \
- && flutter precache \
- && yes "y" | flutter doctor --android-licenses \
- && flutter doctor \
- && flutter update-packages
-
-# git LFS support
-RUN curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash \
- && apt install -y git-lfs \
- && git lfs install
-
-# add gitlab helper functions
-ENV GITLAB_CI_UTILS_VERSION=2.7.0
-RUN curl -o helper_functions.sh "https://raw.githubusercontent.com/AckeeDevOps/gitlab-ci-utils/$GITLAB_CI_UTILS_VERSION/scripts/helper_functions.sh" \
- && curl -o android_ci_functions.sh "https://raw.githubusercontent.com/AckeeCZ/android-gitlab-ci-scripts/v1.0.0/android_ci_functions.sh" \
- && echo "source helper_functions.sh" >> /etc/profile \
- && echo "source android_ci_functions.sh" >> /etc/profile
-
-VOLUME /root/.gradle
+
+
+# == danger-installation ==
+
+FROM build AS danger-installation
+
+ARG DANGER_BASE_PATH
+ARG DANGER_JS_VERSION
+ARG DANGER_KOTLIN_VERSION
+ARG DANGER_KOTLIN_CHECKSUM
+ARG KOTLINC_BASE_PATH
+ARG KOTLINC_CHECKSUM
+ARG KOTLINC_VERSION
+
+# chown of directories where danger will be installed, so nonroot npm process can write there
+RUN chown -R nonroot:nonroot "$DANGER_BASE_PATH"
+# Change to nonroot user for npm install to reduce attack surface if compromised
+USER nonroot
+
+# Install danger JS that is needed for danger-kotlin
+RUN npm install -g "danger@$DANGER_JS_VERSION"
+# Solves dockle's DKL-LI-0003 reported issue to remove unnecessary files
+RUN rm -f "$DANGER_BASE_PATH/lib/node_modules/danger/Dockerfile"
+
+# Clone and run shai-hulud-detector script that checks for shai-hulud supply chain attacks
+WORKDIR /tmp
+RUN git clone https://github.com/Cobenian/shai-hulud-detect
+# Allows user to override shai-hulud detector mode. Useful for running --paranoid mode on CI, which
+# takes longer but is more secure and checks even for other malicious behaviour other than shai-hulud.
+ARG SHAI_HULUD_DETECTOR_MODE=""
+RUN ./shai-hulud-detect/shai-hulud-detector.sh "$SHAI_HULUD_DETECTOR_MODE" "$(npm root -g)"; \
+ exit_code=$?; \
+ case "$exit_code" in \
+ # Succeed on medium-risk issues found (2)
+ 2) exit 0 ;; \
+ # Fail on high-risk issues found (1), succeed (0) or fail with anything else
+ *) exit "$exit_code" ;; \
+ esac
+
+# Switch back to root to finish Danger installation
+USER root
+
+# Install Kotlin compiler
+ARG COMPILER_ZIP="kotlin-compiler.zip"
+ADD --checksum="$KOTLINC_CHECKSUM" \
+ "https://github.com/JetBrains/kotlin/releases/download/v$KOTLINC_VERSION/kotlin-compiler-$KOTLINC_VERSION.zip" \
+ "./$COMPILER_ZIP"
+RUN unzip "$COMPILER_ZIP" -d "$KOTLINC_BASE_PATH"
+
+# Install danger-kotlin
+ADD --checksum="$DANGER_KOTLIN_CHECKSUM" --unpack=true \
+ "https://github.com/danger/kotlin/releases/download/$DANGER_KOTLIN_VERSION/danger-kotlin-linuxX64.tar" \
+ "$DANGER_BASE_PATH"
+
+
+
+# == git-lfs-installation ==
+
+FROM build AS git-lfs-installation
+
+RUN curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
+ apt update && apt install -y --no-install-recommends git-lfs
+
+
+
+# == final ==
+
+# Final stage of the image build process. This is the only stage that is kept in the final image.
+# This should mainly copy prepared binaries from other stages.
+FROM base AS final
+
+ARG DANGER_BASE_PATH
+ARG KOTLINC_BASE_PATH
+
+LABEL tag="ackee-gitlab" \
+ author="Ackee 🦄" \
+ description="This Docker image serves as an environment for running Android builds on Gitlab CI in Ackee workspace"
+
+RUN apt update && apt install -y --no-install-recommends \
+ # Needed for danger-js
+ nodejs \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --from=java-installation "$JAVA_HOME" "$JAVA_HOME"
+
+COPY --from=android-sdk-installation "$ANDROID_HOME" "$ANDROID_HOME"
+# Allows nonroot user to modify Android SDK folders, e.g. download new build tools/platforms
+RUN chown -R nonroot:nonroot "$ANDROID_HOME"
+
+# Danger binaries
+ARG DANGER_BIN_PATH="$DANGER_BASE_PATH/bin"
+COPY --from=danger-installation "$DANGER_BIN_PATH" "$DANGER_BIN_PATH"
+
+ARG DANGER_LIB_PATH="$DANGER_BASE_PATH/lib"
+
+# Danger JS node_modules dependencies
+ARG DANGER_NODE_MODULES_PATH="$DANGER_LIB_PATH/node_modules"
+COPY --from=danger-installation "$DANGER_NODE_MODULES_PATH" "$DANGER_NODE_MODULES_PATH"
+
+# danger-kotlin libs
+ARG DANGER_KOTLIN_LIB_PATH="$DANGER_LIB_PATH/danger"
+COPY --from=danger-installation "$DANGER_KOTLIN_LIB_PATH" "$DANGER_KOTLIN_LIB_PATH"
+
+COPY --from=danger-installation "$KOTLINC_BASE_PATH" "$KOTLINC_BASE_PATH"
+ENV PATH="$PATH:$KOTLINC_BASE_PATH/kotlinc/bin"
+
+ARG GIT_LFS_PATH="/usr/bin/git-lfs"
+COPY --from=git-lfs-installation "$GIT_LFS_PATH" "$GIT_LFS_PATH"
+RUN git lfs install
+
+# Remove binaries that might allow privilege escalation
+RUN rm -f /bin/su
+RUN rm -f /usr/bin/apt /usr/bin/apt-get /usr/bin/apt-cache
+RUN rm -f /usr/bin/dpkg
+RUN rm -f /usr/sbin/unix_chkpwd
+
+USER nonroot
diff --git a/README.MD b/README.MD
index e02f87b..b26f882 100644
--- a/README.MD
+++ b/README.MD
@@ -1,12 +1,72 @@
-# Docker image for Android builds on Gitlab CI
-
-This Docker image serves as an environment for running Android builds on Gitlab CI in Ackee workspace.
-
-Contains:
-- Java 17 environment
-- NVM with default node version to 12.2.0
-- Latest 8 Android version SDKs + Platform CLI tools
-- gcloud CLI tool
-- danger-kotlin
-- Flutter 3.24.3 binaries
-- [Git LFS](https://git-lfs.com/)
+# Docker image for Android builds on GitLab CI
+
+Docker image providing a complete environment for running Android builds on GitLab CI in Ackee workspace.
+
+## Contents
+
+- Java 17 (OpenJDK from Eclipse Temurin)
+- Android SDK (cmdline-tools, platform tools, build-tools)
+- danger-kotlin + danger-js + Kotlin compiler
+- Node.js (system package, required by danger-js)
+- Git LFS
+- Base: dhi.io/debian-base
+
+## Security
+
+- Runs as unprivileged `nonroot` user
+- Privilege escalation binaries removed (`su`, `apt`, `dpkg`, `unix_chkpwd`)
+- npm scripts disabled to guard against supply chain attacks
+- [shai-hulud](https://github.com/AckeeCZ/shai-hulud) supply chain attack detector runs at build time
+- All downloaded artifacts verified with checksums
+
+## Local development
+
+### Prerequisites
+
+Two logins are required. A dedicated personal access token for local testing can be found in
+Passwd under **"docker-gitlab-builder-android local test"**.
+
+```shell
+docker login dhi.io # Required to pull the hardened base image
+docker login docker.io # Required for Docker Scout CVE analysis
+```
+
+### Build
+
+```shell
+docker compose build
+```
+
+Optionally run shai-hulud in paranoid mode:
+
+```shell
+docker compose build --build-arg SHAI_HULUD_DETECTOR_MODE=--paranoid
+```
+
+### Test
+
+```shell
+docker compose run --rm gitlab-builder-android
+```
+
+This runs `image-test.sh` inside the container, which checks security properties, verifies all
+required tools are present and functional, and runs a real Gradle build against the bundled `image-test-app`.
+
+## CI
+
+### Image lint (dockle)
+
+Dockle is used via `erzz/dockle-action` in the common-preflight-check GitHub Actions composite action.
+It runs automatically on every PR and deploy, after the build and test steps.
+
+To reproduce locally (requires dockle installed):
+
+```shell
+dockle --exit-code 1 ackee/gitlab-builder-android:test
+```
+
+### Docker Scout
+
+On pull requests, Docker Scout scans the built image for critical/high CVEs that have a fix available
+and posts results as a PR comment for manual review. Does not ever fail the pipeline, because usually
+it is not easily possible to fix even fixable CVEs, because they can come from transitive dependencies.
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..a30a147
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,12 @@
+services:
+ gitlab-builder-android:
+ build:
+ context: .
+ platforms:
+ - "linux/amd64"
+ image: ackee/gitlab-builder-android:${IMAGE_TAG:-test}
+ stdin_open: true
+ volumes:
+ - ./image-test-app:/image-test-app
+ - ./image-test.sh:/image-test.sh
+ command: bash -e /image-test.sh
diff --git a/image-test-app/.gitignore b/image-test-app/.gitignore
new file mode 100644
index 0000000..b96627a
--- /dev/null
+++ b/image-test-app/.gitignore
@@ -0,0 +1,11 @@
+*.iml
+.gradle
+/local.properties
+.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+.kotlin
diff --git a/image-test-app/app/.gitignore b/image-test-app/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/image-test-app/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/image-test-app/app/build.gradle.kts b/image-test-app/app/build.gradle.kts
new file mode 100644
index 0000000..08fae6b
--- /dev/null
+++ b/image-test-app/app/build.gradle.kts
@@ -0,0 +1,53 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.compose)
+}
+
+android {
+ namespace = "cz.ackee.docker.image.test"
+ compileSdk {
+ version = release(36)
+ }
+
+ defaultConfig {
+ applicationId = "cz.ackee.docker.image.test"
+ minSdk = 26
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ buildFeatures {
+ compose = true
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.graphics)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.compose.material3)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.compose.ui.test.junit4)
+ debugImplementation(libs.androidx.compose.ui.tooling)
+ debugImplementation(libs.androidx.compose.ui.test.manifest)
+}
diff --git a/image-test-app/app/proguard-rules.pro b/image-test-app/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/image-test-app/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/image-test-app/app/src/main/AndroidManifest.xml b/image-test-app/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..49e5819
--- /dev/null
+++ b/image-test-app/app/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/image-test-app/app/src/main/java/cz/ackee/docker/image/test/MainActivity.kt b/image-test-app/app/src/main/java/cz/ackee/docker/image/test/MainActivity.kt
new file mode 100644
index 0000000..bd91e82
--- /dev/null
+++ b/image-test-app/app/src/main/java/cz/ackee/docker/image/test/MainActivity.kt
@@ -0,0 +1,48 @@
+package cz.ackee.docker.image.test
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import cz.ackee.docker.image.test.ui.theme.ImagetestappTheme
+
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ ImagetestappTheme {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ Greeting(
+ name = "Android",
+ modifier = Modifier.padding(innerPadding)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun Greeting(name: String, modifier: Modifier = Modifier) {
+ Text(
+ text = "Hello $name!",
+ modifier = modifier
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun GreetingPreview() {
+ ImagetestappTheme {
+ Greeting("Android")
+ }
+}
diff --git a/image-test-app/app/src/main/java/cz/ackee/docker/image/test/ui/theme/Color.kt b/image-test-app/app/src/main/java/cz/ackee/docker/image/test/ui/theme/Color.kt
new file mode 100644
index 0000000..6443d6c
--- /dev/null
+++ b/image-test-app/app/src/main/java/cz/ackee/docker/image/test/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package cz.ackee.docker.image.test.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/image-test-app/app/src/main/java/cz/ackee/docker/image/test/ui/theme/Theme.kt b/image-test-app/app/src/main/java/cz/ackee/docker/image/test/ui/theme/Theme.kt
new file mode 100644
index 0000000..05abdce
--- /dev/null
+++ b/image-test-app/app/src/main/java/cz/ackee/docker/image/test/ui/theme/Theme.kt
@@ -0,0 +1,57 @@
+package cz.ackee.docker.image.test.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun ImagetestappTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
diff --git a/image-test-app/app/src/main/java/cz/ackee/docker/image/test/ui/theme/Type.kt b/image-test-app/app/src/main/java/cz/ackee/docker/image/test/ui/theme/Type.kt
new file mode 100644
index 0000000..5ac0a4e
--- /dev/null
+++ b/image-test-app/app/src/main/java/cz/ackee/docker/image/test/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package cz.ackee.docker.image.test.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/image-test-app/app/src/main/res/drawable/ic_launcher_background.xml b/image-test-app/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/image-test-app/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/image-test-app/app/src/main/res/drawable/ic_launcher_foreground.xml b/image-test-app/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..7706ab9
--- /dev/null
+++ b/image-test-app/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/image-test-app/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/image-test-app/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..b3e26b4
--- /dev/null
+++ b/image-test-app/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/image-test-app/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/image-test-app/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..b3e26b4
--- /dev/null
+++ b/image-test-app/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/image-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/image-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/image-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/image-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/image-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/image-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/image-test-app/app/src/main/res/values/colors.xml b/image-test-app/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..ca1931b
--- /dev/null
+++ b/image-test-app/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
diff --git a/image-test-app/app/src/main/res/values/strings.xml b/image-test-app/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..45cc628
--- /dev/null
+++ b/image-test-app/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ image-test-app
+
\ No newline at end of file
diff --git a/image-test-app/build.gradle.kts b/image-test-app/build.gradle.kts
new file mode 100644
index 0000000..b546c74
--- /dev/null
+++ b/image-test-app/build.gradle.kts
@@ -0,0 +1,5 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+}
diff --git a/image-test-app/gradle.properties b/image-test-app/gradle.properties
new file mode 100644
index 0000000..132244e
--- /dev/null
+++ b/image-test-app/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
diff --git a/image-test-app/gradle/libs.versions.toml b/image-test-app/gradle/libs.versions.toml
new file mode 100644
index 0000000..4d9ebd1
--- /dev/null
+++ b/image-test-app/gradle/libs.versions.toml
@@ -0,0 +1,31 @@
+[versions]
+agp = "9.0.0"
+coreKtx = "1.17.0"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.5.1"
+lifecycleRuntimeKtx = "2.10.0"
+activityCompose = "1.12.4"
+kotlin = "2.0.21"
+composeBom = "2024.09.00"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+
diff --git a/image-test-app/gradle/wrapper/gradle-wrapper.jar b/image-test-app/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8bdaf60
Binary files /dev/null and b/image-test-app/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/image-test-app/gradle/wrapper/gradle-wrapper.properties b/image-test-app/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..0b1a953
--- /dev/null
+++ b/image-test-app/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+#Sat Feb 14 23:33:34 CET 2026
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/image-test-app/gradlew b/image-test-app/gradlew
new file mode 100755
index 0000000..ef07e01
--- /dev/null
+++ b/image-test-app/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/image-test-app/gradlew.bat b/image-test-app/gradlew.bat
new file mode 100644
index 0000000..5eed7ee
--- /dev/null
+++ b/image-test-app/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/image-test-app/settings.gradle.kts b/image-test-app/settings.gradle.kts
new file mode 100644
index 0000000..c326f2e
--- /dev/null
+++ b/image-test-app/settings.gradle.kts
@@ -0,0 +1,23 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "image-test-app"
+include(":app")
diff --git a/image-test.sh b/image-test.sh
new file mode 100755
index 0000000..7a0cf34
--- /dev/null
+++ b/image-test.sh
@@ -0,0 +1,116 @@
+#!/bin/bash
+
+check_user_not_root() {
+ if [ "$(id -u)" -eq 0 ]; then
+ echo "User must not be root! 💣" >&2
+ exit 1
+ else
+ echo "User is unprivileged $(whoami) 🔒"
+ fi
+}
+
+check_command_missing() {
+ local command=$1
+ if command -v "$command" &> /dev/null; then
+ echo "$command exists but should not 💣" >&2
+ exit 1
+ else
+ echo "$command is missing 🔒"
+ fi
+}
+
+check_command_exists() {
+ local command=$1
+ local default_version_option="--version"
+ local version_option="${2:-$default_version_option}"
+ if "$command" "$version_option" >/dev/null; then
+ echo "$command ✅"
+ else
+ echo "$command ❌" >&2
+ exit 1
+ fi
+}
+
+check_dir() {
+ local dir=$1
+ if [[ -d "$dir" ]]; then
+ echo "$dir exists ✅"
+ else
+ echo "Error: directory not found: $dir" >&2
+ exit 1
+ fi
+}
+
+check_dir_writable() {
+ local dir=$1
+ if test -w "$dir"; then
+ echo "$dir writable ✅"
+ else
+ echo "$dir not writable but must be ❌" >&2
+ exit 1
+ fi
+}
+
+check_env() {
+ local var=$1
+ if [[ -n ${!var} ]]; then
+ echo "$var present ✅"
+ else
+ echo "Error: env var missing or empty: $var" >&2
+ exit 1
+ fi
+}
+
+# Security
+echo
+echo "Security check"
+check_user_not_root
+check_command_missing "apt"
+check_command_missing "apt-get"
+check_command_missing "dpkg"
+check_command_missing "npm"
+check_command_missing "su"
+check_command_missing "sudo"
+
+# Required custom SW
+echo
+echo "Required custom SW check"
+check_command_exists "git"
+check_command_exists "git-lfs"
+check_command_exists "java"
+
+check_command_exists "sdkmanager"
+check_dir_writable "$ANDROID_HOME"
+check_dir "$ANDROID_HOME/cmdline-tools"
+check_dir "$ANDROID_HOME/licenses"
+
+check_command_exists "danger"
+check_command_exists "danger-kotlin"
+check_command_exists "kotlinc" "-version" # Needed for danger-kotlin
+check_command_exists "nodejs" # Needed for danger (JS)
+check_dir "/usr/local/lib/danger" # Check that danger-kotlin lib exists
+check_dir "/usr/local/lib/node_modules/danger" # Check that danger node_modules exists
+
+# Commonly used Linux SW on CI
+echo
+echo "Commonly used Linux SW on CI check"
+check_command_exists "awk"
+check_command_exists "cat"
+check_command_exists "cp"
+check_command_exists "grep"
+check_command_exists "ln"
+check_command_exists "mkdir"
+check_command_exists "rm"
+
+# Env vars
+echo
+echo "Env vars check"
+check_env "ANDROID_HOME"
+check_env "JAVA_HOME"
+
+# Build image-test-app
+echo
+echo "Build image-test-app check"
+cd /image-test-app
+./gradlew assembleDebug --no-daemon
+echo "image-test-app build successful ✅"