/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * 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
 *
 *      http://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
 */
package com.android.tools.build.bundletool.commands;

import static com.android.tools.build.bundletool.model.utils.ModuleDependenciesUtils.getModulesIncludingDependencies;
import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist;
import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndExecutable;
import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable;
import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.RESOURCES_REFERENCED_IN_MANIFEST_TO_MASTER_SPLIT;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;

import com.android.bundle.Commands.LocalTestingInfo;
import com.android.bundle.Config.BundleConfig;
import com.android.bundle.Config.Compression;
import com.android.bundle.Config.SuffixStripping;
import com.android.bundle.Devices.DeviceSpec;
import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode;
import com.android.tools.build.bundletool.device.AdbServer;
import com.android.tools.build.bundletool.device.ApkMatcher;
import com.android.tools.build.bundletool.device.DeviceAnalyzer;
import com.android.tools.build.bundletool.io.ApkSerializerManager;
import com.android.tools.build.bundletool.io.ApkSetBuilderFactory;
import com.android.tools.build.bundletool.io.ApkSetBuilderFactory.ApkSetBuilder;
import com.android.tools.build.bundletool.io.SplitApkSerializer;
import com.android.tools.build.bundletool.io.StandaloneApkSerializer;
import com.android.tools.build.bundletool.mergers.BundleModuleMerger;
import com.android.tools.build.bundletool.model.Aapt2Command;
import com.android.tools.build.bundletool.model.ApkListener;
import com.android.tools.build.bundletool.model.ApkModifier;
import com.android.tools.build.bundletool.model.AppBundle;
import com.android.tools.build.bundletool.model.BundleModule;
import com.android.tools.build.bundletool.model.BundleModule.ModuleDeliveryType;
import com.android.tools.build.bundletool.model.BundleModuleName;
import com.android.tools.build.bundletool.model.GeneratedApks;
import com.android.tools.build.bundletool.model.GeneratedAssetSlices;
import com.android.tools.build.bundletool.model.ModuleSplit;
import com.android.tools.build.bundletool.model.OptimizationDimension;
import com.android.tools.build.bundletool.model.SigningConfiguration;
import com.android.tools.build.bundletool.model.SourceStamp;
import com.android.tools.build.bundletool.model.exceptions.IncompatibleDeviceException;
import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException;
import com.android.tools.build.bundletool.model.targeting.AlternativeVariantTargetingPopulator;
import com.android.tools.build.bundletool.model.utils.SplitsXmlInjector;
import com.android.tools.build.bundletool.model.utils.Versions;
import com.android.tools.build.bundletool.model.utils.files.FileUtils;
import com.android.tools.build.bundletool.model.version.BundleToolVersion;
import com.android.tools.build.bundletool.model.version.Version;
import com.android.tools.build.bundletool.optimizations.ApkOptimizations;
import com.android.tools.build.bundletool.optimizations.OptimizationsMerger;
import com.android.tools.build.bundletool.preprocessors.AppBundle64BitNativeLibrariesPreprocessor;
import com.android.tools.build.bundletool.preprocessors.EmbeddedApkSigningPreprocessor;
import com.android.tools.build.bundletool.preprocessors.EntryCompressionPreprocessor;
import com.android.tools.build.bundletool.preprocessors.LocalTestingPreprocessor;
import com.android.tools.build.bundletool.splitters.ApkGenerationConfiguration;
import com.android.tools.build.bundletool.splitters.AssetSlicesGenerator;
import com.android.tools.build.bundletool.splitters.ResourceAnalyzer;
import com.android.tools.build.bundletool.splitters.ShardedApksGenerator;
import com.android.tools.build.bundletool.splitters.SplitApksGenerator;
import com.android.tools.build.bundletool.validation.AppBundleValidator;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.logging.Logger;
import java.util.stream.Stream;
import java.util.zip.ZipFile;
import javax.annotation.CheckReturnValue;

/** Executes the "build-apks" command. */
final class BuildApksManager {

