Repository: ReactiveCircus/android-emulator-runner (js)

Issue:
API-LEVEL does not work with latest version within the latest sdk like (35-ext15)
Google started to release images like `system-images;android-35-ext15;android-wear;arm64-v8a`. Right now in the current version of this Action we can't use it.

We should either add a `suffix` option to keep api-level a number or parse the number in api-level.

Something like
```
 - uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: "35-ext15"
```
OR
```
 - uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: "35"
          api-level-sufix: "-ext15"
```

Pull request:
Support API levels for SDK extensions and add missing targets for automotive and desktop.
Fixes #429.

This action is missing some targets. I am currently working into using this action into https://github.com/home-assistant/android/ and I need the `android-automotive` target. I've added all the targets that I found missing.
I don't have the opportunity to test them on Github Action yet.

-----

Added targets:
- android-automotive
- android-automotive-playstore
- android-desktop

Also added `system-image-api-level` to support specifying different api-levels for platform and system images as they aren't always aligned for sdk extensions.

Removed input validation on `api-level` so SDK extensions e.g. `android-35-ext15` and preview API levels e.g. `android-Baklava` are supported.

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 587da86..e8cd13b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -39,6 +39,11 @@ jobs:
             api-level: 35
             target: google_apis
             arch: x86_64
+          - os: ubuntu-latest
+            api-level: 34-ext10
+            target: android-automotive
+            arch: x86_64
+            system-image-api-level: 34-ext9
 
     steps:
     - name: checkout
@@ -85,6 +90,7 @@ jobs:
         api-level: ${{ matrix.api-level }}
         target: ${{ matrix.target }}
         arch: ${{ matrix.arch }}
+        system-image-api-level: ${{ matrix.system-image-api-level }}
         profile: Galaxy Nexus
         cores: 2
         sdcard-path-or-size: 100M
@@ -102,6 +108,7 @@ jobs:
         api-level: ${{ matrix.api-level }}
         target: ${{ matrix.target }}
         arch: ${{ matrix.arch }}
+        system-image-api-level: ${{ matrix.system-image-api-level }}
         profile: Galaxy Nexus
         cores: 2
         ram-size: 2048M
diff --git a/.github/workflows/manually.yml b/.github/workflows/manually.yml
index a74e1b3..3a99f08 100644
--- a/.github/workflows/manually.yml
+++ b/.github/workflows/manually.yml
@@ -7,11 +7,13 @@ on:
         required: true
         default: 'ubuntu-latest'
       api-level:
-        description: 'API level of the platform and system image'
+        description: 'API level of the platform and system image (if not overridden with system-image-api-level input) - e.g. 33, 35-ext15, Baklava'
         required: true
         default: '34'
+      system-image-api-level:
+        description: 'API level of the system image - e.g. 34-ext10, 35-ext15'
       target:
-        description: 'target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv or google-tv'
+        description: 'target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv, google-tv, android-automotive, android-automotive-playstore or android-desktop'
         required: true
         default: 'default'
       arch:
@@ -68,6 +70,7 @@ jobs:
         api-level: ${{ github.event.inputs.api-level }}
         target: ${{ github.event.inputs.target }}
         arch: ${{ github.event.inputs.arch }}
+        system-image-api-level: ${{ github.event.inputs.system-image-api-level }}
         profile: Galaxy Nexus
         emulator-options: ${{ github.event.inputs.emulator-options }}
         emulator-build: ${{ github.event.inputs.emulator-build }}