  private static final Logger logger = Logger.getLogger(BuildApksManager.class.getName());

  private final BuildApksCommand command;
  private final Aapt2Command aapt2Command;
  private final Path tempDir;

  BuildApksManager(BuildApksCommand command, Aapt2Command aapt2Command, Path tempDir) {
    this.command = command;
    this.aapt2Command = aapt2Command;
    this.tempDir = tempDir;
  }

  public Path execute() {
    validateInput();

    Path outputDirectory =
        command.getCreateApkSetArchive()
            ? command.getOutputFile().getParent()
            : command.getOutputFile();
    if (outputDirectory != null && Files.notExists(outputDirectory)) {
      logger.info("Output directory '" + outputDirectory + "' does not exist, creating it.");
      FileUtils.createDirectories(outputDirectory);
    }

    // Fail fast with ADB before generating any APKs.
    Optional<DeviceSpec> deviceSpec = command.getDeviceSpec();
    if (command.getGenerateOnlyForConnectedDevice()) {
      deviceSpec = Optional.of(getDeviceSpecFromConnectedDevice());
    }

    try (ZipFile bundleZip = new ZipFile(command.getBundlePath().toFile())) {
      executeWithZip(bundleZip, deviceSpec, command.getSourceStamp());
    } catch (IOException e) {
      throw new UncheckedIOException(
          String.format(
              "An error occurred when processing the bundle '%s'.", command.getBundlePath()),
          e);
    } finally {
      if (command.isExecutorServiceCreatedByBundleTool()) {
        command.getExecutorService().shutdown();
      }
    }

    return command.getOutputFile();
  }