diff --git a/README.md b/README.md
index 9808792..4f68e1b 100644
--- a/README.md
+++ b/README.md
@@ -119,6 +119,31 @@ jobs:
           script: ./gradlew connectedCheck
 ```
 
+If you need a specific [SDKExtension](https://developer.android.com/guide/sdk-extensions) for the system image but not the platform
+
+```yml
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+
+      - 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: run tests
+        uses: reactivecircus/android-emulator-runner@v2
+        with:
+          api-level: 34
+          system-image-api-level: 34-ext9
+          target: android-automotive
+          script: ./gradlew connectedCheck
+```
+
 We can significantly reduce emulator startup time by setting up AVD snapshot caching:
 
 1. add a `gradle/actions/setup-gradle@v4` step for caching Gradle, more details see [#229](https://github.com/ReactiveCircus/android-emulator-runner/issues/229)
@@ -180,7 +205,8 @@ jobs:
 | **Input** | **Required** | **Default** | **Description** |
 |-|-|-|-|
 | `api-level` | Required | N/A | API level of the platform system image - e.g. 23 for Android Marshmallow, 29 for Android 10. **Minimum API level supported is 15**. |
-| `target` | Optional | `default` | Target of the system image - `default`, `google_apis`, `playstore`, `android-wear`, `android-wear-cn`, `android-tv`, `google-tv`, `aosp_atd` or `google_atd`. Note that `aosp_atd` and `google_atd` currently require the following: `api-level: 30`, `arch: x86` or `arch: arm64-v8` and `channel: canary`. |
+| `system-image-api-level` | Optional | `ap-level` | API level of the system image - e.g. 23 for Android Marshmallow, 29 for Android 10. |
+| `target` | Optional | `default` | Target of the system image - `default`, `google_apis`, `playstore`, `android-wear`, `android-wear-cn`, `android-tv`, `google-tv`, `aosp_atd`, `google_atd`, `android-automotive`, `android-automotive-playstore` or `android-desktop`. Note that `aosp_atd` and `google_atd` currently require the following: `api-level: 30`, `arch: x86` or `arch: arm64-v8` and `channel: canary`. |
 | `arch` | Optional | `x86` | CPU architecture of the system image - `x86`, `x86_64` or `arm64-v8a`. Note that `x86_64` image is only available for API 21+. `arm64-v8a` images require Android 4.2+ and are limited to fewer API levels (e.g. 30). |
 | `profile` | Optional | N/A | Hardware profile used for creating the AVD - e.g. `Nexus 6`. For a list of all profiles available, run `avdmanager list device`. |
 | `cores` | Optional | 2 | Number of cores to use for the emulator (`hw.cpu.ncore` in config.ini). |
@@ -243,5 +269,6 @@ These are some of the open-source projects using (or used) **Android Emulator Ru
 - [ACRA/acra](https://github.com/ACRA/acra/blob/master/.github/workflows/test.yml)
 - [bitfireAT/davx5-ose](https://github.com/bitfireAT/davx5-ose/blob/dev-ose/.github/workflows/test-dev.yml)
 - [robolectric/robolectric](https://github.com/robolectric/robolectric/blob/master/.github/workflows/tests.yml)
+- [home-assistant/android](https://github.com/home-assistant/android/blob/master/.github/workflows/pr.yml)
 
 If you are using **Android Emulator Runner** and want your project included in the list, please feel free to open a pull request.
diff --git a/action-types.yml b/action-types.yml
index b77d4b1..49bcc51 100644
--- a/action-types.yml
+++ b/action-types.yml
@@ -1,6 +1,8 @@
 inputs:
   api-level:
-    type: integer
+    type: string
+  system-image-api-level:
+    type: string
   target:
     type: enum
     allowed-values:
@@ -13,6 +15,9 @@ inputs:
       - android-wear-cn
       - android-tv
       - google-tv
+      - android-automotive
+      - android-automotive-playstore
+      - android-desktop
   arch:
     type: enum
     allowed-values:
diff --git a/action.yml b/action.yml
index fa60d74..35dcccd 100644
--- a/action.yml
+++ b/action.yml
@@ -6,10 +6,13 @@ branding:
   color: 'green'
 inputs:
   api-level:
-    description: 'API level of the platform and system image - e.g. 23 for Android Marshmallow, 29 for Android 10'
+    description: 'API level of the platform and system image - e.g. 23, 33, 35-ext15, Baklava'
     required: true
+  system-image-api-level:
+    description: 'API level of the system image - e.g. 34-ext10, 35-ext15. If not set the `api-level` input will be used.'
+    required: false
   target:
-    description: 'target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv or google-tv'
+    description: 'target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv, google-tv, android-automotive, android-automotive-playstore or android-desktop'
     default: 'default'
   arch:
     description: 'CPU architecture of the system image - x86, x86_64 or arm64-v8a'
diff --git a/lib/emulator-manager.js b/lib/emulator-manager.js
index a74991e..56a6d11 100644
--- a/lib/emulator-manager.js
+++ b/lib/emulator-manager.js
@@ -38,7 +38,7 @@ const fs = __importStar(require("fs"));
 /**
  * Creates and launches a new AVD instance with the specified configurations.
  */
-function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellChecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard) {
+function launchEmulator(systemImageApiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellChecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard) {
     return __awaiter(this, void 0, void 0, function* () {
         try {
             console.log(`::group::Launch Emulator`);
@@ -48,7 +48,7 @@ function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSiz
                 const profileOption = profile.trim() !== '' ? `--device '${profile}'` : '';
                 const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : '';
                 console.log(`Creating AVD.`);
-                yield exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`);
+                yield exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${systemImageApiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`);
             }
             if (cores) {
                 yield exec.exec(`sh -c \\"printf 'hw.cpu.ncore=${cores}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
diff --git a/lib/input-validator.js b/lib/input-validator.js
index 626ada8..9960b46 100644
--- a/lib/input-validator.js
+++ b/lib/input-validator.js
@@ -1,24 +1,25 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.checkDiskSize = exports.checkEmulatorBuild = exports.checkEnableHardwareKeyboard = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkPort = exports.checkForceAvdCreation = exports.checkChannel = exports.checkArch = exports.checkTarget = exports.checkApiLevel = exports.PREVIEW_API_LEVELS = exports.MAX_PORT = exports.MIN_PORT = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.VALID_TARGETS = exports.MIN_API_LEVEL = void 0;
+exports.checkDiskSize = exports.checkEmulatorBuild = exports.checkEnableHardwareKeyboard = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkPort = exports.checkForceAvdCreation = exports.checkChannel = exports.checkArch = exports.checkTarget = exports.MAX_PORT = exports.MIN_PORT = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.VALID_TARGETS = exports.MIN_API_LEVEL = void 0;
 exports.MIN_API_LEVEL = 15;
-exports.VALID_TARGETS = ['default', 'google_apis', 'aosp_atd', 'google_atd', 'google_apis_playstore', 'android-wear', 'android-wear-cn', 'android-tv', 'google-tv'];
+exports.VALID_TARGETS = [
+    'default',
+    'google_apis',
+    'aosp_atd',
+    'google_atd',
+    'google_apis_playstore',
+    'android-wear',
+    'android-wear-cn',
+    'android-tv',
+    'google-tv',
+    'android-automotive',
+    'android-automotive-playstore',
+    'android-desktop',
+];
 exports.VALID_ARCHS = ['x86', 'x86_64', 'arm64-v8a'];
 exports.VALID_CHANNELS = ['stable', 'beta', 'dev', 'canary'];
 exports.MIN_PORT = 5554;
 exports.MAX_PORT = 5584;
-exports.PREVIEW_API_LEVELS = ['Tiramisu', 'UpsideDownCake', 'VanillaIceCream', 'Baklava'];
-function checkApiLevel(apiLevel) {
-    if (exports.PREVIEW_API_LEVELS.some((previewLevel) => apiLevel.startsWith(previewLevel)))
-        return;
-    if (isNaN(Number(apiLevel)) || !Number.isInteger(Number(apiLevel))) {
-        throw new Error(`Unexpected API level: '${apiLevel}'.`);
-    }
-    if (Number(apiLevel) < exports.MIN_API_LEVEL) {
-        throw new Error(`Minimum API level supported is ${exports.MIN_API_LEVEL}.`);
-    }
-}
-exports.checkApiLevel = checkApiLevel;
 function checkTarget(target) {
     if (!exports.VALID_TARGETS.includes(target)) {
         throw new Error(`Value for input.target '${target}' is unknown. Supported options: ${exports.VALID_TARGETS}.`);
diff --git a/lib/main.js b/lib/main.js
index 21a5d49..9bff6cb 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -63,8 +63,12 @@ function run() {
             }
             // API level of the platform and system image
             const apiLevel = core.getInput('api-level', { required: true });
-            (0, input_validator_1.checkApiLevel)(apiLevel);
             console.log(`API level: ${apiLevel}`);
+            let systemImageApiLevel = core.getInput('system-image-api-level');
+            if (!systemImageApiLevel) {
+                systemImageApiLevel = apiLevel;
+            }
+            console.log(`System image API level: ${systemImageApiLevel}`);
             // target of the system image
             const targetInput = core.getInput('target');
             const target = targetInput == 'playstore' ? 'google_apis_playstore' : targetInput;
@@ -179,7 +183,7 @@ function run() {
             }));
             console.log(`::endgroup::`);
             // install SDK
-            yield (0, sdk_installer_1.installAndroidSdk)(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
+            yield (0, sdk_installer_1.installAndroidSdk)(apiLevel, systemImageApiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
             // execute pre emulator launch script if set
             if (preEmulatorLaunchScripts !== undefined) {
                 console.log(`::group::Run pre emulator launch script`);
@@ -198,7 +202,7 @@ function run() {
                 console.log(`::endgroup::`);
             }
             // launch an emulator
-            yield (0, emulator_manager_1.launchEmulator)(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard);
+            yield (0, emulator_manager_1.launchEmulator)(systemImageApiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard);
             // execute the custom script
             try {
                 // move to custom working directory if set
diff --git a/lib/sdk-installer.js b/lib/sdk-installer.js
index fe7f513..237b67f 100644
--- a/lib/sdk-installer.js
+++ b/lib/sdk-installer.js
@@ -46,7 +46,7 @@ const CMDLINE_TOOLS_URL_LINUX = 'https://dl.google.com/android/repository/comman
  * Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator,
  * and the system image for the chosen API level, CPU arch, and target.
  */
-function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion) {
+function installAndroidSdk(apiLevel, systemImageApiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion) {
     return __awaiter(this, void 0, void 0, function* () {
         try {
             console.log(`::group::Install Android SDK`);
@@ -95,7 +95,7 @@ function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndk
                 yield io.rmRF('emulator.zip');
             }
             console.log('Installing system images.');
-            yield exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
+            yield exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${systemImageApiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
             if (ndkVersion) {
                 console.log(`Installing NDK ${ndkVersion}.`);
                 yield exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`);
diff --git a/src/emulator-manager.ts b/src/emulator-manager.ts
index aa2a67b..14684de 100644
--- a/src/emulator-manager.ts
+++ b/src/emulator-manager.ts
@@ -5,7 +5,7 @@ import * as fs from 'fs';
  * Creates and launches a new AVD instance with the specified configurations.
  */
 export async function launchEmulator(
-  apiLevel: string,
+  systemImageApiLevel: string,
   target: string,
   arch: string,
   profile: string,
@@ -33,7 +33,7 @@ export async function launchEmulator(
       const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : '';
       console.log(`Creating AVD.`);
       await exec.exec(
-        `sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`
+        `sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${systemImageApiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`
       );
     }
 
diff --git a/src/input-validator.ts b/src/input-validator.ts
index e5142cc..e887405 100644
--- a/src/input-validator.ts
+++ b/src/input-validator.ts
@@ -1,20 +1,22 @@
 export const MIN_API_LEVEL = 15;
-export const VALID_TARGETS: Array<string> = ['default', 'google_apis', 'aosp_atd', 'google_atd', 'google_apis_playstore', 'android-wear', 'android-wear-cn', 'android-tv', 'google-tv'];
+export const VALID_TARGETS: Array<string> = [
+  'default',
+  'google_apis',
+  'aosp_atd',
+  'google_atd',
+  'google_apis_playstore',
+  'android-wear',
+  'android-wear-cn',
+  'android-tv',
+  'google-tv',
+  'android-automotive',
+  'android-automotive-playstore',
+  'android-desktop',
+];
 export const VALID_ARCHS: Array<string> = ['x86', 'x86_64', 'arm64-v8a'];
 export const VALID_CHANNELS: Array<string> = ['stable', 'beta', 'dev', 'canary'];
 export const MIN_PORT = 5554;
 export const MAX_PORT = 5584;
-export const PREVIEW_API_LEVELS: Array<string> = ['Tiramisu', 'UpsideDownCake', 'VanillaIceCream', 'Baklava'];
-
-export function checkApiLevel(apiLevel: string): void {
-  if (PREVIEW_API_LEVELS.some((previewLevel) => apiLevel.startsWith(previewLevel))) return;
-  if (isNaN(Number(apiLevel)) || !Number.isInteger(Number(apiLevel))) {
-    throw new Error(`Unexpected API level: '${apiLevel}'.`);
-  }
-  if (Number(apiLevel) < MIN_API_LEVEL) {
-    throw new Error(`Minimum API level supported is ${MIN_API_LEVEL}.`);
-  }
-}
 
 export function checkTarget(target: string): void {
   if (!VALID_TARGETS.includes(target)) {
diff --git a/src/main.ts b/src/main.ts
index a958824..096f00a 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,7 +1,6 @@
 import * as core from '@actions/core';
 import { installAndroidSdk } from './sdk-installer';
 import {
-  checkApiLevel,
   checkTarget,
   checkArch,
   checkDisableAnimations,
@@ -44,9 +43,14 @@ async function run() {
 
     // API level of the platform and system image
     const apiLevel = core.getInput('api-level', { required: true });
-    checkApiLevel(apiLevel);
     console.log(`API level: ${apiLevel}`);
 
+    let systemImageApiLevel = core.getInput('system-image-api-level');
+    if (!systemImageApiLevel) {
+      systemImageApiLevel = apiLevel;
+    }
+    console.log(`System image API level: ${systemImageApiLevel}`);
+
     // target of the system image
     const targetInput = core.getInput('target');
     const target = targetInput == 'playstore' ? 'google_apis_playstore' : targetInput;
@@ -185,7 +189,7 @@ async function run() {
     console.log(`::endgroup::`);
 
     // install SDK
-    await installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
+    await installAndroidSdk(apiLevel, systemImageApiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
 
     // execute pre emulator launch script if set
     if (preEmulatorLaunchScripts !== undefined) {
@@ -206,7 +210,7 @@ async function run() {
 
     // launch an emulator
     await launchEmulator(
-      apiLevel,
+      systemImageApiLevel,
       target,
       arch,
       profile,
diff --git a/src/sdk-installer.ts b/src/sdk-installer.ts
index f87028a..b2e1baf 100644
--- a/src/sdk-installer.ts
+++ b/src/sdk-installer.ts
@@ -13,7 +13,16 @@ const CMDLINE_TOOLS_URL_LINUX = 'https://dl.google.com/android/repository/comman
  * Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator,
  * and the system image for the chosen API level, CPU arch, and target.
  */
-export async function installAndroidSdk(apiLevel: string, target: string, arch: string, channelId: number, emulatorBuild?: string, ndkVersion?: string, cmakeVersion?: string): Promise<void> {
+export async function installAndroidSdk(
+  apiLevel: string,
+  systemImageApiLevel: String,
+  target: string,
+  arch: string,
+  channelId: number,
+  emulatorBuild?: string,
+  ndkVersion?: string,
+  cmakeVersion?: string
+): Promise<void> {
   try {
     console.log(`::group::Install Android SDK`);
     const isOnMac = process.platform === 'darwin';
@@ -66,7 +75,7 @@ export async function installAndroidSdk(apiLevel: string, target: string, arch:
       await io.rmRF('emulator.zip');
     }
     console.log('Installing system images.');
-    await exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
+    await exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${systemImageApiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
 
     if (ndkVersion) {
       console.log(`Installing NDK ${ndkVersion}.`);


Test patch:
diff --git a/__tests__/input-validator.test.ts b/__tests__/input-validator.test.ts
index b08d5f5..91c4090 100644
--- a/__tests__/input-validator.test.ts
+++ b/__tests__/input-validator.test.ts
@@ -1,49 +1,6 @@
 import * as validator from '../src/input-validator';
 import { MAX_PORT, MIN_PORT } from '../src/input-validator';
 
-describe('api-level validator tests', () => {
-  it('Throws if api-level is not a number', () => {
-    const func = () => {
-      validator.checkApiLevel('api');
-    };
-    expect(func).toThrowError(`Unexpected API level: 'api'.`);
-  });
-
-  it('Throws if api-level is not an integer', () => {
-    const func = () => {
-      validator.checkApiLevel('29.1');
-    };
-    expect(func).toThrowError(`Unexpected API level: '29.1'.`);
-  });
-
-  it('Throws if api-level is lower than min API supported', () => {
-    const func = () => {
-      validator.checkApiLevel('14');
-    };
-    expect(func).toThrowError(`Minimum API level supported is ${validator.MIN_API_LEVEL}.`);
-  });
-
-  it('Validates successfully with valid api-level', () => {
-    const func1 = () => {
-      validator.checkApiLevel('15');
-    };
-    expect(func1).not.toThrow();
-
-    const func2 = () => {
-      validator.checkApiLevel('29');
-    };
-    expect(func2).not.toThrow();
-    const func3 = () => {
-      validator.checkApiLevel('UpsideDownCake-ext5');
-    };
-    expect(func3).not.toThrow();
-    const func4 = () => {
-      validator.checkApiLevel('TiramisuPrivacySandbox');
-    };
-    expect(func4).not.toThrow();
-  });
-});
-
 describe('target validator tests', () => {
   it('Throws if target is unknown', () => {
     const func = () => {
@@ -97,6 +54,21 @@ describe('target validator tests', () => {
       validator.checkTarget('google-tv');
     };
     expect(func9).not.toThrow();
+
+    const func10 = () => {
+      validator.checkTarget('android-automotive');
+    };
+    expect(func10).not.toThrow();
+
+    const func11 = () => {
+      validator.checkTarget('android-automotive-playstore');
+    };
+    expect(func11).not.toThrow();
+
+    const func12 = () => {
+      validator.checkTarget('android-desktop');
+    };
+    expect(func12).not.toThrow();
   });
 });