  private void executeWithZip(
      ZipFile bundleZip, Optional<DeviceSpec> deviceSpec, Optional<SourceStamp> sourceStamp)
      throws IOException {
    Optional<String> stampSource = sourceStamp.map(SourceStamp::getSource);
    AppBundleValidator bundleValidator = AppBundleValidator.create(command.getExtraValidators());

    bundleValidator.validateFile(bundleZip);
    AppBundle appBundle = AppBundle.buildFromZip(bundleZip);
    bundleValidator.validate(appBundle);

    appBundle = applyPreprocessors(appBundle);

    ImmutableSet<BundleModule> requestedModules =
        command.getModules().isEmpty()
            ? ImmutableSet.of()
            : getModulesIncludingDependencies(
                appBundle, getBundleModules(appBundle, command.getModules()));

    BundleConfig bundleConfig = appBundle.getBundleConfig();
    Version bundleVersion = BundleToolVersion.getVersionFromBundleConfig(bundleConfig);

    GeneratedApks.Builder generatedApksBuilder = GeneratedApks.builder();
    GeneratedAssetSlices.Builder generatedAssetSlices = GeneratedAssetSlices.builder();

    boolean enableUniversalAsFallbackForSplits = false;
    boolean enableInstallTimeNonRemovableModules = false;
    ApksToGenerate apksToGenerate =
        new ApksToGenerate(
            appBundle, command.getApkBuildMode(), enableUniversalAsFallbackForSplits, deviceSpec);

    // Split APKs
    if (apksToGenerate.generateSplitApks()) {
      AppBundle mergedAppBundle =
          BundleModuleMerger.mergeNonRemovableInstallTimeModules(
              appBundle, enableInstallTimeNonRemovableModules);
      bundleValidator.validate(mergedAppBundle);
      generatedApksBuilder.setSplitApks(generateSplitApks(mergedAppBundle, stampSource));
    }

    // Instant APKs
    if (apksToGenerate.generateInstantApks()) {
      generatedApksBuilder.setInstantApks(generateInstantApks(appBundle, stampSource));
    }

    // Standalone APKs
    if (apksToGenerate.generateStandaloneApks()) {
      generatedApksBuilder.setStandaloneApks(
          generateStandaloneApks(tempDir, appBundle, stampSource));
    }

    // Universal APK
    if (apksToGenerate.generateUniversalApk()) {
      // Note: Universal APK is a special type of standalone, with no optimization dimensions.
      ImmutableList<BundleModule> modulesToFuse =
          requestedModules.isEmpty()
              ? modulesToFuse(getModulesForStandaloneApks(appBundle))
              : requestedModules.asList();
      generatedApksBuilder.setStandaloneApks(
          new ShardedApksGenerator(
                  tempDir,
                  bundleVersion,
                  /* strip64BitLibrariesFromShards= */ false,
                  getSuffixStrippings(bundleConfig),
                  stampSource)
              .generateSplits(
                  modulesToFuse,
                  appBundle.getBundleMetadata(),
                  ApkOptimizations.getOptimizationsForUniversalApk()));
    }

    // System APKs
    if (apksToGenerate.generateSystemApks()) {
      generatedApksBuilder.setSystemApks(
          generateSystemApks(appBundle, deviceSpec, requestedModules));
    }

    // Asset Slices
    if (apksToGenerate.generateAssetSlices()) {
      generatedAssetSlices.setAssetSlices(generateAssetSlices(appBundle));
    }

    // Populate alternative targeting based on variant targeting of all APKs.
    GeneratedApks generatedApks =
        AlternativeVariantTargetingPopulator.populateAlternativeVariantTargeting(
            generatedApksBuilder.build(),
            appBundle.getBaseModule().getAndroidManifest().getMaxSdkVersion());

    SplitsXmlInjector splitsXmlInjector = new SplitsXmlInjector();
    generatedApks = splitsXmlInjector.process(generatedApks);

    if (deviceSpec.isPresent()) {
      // It is easier to fully check device compatibility once the splits have been generated (in
      // memory). Note that no costly I/O happened up until this point, so it's not too late for
      // this check.
      checkDeviceCompatibilityWithBundle(generatedApks, deviceSpec.get());
    }

    Optional<SigningConfiguration> stampSigningConfiguration =
        sourceStamp.map(SourceStamp::getSigningConfiguration);
    ApkSetBuilder apkSetBuilder =
        createApkSetBuilder(
            aapt2Command,
            command.getSigningConfiguration(),
            stampSigningConfiguration,
            bundleVersion,
            bundleConfig.getCompression(),
            tempDir);

    // Create variants and serialize APKs.
    ApkSerializerManager apkSerializerManager =
        new ApkSerializerManager(
            appBundle,
            apkSetBuilder,
            command.getExecutorService(),
            command.getApkListener().orElse(ApkListener.NO_OP),
            command.getApkModifier().orElse(ApkModifier.NO_OP),
            command.getFirstVariantNumber().orElse(0));

    apkSerializerManager.populateApkSetBuilder(
        generatedApks,
        generatedAssetSlices.build(),
        command.getApkBuildMode(),
        deviceSpec,
        getLocalTestingInfo(appBundle));

    if (command.getOverwriteOutput()) {
      Files.deleteIfExists(command.getOutputFile());
    }
    apkSetBuilder.writeTo(command.getOutputFile());
  }

  private ImmutableList<ModuleSplit> generateStandaloneApks(
      Path tempDir, AppBundle appBundle, Optional<String> stampSource) {
    ImmutableList<BundleModule> allModules = getModulesForStandaloneApks(appBundle);
    Version bundleVersion = Version.of(appBundle.getBundleConfig().getBundletool().getVersion());
    ShardedApksGenerator shardedApksGenerator =
        new ShardedApksGenerator(
            tempDir,
            bundleVersion,
            shouldStrip64BitLibrariesFromShards(appBundle),
            getSuffixStrippings(appBundle.getBundleConfig()),
            stampSource);
    return appBundle.isApex()
        ? shardedApksGenerator.generateApexSplits(modulesToFuse(allModules))
        : shardedApksGenerator.generateSplits(
            modulesToFuse(allModules),
            appBundle.getBundleMetadata(),
            getApkOptimizations(appBundle.getBundleConfig()));
  }

  private ImmutableList<ModuleSplit> generateAssetSlices(AppBundle appBundle) {
    ApkGenerationConfiguration assetSlicesGenerationConfiguration =
        getAssetSliceGenerationConfiguration(appBundle.getBundleConfig());
    AssetSlicesGenerator assetSlicesGenerator =
        new AssetSlicesGenerator(appBundle, assetSlicesGenerationConfiguration);
    return assetSlicesGenerator.generateAssetSlices();
  }

  private ImmutableList<ModuleSplit> generateInstantApks(
      AppBundle appBundle, Optional<String> stampSource) {
    Version bundleVersion = Version.of(appBundle.getBundleConfig().getBundletool().getVersion());
    ImmutableList<BundleModule> allFeatureModules = appBundle.getFeatureModules().values().asList();
    ImmutableList<BundleModule> instantModules =
        allFeatureModules.stream().filter(BundleModule::isInstantModule).collect(toImmutableList());
    ApkGenerationConfiguration instantApkGenerationConfiguration =
        getCommonSplitApkGenerationConfiguration(appBundle)
            .setForInstantAppVariants(true)
            // We can't enable this splitter for instant APKs, as currently they
            // only support one variant.
            .setEnableDexCompressionSplitter(false)
            .build();
    return new SplitApksGenerator(
            instantModules, bundleVersion, instantApkGenerationConfiguration, stampSource)
        .generateSplits();
  }

  private ImmutableList<ModuleSplit> generateSplitApks(
      AppBundle appBundle, Optional<String> stampSource) throws IOException {
    Version bundleVersion = Version.of(appBundle.getBundleConfig().getBundletool().getVersion());
    ApkGenerationConfiguration.Builder apkGenerationConfiguration =
        getCommonSplitApkGenerationConfiguration(appBundle);
    if (RESOURCES_REFERENCED_IN_MANIFEST_TO_MASTER_SPLIT.enabledForVersion(bundleVersion)) {
      // Make sure that resources reachable from the manifest of the base module will be
      // represented in the master split (by at least one config). This prevents the app
      // from crashing too soon (before reaching Application#onCreate), in case when only
      // the base master split is installed.
      apkGenerationConfiguration.setBaseManifestReachableResources(
          new ResourceAnalyzer(appBundle).findAllAppResourcesReachableFromBaseManifest());
    }

    ImmutableList<BundleModule> featureModules = appBundle.getFeatureModules().values().asList();
    return new SplitApksGenerator(
            featureModules, bundleVersion, apkGenerationConfiguration.build(), stampSource)
        .generateSplits();
  }

  private ImmutableList<ModuleSplit> generateSystemApks(
      AppBundle appBundle,
      Optional<DeviceSpec> deviceSpec,
      ImmutableSet<BundleModule> requestedModules) {
    Version bundleVersion = Version.of(appBundle.getBundleConfig().getBundletool().getVersion());
    ImmutableList<BundleModule> featureModules = appBundle.getFeatureModules().values().asList();
    ImmutableList<BundleModule> modulesToFuse =
        requestedModules.isEmpty() ? modulesToFuse(featureModules) : requestedModules.asList();
    return new ShardedApksGenerator(
            tempDir,
            bundleVersion,
            shouldStrip64BitLibrariesFromShards(appBundle),
            getSuffixStrippings(appBundle.getBundleConfig()))
        .generateSystemSplits(
            /* modules= */ featureModules,
            /* modulesToFuse= */ modulesToFuse.stream()
                .map(BundleModule::getName)
                .collect(toImmutableSet()),
            appBundle.getBundleMetadata(),
            getApkOptimizations(appBundle.getBundleConfig()),
            deviceSpec);
  }

  private static void checkDeviceCompatibilityWithBundle(
      GeneratedApks generatedApks, DeviceSpec deviceSpec) {
    ApkMatcher apkMatcher = new ApkMatcher(deviceSpec);
    generatedApks.getAllApksStream().forEach(apkMatcher::checkCompatibleWithApkTargeting);
  }

  private DeviceSpec getDeviceSpecFromConnectedDevice() {
    AdbServer adbServer = command.getAdbServer().get();
    adbServer.init(command.getAdbPath().get());

    return new DeviceAnalyzer(adbServer).getDeviceSpec(command.getDeviceId());
  }

  private ApkSetBuilder createApkSetBuilder(
      Aapt2Command aapt2Command,
      Optional<SigningConfiguration> signingConfiguration,
      Optional<SigningConfiguration> stampSigningConfiguration,
      Version bundleVersion,
      Compression compression,
      Path tempDir) {
    SplitApkSerializer splitApkSerializer =
        new SplitApkSerializer(
            aapt2Command,
            signingConfiguration,
            stampSigningConfiguration,
            bundleVersion,
            compression);
    StandaloneApkSerializer standaloneApkSerializer =
        new StandaloneApkSerializer(
            aapt2Command,
            signingConfiguration,
            stampSigningConfiguration,
            bundleVersion,
            compression);

    if (!command.getCreateApkSetArchive()) {
      return ApkSetBuilderFactory.createApkSetWithoutArchiveBuilder(
          splitApkSerializer, standaloneApkSerializer, command.getOutputFile());
    }
    return ApkSetBuilderFactory.createApkSetBuilder(
        splitApkSerializer, standaloneApkSerializer, tempDir);
  }

  private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfiguration(
      AppBundle appBundle) {
    BundleConfig bundleConfig = appBundle.getBundleConfig();
    Version bundleToolVersion = Version.of(bundleConfig.getBundletool().getVersion());

    ApkOptimizations apkOptimizations = getApkOptimizations(bundleConfig);

    ApkGenerationConfiguration.Builder apkGenerationConfiguration =
        ApkGenerationConfiguration.builder()
            .setOptimizationDimensions(apkOptimizations.getSplitDimensions());

    boolean enableNativeLibraryCompressionSplitter =
        apkOptimizations.getUncompressNativeLibraries();
    apkGenerationConfiguration.setEnableNativeLibraryCompressionSplitter(
        enableNativeLibraryCompressionSplitter);

    apkGenerationConfiguration.setInstallableOnExternalStorage(
        appBundle
            .getBaseModule()
            .getAndroidManifest()
            .getInstallLocationValue()
            .map(
                installLocation ->
                    installLocation.equals("auto") || installLocation.equals("preferExternal"))
            .orElse(false));

    apkGenerationConfiguration.setMasterPinnedResourceIds(appBundle.getMasterPinnedResourceIds());

    apkGenerationConfiguration.setMasterPinnedResourceNames(
        appBundle.getMasterPinnedResourceNames());

    apkGenerationConfiguration.setSuffixStrippings(getSuffixStrippings(bundleConfig));

    return apkGenerationConfiguration;
  }

  private static ApkGenerationConfiguration getAssetSliceGenerationConfiguration(
      BundleConfig bundleConfig) {
    ApkOptimizations apkOptimizations = ApkOptimizations.getOptimizationsForAssetSlices();

    return ApkGenerationConfiguration.builder()
        .setOptimizationDimensions(apkOptimizations.getSplitDimensions())
        .setSuffixStrippings(getSuffixStrippings(bundleConfig))
        .build();
  }

  private static ImmutableList<BundleModule> modulesToFuse(ImmutableList<BundleModule> modules) {
    return modules.stream().filter(BundleModule::isIncludedInFusing).collect(toImmutableList());
  }

  private static ImmutableMap<OptimizationDimension, SuffixStripping> getSuffixStrippings(
      BundleConfig bundleConfig) {
    return OptimizationsMerger.getSuffixStrippings(
        bundleConfig.getOptimizations().getSplitsConfig().getSplitDimensionList());
  }

  private ApkOptimizations getApkOptimizations(BundleConfig bundleConfig) {
    return new OptimizationsMerger()
        .mergeWithDefaults(bundleConfig, command.getOptimizationDimensions());
  }

  private void validateInput() {
    checkFileExistsAndReadable(command.getBundlePath());

    if (command.getCreateApkSetArchive()) {
      if (!command.getOverwriteOutput()) {
        checkFileDoesNotExist(command.getOutputFile());
      }
    }

    if (command.getGenerateOnlyForConnectedDevice()) {
      checkArgument(
          command.getAdbServer().isPresent(),
          "Property 'adbServer' is required when 'generateOnlyForConnectedDevice' is true.");
      checkArgument(
          command.getAdbPath().isPresent(),
          "Property 'adbPath' is required when 'generateOnlyForConnectedDevice' is true.");
      checkFileExistsAndExecutable(command.getAdbPath().get());
    }
  }

  private static boolean targetsOnlyPreL(AppBundle bundle) {
    Optional<Integer> maxSdkVersion =
        bundle.getBaseModule().getAndroidManifest().getMaxSdkVersion();
    return maxSdkVersion.isPresent() && maxSdkVersion.get() < Versions.ANDROID_L_API_VERSION;
  }

  private static boolean targetsPreL(AppBundle bundle) {
    int baseMinSdkVersion = bundle.getBaseModule().getAndroidManifest().getEffectiveMinSdkVersion();
    return baseMinSdkVersion < Versions.ANDROID_L_API_VERSION;
  }

  private static ImmutableList<BundleModule> getBundleModules(
      AppBundle appBundle, ImmutableSet<String> moduleNames) {
    return moduleNames.stream()
        .map(BundleModuleName::create)
        .map(appBundle::getModule)
        .collect(toImmutableList());
  }

  private static ImmutableList<BundleModule> getModulesForStandaloneApks(AppBundle appBundle) {
    return Stream.concat(
            appBundle.getFeatureModules().values().stream(),
            appBundle.getAssetModules().values().stream()
                .filter(
                    module ->
                        module.getDeliveryType().equals(ModuleDeliveryType.ALWAYS_INITIAL_INSTALL)))
        .collect(toImmutableList());
  }

  private static boolean shouldStrip64BitLibrariesFromShards(AppBundle appBundle) {
    return appBundle
        .getBundleConfig()
        .getOptimizations()
        .getStandaloneConfig()
        .getStrip64BitLibraries();
  }

  @CheckReturnValue
  private AppBundle applyPreprocessors(AppBundle bundle) {
    bundle =
        new AppBundle64BitNativeLibrariesPreprocessor(command.getOutputPrintStream())
            .preprocess(bundle);
    if (command.getLocalTestingMode()) {
      bundle = new LocalTestingPreprocessor().preprocess(bundle);
    }
    bundle = new EntryCompressionPreprocessor().preprocess(bundle);
    bundle = new EmbeddedApkSigningPreprocessor().preprocess(bundle);
    return bundle;
  }

  private static LocalTestingInfo getLocalTestingInfo(AppBundle bundle) {
    LocalTestingInfo.Builder localTestingInfo = LocalTestingInfo.newBuilder();
    bundle
        .getBaseModule()
        .getAndroidManifest()
        .getMetadataValue(LocalTestingPreprocessor.METADATA_NAME)
        .ifPresent(
            localTestingPath ->
                localTestingInfo.setEnabled(true).setLocalTestingPath(localTestingPath));
    return localTestingInfo.build();
  }

  private static class ApksToGenerate {
    private final AppBundle appBundle;
    private final ApkBuildMode apkBuildMode;
    private final boolean enableUniversalAsFallbackForSplits;
    private final Optional<DeviceSpec> deviceSpec;

    private ApksToGenerate(
        AppBundle appBundle,
        ApkBuildMode apkBuildMode,
        boolean enableUniversalAsFallbackForSplits,
        Optional<DeviceSpec> deviceSpec) {
      this.appBundle = appBundle;
      this.apkBuildMode = apkBuildMode;
      this.enableUniversalAsFallbackForSplits = enableUniversalAsFallbackForSplits;
      this.deviceSpec = deviceSpec;
      validate();
    }

    private void validate() {
      if (appBundle.isApex() && apkBuildMode.equals(ApkBuildMode.UNIVERSAL)) {
        throw InvalidCommandException.builder()
            .withInternalMessage("APEX bundles do not support universal apks.")
            .build();
      }

      if (deviceSpec.isPresent()) {
        int deviceSdk = deviceSpec.get().getSdkVersion();
        Optional<Integer> appMaxSdk =
            appBundle.getBaseModule().getAndroidManifest().getMaxSdkVersion();

        if ((apkBuildMode.equals(ApkBuildMode.DEFAULT)
            || apkBuildMode.equals(ApkBuildMode.PERSISTENT))) {
          if (deviceSdk >= Versions.ANDROID_L_API_VERSION) {
            if (!generateSplitApks()) {
              throw IncompatibleDeviceException.builder()
                  .withUserMessage(
                      "App Bundle targets pre-L devices, but the device has SDK version higher "
                          + "or equal to L.")
                  .build();
            }
          } else {
            if (!generateStandaloneApks()) {
              throw IncompatibleDeviceException.builder()
                  .withUserMessage(
                      "App Bundle targets L+ devices, but the device has SDK version lower than L.")
                  .build();
            }
          }
        }

        if (appMaxSdk.isPresent() && deviceSdk > appMaxSdk.get()) {
          throw IncompatibleDeviceException.builder()
              .withUserMessage(
                  "Max SDK version of the App Bundle is lower than SDK version of the device")
              .build();
        }
      }

      boolean generatesAtLeastOneApk =
          generateStandaloneApks()
              || generateSplitApks()
              || generateInstantApks()
              || generateUniversalApk()
              || generateSystemApks();
      if (!generatesAtLeastOneApk) {
        throw InvalidCommandException.builder().withInternalMessage("No APKs to generate.").build();
      }
    }

    public boolean generateSplitApks() {
      if (appBundle.isApex()) {
        return false;
      }
      if (!apkBuildMode.equals(ApkBuildMode.DEFAULT)
          && !apkBuildMode.equals(ApkBuildMode.PERSISTENT)) {
        return false;
      }
      return !targetsOnlyPreL(appBundle)
          && deviceSpec
              .map(spec -> spec.getSdkVersion() >= Versions.ANDROID_L_API_VERSION)
              .orElse(true);
    }

    public boolean generateStandaloneApks() {
      if (!apkBuildMode.equals(ApkBuildMode.DEFAULT)
          && !apkBuildMode.equals(ApkBuildMode.PERSISTENT)) {
        return false;
      }
      if (appBundle.isApex()) {
        return true;
      }
      return targetsPreL(appBundle)
          && deviceSpec
              .map(spec -> spec.getSdkVersion() < Versions.ANDROID_L_API_VERSION)
              .orElse(true);
    }

    public boolean generateInstantApks() {
      if (appBundle.isApex()) {
        return false;
      }
      return apkBuildMode.equals(ApkBuildMode.DEFAULT) || apkBuildMode.equals(ApkBuildMode.INSTANT);
    }

    public boolean generateUniversalApk() {
      if (appBundle.isApex()) {
        return false;
      }
      boolean shouldGenerateAsFallback =
          enableUniversalAsFallbackForSplits && generateSplitApks() && !generateStandaloneApks();
      return apkBuildMode.equals(ApkBuildMode.UNIVERSAL) || shouldGenerateAsFallback;
    }

    public boolean generateSystemApks() {
      if (appBundle.isApex()) {
        return false;
      }
      return apkBuildMode.isAnySystemMode();
    }

    public boolean generateAssetSlices() {
      if (appBundle.isApex()) {
        return false;
      }
      return apkBuildMode.equals(ApkBuildMode.DEFAULT)
          || apkBuildMode.equals(ApkBuildMode.INSTANT)
          || apkBuildMode.equals(ApkBuildMode.PERSISTENT);
    }
  }
}