/*
 * Copyright (C) 2020 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.tradefed.targetprep;

import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceUnresponsiveException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.device.SnapuserdWaitPhase;
import com.android.tradefed.device.TestDeviceState;
import com.android.tradefed.host.IHostOptions;
import com.android.tradefed.host.IHostOptions.PermitLimitType;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.TarUtil;
import com.android.tradefed.util.ZipUtil2;
import com.android.tradefed.util.image.DeviceImageTracker;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.io.PatternFilenameFilter;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * A target preparer that flash the device with android common kernel generic image. Please see
 * https://source.android.com/devices/architecture/kernel/android-common for details.
 */
@OptionClass(alias = "gki-device-flash-preparer")
public class GkiDeviceFlashPreparer extends BaseTargetPreparer implements ILabPreparer {

    private static final String AVBTOOL = "bin/avbtool";
    private static final String MKBOOTIMG = "bin/mkbootimg";
    private static final String BUILD_IMAGE = "bin/build_image";
    private static final String MKE2FS = "bin/mke2fs";
    private static final String MKUSERIMG_MKE2FS = "bin/mkuserimg_mke2fs";
    private static final String E2FSDROID = "bin/e2fsdroid";
    private static final String OTATOOLS_ZIP = "otatools.zip";
    private static final String KERNEL_IMAGE = "Image.gz";
    // Wait time for device state to stablize in millisecond
    private static final int STATE_STABLIZATION_WAIT_TIME = 60000;

    @Option(
            name = "device-boot-time",
            description = "max time to wait for device to boot. Set as 5 minutes by default",
            isTimeVal = true)
    private long mDeviceBootTime = 5 * 60 * 1000;

    @Option(
            name = "gki-boot-image-name",
            description = "The file name in BuildInfo that provides GKI boot image.")
    private String mGkiBootImageName = "gki_boot.img";

    @Option(
            name = "ramdisk-image-name",
            description = "The file name in BuildInfo that provides ramdisk image.")
    private String mRamdiskImageName = "ramdisk.img";

    @Option(
            name = "initramfs-image-name",
            description = "The file name in BuildInfo that provides initramfs image.")
    private String mInitramfsImageName = "initramfs.img";

    @Option(
            name = "vendor-boot-image-name",
            description = "The file name in BuildInfo that provides vendor boot image.")
    private String mVendorBootImageName = "vendor_boot.img";

    @Option(
            name = "vendor-kernel-boot-image-name",
            description = "The file name in BuildInfo that provides vendor kernel boot image.")
    private String mVendorKernelBootImageName = "vendor_kernel_boot.img";

    @Option(
            name = "dtbo-image-name",
            description = "The file name in BuildInfo that provides dtbo image.")
    private String mDtboImageName = "dtbo.img";

    @Option(
            name = "vendor-dlkm-image-name",
            description = "The file name in BuildInfo that provides vendor_dlkm image.")
    private String mVendorDlkmImageName = "vendor_dlkm.img";

    @Option(
            name = "system-dlkm-image-name",
            description = "The file name in BuildInfo that provides system_dlkm image.")
    private String mSystemDlkmImageName = "system_dlkm.img";

    @Option(
            name = "system-dlkm-archive-name",
            description =
                    "The file name in BuildInfo that provides system_dlkm_staging_archive.tar.gz.")
    private String mSystemDlkmArchiveName = "system_dlkm_staging_archive.tar.gz";

    @Option(
            name = "boot-image-file-name",
            description =
                    "The boot image file name to search for if gki-boot-image-name in "
                            + "BuildInfo is a zip file or directory, for example boot-5.4-gz.img.")
    private String mBootImageFileName = "boot(.*).img";

    @Option(
            name = "vendor-boot-image-file-name",
            description =
                    "The vendor boot image file name to search for if vendor-boot-image-name in "
                            + "BuildInfo is a zip file or directory, for example vendor_boot.img.")
    private String mVendorBootImageFileName = "vendor_boot.img";

    @Option(
            name = "vendor-kernel-boot-image-file-name",
            description =
                    "The vendor kernel boot image file name to search for if "
                            + "vendor-kernel-boot-image-name in BuildInfo is a zip file or "
                            + "directory, for example vendor_kernel_boot.img.")
    private String mVendorKernelBootImageFileName = "vendor_kernel_boot.img";

    @Option(
            name = "dtbo-image-file-name",
            description =
                    "The dtbo image file name to search for if dtbo-image-name in "
                            + "BuildInfo is a zip file or directory, for example dtbo.img.")
    private String mDtboImageFileName = "dtbo.img";

    @Option(
            name = "vendor-dlkm-image-file-name",
            description =
                    "The vendor_dlkm image file name to search for if vendor-dlkm-image-name in "
                            + "BuildInfo is a zip file or directory, for example vendor_dlkm.img.")
    private String mVendorDlkmImageFileName = "vendor_dlkm.img";

    @Option(
            name = "system-dlkm-image-file-name",
            description =
                    "The system_dlkm image file name to search for if system-dlkm-image-name in "
                            + "BuildInfo is a zip file or directory, for example system_dlkm.img.")
    private String mSystemDlkmImageFileName = "system_dlkm.img";

    @Option(
            name = "post-reboot-device-into-user-space",
            description = "whether to boot the device in user space after flash.")
    private boolean mPostRebootDeviceIntoUserSpace = true;

    @Option(
            name = "wipe-device-before-gki-flash",
            description = "Whether to wipe device before GKI boot image flash.")
    private boolean mShouldWipeDeviceBeforeFlash = true;

    @Deprecated
    @Option(
            name = "wipe-device-after-gki-flash",
            description = "deprecated, use option wipe-device-before-gki-flash instead.")
    private boolean mShouldWipeDevice = false;

    @Option(name = "disable-verity", description = "Whether to disable-verity.")
    private boolean mShouldDisableVerity = false;

    @Option(name = "oem-disable-verity", description = "Whether to run oem disable-verity.")
    private boolean mShouldDisableOemVerity = false;

    @Option(
            name = "fastboot-flash-option",
            description = "additional options to pass with fastboot flash command.")
    private Collection<String> mFastbootFlashOptions = new ArrayList<>();

    @Option(
            name = "boot-header-version",
            description = "The version of the boot.img header. Set to 3 by default.")
    private int mBootHeaderVersion = 3;

    @Option(
            name = "add-hash-footer",
            description =
                    "Add hash footer to GKI boot image. More info at "
                        + "https://android.googlesource.com/platform/external/avb/+/master/README.md")
    private boolean mAddHashFooter = false;

    private File mBootImg = null;
    private File mSystemDlkmImg = null;
    private Collection<String> mFlashOptions = new ArrayList<>();

    /** {@inheritDoc} */
    @Override
    public void setUp(TestInformation testInfo)
            throws TargetSetupError, BuildError, DeviceNotAvailableException {
        // If we use the GKI preparer invalidate baseline
        DeviceImageTracker.getDefaultCache()
                .invalidateTracking(testInfo.getDevice().getSerialNumber());
        ITestDevice device = testInfo.getDevice();
        IBuildInfo buildInfo = testInfo.getBuildInfo();

        mFlashOptions =
                mFastbootFlashOptions.stream().map(String::trim).collect(Collectors.toList());
        File tmpDir = null;
        try {
            tmpDir = FileUtil.createTempDir("gki_preparer");
            validateGkiBootImg(device, buildInfo, tmpDir);
            if (mAddHashFooter) {
                addHashFooter(device, buildInfo, tmpDir);
            }
            buildGkiSystemDlkmImg(device, buildInfo, tmpDir);
            flashGki(device, buildInfo, tmpDir);
        } catch (IOException ioe) {
            throw new TargetSetupError(ioe.getMessage(), ioe, device.getDeviceDescriptor());
        } finally {
            FileUtil.recursiveDelete(tmpDir);
        }

        if (!mPostRebootDeviceIntoUserSpace) {
            return;
        }
        // Wait some time after flashing the image.
        getRunUtil().sleep(STATE_STABLIZATION_WAIT_TIME);
        device.rebootUntilOnline();
        if (device.enableAdbRoot()) {
            device.setDate(null);
        }
        try {
            device.setRecoveryMode(RecoveryMode.AVAILABLE);
            device.waitForDeviceAvailable(mDeviceBootTime);
        } catch (DeviceUnresponsiveException e) {
            // assume this is a build problem
            throw new DeviceFailedToBootError(
                    String.format(
                            "Device %s did not become available after flashing GKI. Exception: %s",
                            device.getSerialNumber(), e),
                    device.getDeviceDescriptor(),
                    DeviceErrorIdentifier.ERROR_AFTER_FLASHING);
        }
        device.postBootSetup();
        CLog.i("Device update completed on %s", device.getDeviceDescriptor());
    }

    /**
     * Get a reference to the {@link IHostOptions}
     *
     * @return the {@link IHostOptions} to use
     */
    @VisibleForTesting
    protected IHostOptions getHostOptions() {
        return GlobalConfiguration.getInstance().getHostOptions();
    }

    /**
     * Get the {@link IRunUtil} instance to use.
     *
     * @return the {@link IRunUtil} to use
     */
    @VisibleForTesting
    protected IRunUtil getRunUtil() {
        return RunUtil.getDefault();
    }

    /**
     * Flash GKI images.
     *
     * @param device the {@link ITestDevice}
     * @param buildInfo the {@link IBuildInfo} the build info
     * @param tmpDir the temporary directory {@link File}
     * @throws TargetSetupError, DeviceNotAvailableException, IOException
     */
    private void flashGki(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
            throws TargetSetupError, DeviceNotAvailableException {
        if (mShouldDisableVerity) {
            device.enableAdbRoot();
            device.executeAdbCommand("disable-verity");
            device.reboot();
        }
        device.rebootIntoBootloader();
        if (mShouldDisableOemVerity) {
            executeFastbootCmd(device, "oem disable-verity");
        }
        long start = System.currentTimeMillis();
        getHostOptions().takePermit(PermitLimitType.CONCURRENT_FLASHER);
        // Ensure snapuserd isn't running
        device.waitForSnapuserd(SnapuserdWaitPhase.BLOCK_BEFORE_RELEASING);
        CLog.v(
                "Flashing permit obtained after %ds",
                TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - start)));
        // Don't allow interruptions during flashing operations.
        getRunUtil().allowInterrupt(false);
        try {
            if (mShouldWipeDeviceBeforeFlash) {
                executeFastbootCmd(device, "-w");
            }

            if (buildInfo.getFile(mVendorBootImageName) != null) {
                File vendorBootImg =
                        getRequestedFile(
                                device,
                                mVendorBootImageFileName,
                                buildInfo.getFile(mVendorBootImageName),
                                tmpDir);
                executeFastbootCmd(device, "flash", "vendor_boot", vendorBootImg.getAbsolutePath());
            }
            if (buildInfo.getFile(mVendorKernelBootImageName) != null) {
                File vendorKernelBootImg =
                        getRequestedFile(
                                device,
                                mVendorKernelBootImageFileName,
                                buildInfo.getFile(mVendorKernelBootImageName),
                                tmpDir);
                executeFastbootCmd(
                        device,
                        "flash",
                        "vendor_kernel_boot",
                        vendorKernelBootImg.getAbsolutePath());
            }
            if (buildInfo.getFile(mInitramfsImageName) != null) {
                File initramfsImg =
                        getRequestedFile(
                                device,
                                mInitramfsImageName,
                                buildInfo.getFile(mInitramfsImageName),
                                tmpDir);
                executeFastbootCmd(
                        device, "flash", "vendor_boot:dlkm", initramfsImg.getAbsolutePath());
            }
            if (buildInfo.getFile(mDtboImageName) != null) {
                File dtboImg =
                        getRequestedFile(
                                device,
                                mDtboImageFileName,
                                buildInfo.getFile(mDtboImageName),
                                tmpDir);
                executeFastbootCmd(device, "flash", "dtbo", dtboImg.getAbsolutePath());
            }

            executeFastbootCmd(device, "flash", "boot", mBootImg.getAbsolutePath());

            if (buildInfo.getFile(mVendorDlkmImageName) != null) {
                File vendorDlkmImg =
                        getRequestedFile(
                                device,
                                mVendorDlkmImageFileName,
                                buildInfo.getFile(mVendorDlkmImageName),
                                tmpDir);
                if (!TestDeviceState.FASTBOOTD.equals(device.getDeviceState())) {
                    device.rebootIntoFastbootd();
                }
                executeFastbootCmd(device, "flash", "vendor_dlkm", vendorDlkmImg.getAbsolutePath());
            }

            if (buildInfo.getFile(mSystemDlkmImageName) != null) {
                File systemDlkmImg =
                        getRequestedFile(
                                device,
                                mSystemDlkmImageFileName,
                                buildInfo.getFile(mSystemDlkmImageName),
                                tmpDir);
                if (!TestDeviceState.FASTBOOTD.equals(device.getDeviceState())) {
                    device.rebootIntoFastbootd();
                }
                executeFastbootCmd(device, "flash", "system_dlkm", systemDlkmImg.getAbsolutePath());
            }

        } finally {
            getHostOptions().returnPermit(PermitLimitType.CONCURRENT_FLASHER);
            // Allow interruption at the end no matter what.
            getRunUtil().allowInterrupt(true);
            CLog.v(
                    "Flashing permit returned after %ds",
                    TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - start)));
        }
    }

    /**
     * Validate GKI boot image is expected. (Obsoleted. Please call with tmpDir provided)
     *
     * @param device the {@link ITestDevice}
     * @param buildInfo the {@link IBuildInfo} the build info
     * @throws TargetSetupError if there is no valid gki boot.img
     */
    public void validateGkiBootImg(ITestDevice device, IBuildInfo buildInfo)
            throws TargetSetupError {
        throw new TargetSetupError(
                "Obsoleted. Please use validateGkiBootImg(ITestDevice, IBuildInfo, File)",
                device.getDeviceDescriptor());
    }

    /**
     * Validate GKI boot image is expected. Throw exception if there is no valid boot.img.
     *
     * @param device the {@link ITestDevice}
     * @param buildInfo the {@link IBuildInfo} the build info
     * @param tmpDir the temporary directory {@link File}
     * @throws TargetSetupError if there is no valid gki boot.img
     */
    @VisibleForTesting
    protected void validateGkiBootImg(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
            throws TargetSetupError {
        if (buildInfo.getFile(mGkiBootImageName) != null && mBootImageFileName != null) {
            mBootImg =
                    getRequestedFile(
                            device,
                            mBootImageFileName,
                            buildInfo.getFile(mGkiBootImageName),
                            tmpDir);
            return;
        }
        if (buildInfo.getFile(KERNEL_IMAGE) == null) {
            throw new TargetSetupError(
                    KERNEL_IMAGE + " is not provided. Can not generate GKI boot.img.",
                    device.getDeviceDescriptor());
        }
        if (buildInfo.getFile(mRamdiskImageName) == null) {
            throw new TargetSetupError(
                    mRamdiskImageName + " is not provided. Can not generate GKI boot.img.",
                    device.getDeviceDescriptor());
        }
        if (buildInfo.getFile(OTATOOLS_ZIP) == null) {
            throw new TargetSetupError(
                    OTATOOLS_ZIP + " is not provided. Can not generate GKI boot.img.",
                    device.getDeviceDescriptor());
        }
        try {
            File mkbootimg =
                    getRequestedFile(device, MKBOOTIMG, buildInfo.getFile(OTATOOLS_ZIP), tmpDir);
            mkbootimg.setExecutable(true, false);
            mBootImg = FileUtil.createTempFile("boot", ".img", tmpDir);
            String cmd =
                    String.format(
                            "%s --kernel %s --header_version %d --base 0x00000000 "
                                    + "--pagesize 4096 --ramdisk %s -o %s",
                            mkbootimg.getAbsolutePath(),
                            buildInfo.getFile(KERNEL_IMAGE),
                            mBootHeaderVersion,
                            buildInfo.getFile(mRamdiskImageName),
                            mBootImg.getAbsolutePath());
            executeHostCommand(device, cmd);
            CLog.i("The GKI boot.img is of size %d", mBootImg.length());
            if (mBootImg.length() == 0) {
                throw new TargetSetupError(
                        "The mkbootimg tool didn't generate a valid boot.img.",
                        device.getDeviceDescriptor());
            }
            buildInfo.setFile(mGkiBootImageName, mBootImg, "0");
        } catch (IOException e) {
            throw new TargetSetupError(
                    "Fail to generate GKI boot.img.", e, device.getDeviceDescriptor());
        }
    }

    /**
     * Extracts the system_dlkm tar gzip file into the system_dlkm_staging folder. This function is
     * a wrapper around {@link TarUtil.extractTarGzipToTemp} in order to stub out the untarring for
     * unit testing.
     *
     * @param systemDlkmArchive the system_dlkm tar gzip file containing GKI modules.
     * @return File containing the system_dlkm tar gzip contents.
     * @throws IOException
     */
    @VisibleForTesting
    protected File extractSystemDlkmTarGzip(File systemDlkmArchive) throws IOException {
        return TarUtil.extractTarGzipToTemp(systemDlkmArchive, "system_dlkm_staging");
    }

    /**
     * Flatten the system_dlkm staging directory so that all the kernel modules are directly under
     * /lib/modules. This is necessary to match the expected system_dlkm file layout for platform
     * builds.
     *
     * @param device the {@link ITestDevice}
     * @param systemDlkmStagingDir the system_dlkm staging directory {@link File}
     * @throws IOException or TargetSetupError if there is an error flattening the system_dlkm.
     */
    @VisibleForTesting
    protected void flattenSystemDlkm(ITestDevice device, File systemDlkmStagingDir)
            throws IOException, TargetSetupError {
        File systemStagingLibModulesDir = new File(systemDlkmStagingDir, "lib/modules");

        // Move all modules from the kernel directory to /lib/modules
        Path libModulesPath = systemStagingLibModulesDir.toPath();
        File[] libModulesVersionFiles = systemStagingLibModulesDir.listFiles();
        File libModulesVersionDir = null;
        if (libModulesVersionFiles.length == 1) {
            // Move all the files under the kernel version folder to be
            // under lib/modules.
            libModulesVersionDir = libModulesVersionFiles[0];
            for (File file : libModulesVersionDir.listFiles()) {
                if (file.isFile()) {
                    File hardLink = new File(systemStagingLibModulesDir, file.getName());
                    try {
                        FileUtil.hardlinkFile(file, hardLink, true);
                    } catch (IOException e) {
                        throw new TargetSetupError(
                                String.format(
                                        "Failed to create hardlink of %s to %s",
                                        file.toString(), hardLink.toString()),
                                device.getDeviceDescriptor());
                    }
                }
            }
        }

        Path libModulesKernel =
                new File(
                                libModulesVersionDir != null
                                        ? libModulesVersionDir
                                        : systemStagingLibModulesDir,
                                "kernel")
                        .toPath();
        try (Stream<Path> allPaths = Files.walk(libModulesKernel)) {
            Path[] modulePaths =
                    allPaths.filter(path -> path.toString().endsWith(".ko")).toArray(Path[]::new);
            for (Path path : modulePaths) {
                File hardLink = new File(systemStagingLibModulesDir, path.toFile().getName());
                try {
                    FileUtil.hardlinkFile(path.toFile(), hardLink, true);
                } catch (IOException e) {
                    throw new TargetSetupError(
                            String.format(
                                    "Failed to create a hardlink of %s to %s",
                                    path.toString(), hardLink.toString()),
                            device.getDeviceDescriptor());
                }
            }
        } catch (NoSuchFileException e) {
            // Not a problem. Just means there's either no modules or the
            // tarball is already flat.
            CLog.i("Didn't find a kernel directory under lib/modules");
        }
        if (libModulesVersionDir != null) {
            FileUtil.recursiveDelete(libModulesVersionDir);
        } else if (libModulesKernel != null) {
            FileUtil.recursiveDelete(libModulesKernel.toFile());
        }

        // Remove modules.*.bin and modules.order. These aren't used or
        // included in the platform system_dlkm image.
        File[] files =
                libModulesPath.toFile().listFiles(new PatternFilenameFilter("modules\\..*\\.bin"));
        for (File f : files) {
            Files.deleteIfExists(f.toPath());
        }
        Files.deleteIfExists(libModulesPath.resolve("modules.order"));

        File[] depmodFiles =
                libModulesPath.toFile().listFiles(new PatternFilenameFilter("modules\\..*"));

        // Update the depmod files that reference the kernel modules to use the
        // new path.
        for (File f : depmodFiles) {
            String contents = FileUtil.readStringFromFile(f);
            contents =
                    Pattern.compile("kernel[^: \n\t]*/([^: \n\t]+\\.ko)")
                            .matcher(contents)
                            .replaceAll("$1");
            FileUtil.writeToFile(contents, f);
        }
    }

    /**
     * Build GKI system_dlkm image if the system_dlkm archive is provided.
     *
     * @param device the {@link ITestDevice}
     * @param buildInfo the {@link IBuildInfo} the build info
     * @param tmpDir the temporary directory {@link File}
     * @throws TargetSetupError if there is an error building the image file.
     */
    @VisibleForTesting
    protected void buildGkiSystemDlkmImg(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
            throws TargetSetupError {
        File systemDlkmStagingDir = null;

        if (buildInfo.getFile(mSystemDlkmArchiveName) == null) {
            /* Nothing to do here */
            return;
        }

        File systemDlkmArchive =
                getRequestedFile(
                        device,
                        mSystemDlkmArchiveName,
                        buildInfo.getFile(mSystemDlkmArchiveName),
                        tmpDir);
        if (systemDlkmArchive == null) {
            throw new TargetSetupError(
                    mSystemDlkmArchiveName
                            + " is not provided. Can not generate GKI system_dlkm.img.",
                    device.getDeviceDescriptor());
        }

        if (buildInfo.getFile(OTATOOLS_ZIP) == null) {
            throw new TargetSetupError(
                    OTATOOLS_ZIP + " is not provided. Can not generate GKI system_dlkm.img.",
                    device.getDeviceDescriptor());
        }

        File build_image =
                getRequestedFile(device, BUILD_IMAGE, buildInfo.getFile(OTATOOLS_ZIP), tmpDir);
        // Get build_image dependencies
        File mkuserimg_mke2fs =
                getRequestedFile(device, MKUSERIMG_MKE2FS, buildInfo.getFile(OTATOOLS_ZIP), tmpDir);
        File mke2fs = getRequestedFile(device, MKE2FS, buildInfo.getFile(OTATOOLS_ZIP), tmpDir);
        File e2fsdroid =
                getRequestedFile(device, E2FSDROID, buildInfo.getFile(OTATOOLS_ZIP), tmpDir);
        build_image.setExecutable(true, false);
        mkuserimg_mke2fs.setExecutable(true, false);
        mke2fs.setExecutable(true, false);
        e2fsdroid.setExecutable(true, false);

        try {
            systemDlkmStagingDir = extractSystemDlkmTarGzip(systemDlkmArchive);
            flattenSystemDlkm(device, systemDlkmStagingDir);

            // Create temporary files for the system_dlkm properties and file contexts
            File systemDlkmPropsFile = new File(tmpDir, "system_dlkm.props");
            File systemDlkmFileContexts = new File(tmpDir, "system_dlkm_file_contexts");

            // These are defaults GKI uses. We might want to pull this file from
            // a device build if devices require different properties.
            PrintWriter systemDlkmFileContextsWriter =
                    new PrintWriter(new FileWriter(systemDlkmFileContexts));
            systemDlkmFileContextsWriter.println(
                    "/system_dlkm(/.*)? u:object_r:system_dlkm_file:s0");
            systemDlkmFileContextsWriter.close();

            PrintWriter systemDlkmPropsPrintWriter =
                    new PrintWriter(new FileWriter(systemDlkmPropsFile));
            systemDlkmPropsPrintWriter.println("fs_type=ext4");
            systemDlkmPropsPrintWriter.println("use_dynamic_partition_size=true");
            systemDlkmPropsPrintWriter.println("ext_mkuserimg=mkuserimg_mke2fs");
            systemDlkmPropsPrintWriter.println("ext4_share_dup_blocks=true");
            systemDlkmPropsPrintWriter.println("extfs_rsv_pct=0");
            systemDlkmPropsPrintWriter.println("journal_size=0");
            systemDlkmPropsPrintWriter.println("mount_point=system_dlkm");
            systemDlkmPropsPrintWriter.println(
                    String.format("selinux_fc=%s", systemDlkmFileContexts.getAbsolutePath()));
            systemDlkmPropsPrintWriter.close();

            mSystemDlkmImg = new File(tmpDir, "system_dlkm.img");
            String buildImageCmd =
                    String.format(
                            "%s %s %s %s /dev/null",
                            build_image.getAbsolutePath(),
                            systemDlkmStagingDir.getAbsolutePath(),
                            systemDlkmPropsFile.getAbsolutePath(),
                            mSystemDlkmImg.getAbsolutePath());
            executeHostCommand(device, buildImageCmd);
            CLog.i("The GKI system_dlkm.img is of size %d", mSystemDlkmImg.length());
            if (mSystemDlkmImg.length() == 0) {
                throw new TargetSetupError(
                        "The build_image tool didn't generate a valid system_dlkm.img. (size=0)",
                        device.getDeviceDescriptor());
            }
            buildInfo.setFile(mSystemDlkmImageName, mSystemDlkmImg, "0");
        } catch (IOException e) {
            throw new TargetSetupError(
                    "Failed to generate GKI system_dlkm.img.", e, device.getDeviceDescriptor());
        } finally {
            // Clean up the system dlkm staging dir
            FileUtil.recursiveDelete(systemDlkmStagingDir);
        }

        File avbtool = getRequestedFile(device, AVBTOOL, buildInfo.getFile(OTATOOLS_ZIP), tmpDir);
        avbtool.setExecutable(true, false);
        String cmd =
                String.format(
                        "%s add_hashtree_footer --do_not_generate_fec "
                                + "--hash_algorithm sha256 "
                                + "--image %s "
                                + "--partition_name system_dlkm",
                        avbtool.getAbsolutePath(), mSystemDlkmImg.getAbsolutePath());
        executeHostCommand(device, cmd);
    }

    /**
     * Validate GKI boot image is expected. Throw exception if there is no valid boot.img.
     *
     * @param device the {@link ITestDevice}
     * @param buildInfo the {@link IBuildInfo} the build info
     * @param tmpDir the temporary directory {@link File}
     * @throws TargetSetupError if there is no valid gki boot.img
     */
    @VisibleForTesting
    protected void addHashFooter(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
            throws TargetSetupError, DeviceNotAvailableException {
        if (mBootImg == null) {
            throw new TargetSetupError(
                    mGkiBootImageName + " is not provided. Can not add hash footer to it.",
                    device.getDeviceDescriptor());
        }
        if (buildInfo.getFile(OTATOOLS_ZIP) == null) {
            throw new TargetSetupError(
                    OTATOOLS_ZIP + " is not provided. Can not add hash footer to GKI boot.img.",
                    device.getDeviceDescriptor());
        }
        File avbtool = getRequestedFile(device, AVBTOOL, buildInfo.getFile(OTATOOLS_ZIP), tmpDir);
        avbtool.setExecutable(true, false);

        String android_version = device.getProperty("ro.build.version.release");
        if (Strings.isNullOrEmpty(android_version)) {
            throw new TargetSetupError(
                    "Can not get android version from property ro.build.version.release.",
                    device.getDeviceDescriptor());
        }
        String security_path_version = device.getProperty("ro.build.version.security_patch");
        if (Strings.isNullOrEmpty(security_path_version)) {
            throw new TargetSetupError(
                    "Can not get security path version from property"
                            + " ro.build.version.security_patch.",
                    device.getDeviceDescriptor());
        }

        String command = String.format("du -b %s", mBootImg.getAbsolutePath());
        CommandResult cmdResult = executeHostCommand(device, command);
        String partition_size = cmdResult.getStdout().split("\\s+")[0];
        CLog.i("Boot image partition size: %s", partition_size);
        String cmd =
                String.format(
                        "%s add_hash_footer --image %s --partition_size %s "
                                + "--partition_name boot "
                                + "--prop com.android.build.boot.os_version:%s "
                                + "--prop com.android.build.boot.security_patch:%s",
                        avbtool.getAbsolutePath(),
                        mBootImg.getAbsolutePath(),
                        partition_size,
                        android_version,
                        security_path_version);
        executeHostCommand(device, cmd);
    }

    /**
     * Helper method to execute host command.
     *
     * @param device the {@link ITestDevice}
     * @param command the command string
     * @return the CommandResult
     * @throws TargetSetupError, DeviceNotAvailableException
     */
    private CommandResult executeHostCommand(ITestDevice device, final String command)
            throws TargetSetupError {
        final CommandResult result = getRunUtil().runTimedCmd(300000L, command.split("\\s+"));
        switch (result.getStatus()) {
            case SUCCESS:
                CLog.i(
                        "Command %s finished successfully, stdout = [%s].",
                        command, result.getStdout().trim());
                break;
            case FAILED:
                throw new TargetSetupError(
                        String.format(
                                "Command %s failed, stdout = [%s], stderr = [%s].",
                                command, result.getStdout().trim(), result.getStderr().trim()),
                        device.getDeviceDescriptor());
            case TIMED_OUT:
                throw new TargetSetupError(
                        String.format("Command %s timed out.", command),
                        device.getDeviceDescriptor());
            case EXCEPTION:
                throw new TargetSetupError(
                        String.format("Exception occurred when running command %s.", command),
                        device.getDeviceDescriptor());
        }
        return result;
    }

    /**
     * Get the requested file from the source file (zip or folder) by requested file name.
     *
     * <p>The provided source file can be a zip file. The method will unzip it to tempary directory
     * and find the requested file by the provided file name.
     *
     * <p>The provided source file can be a file folder. The method will find the requestd file by
     * the provided file name.
     *
     * @param device the {@link ITestDevice}
     * @param requestedFileName the requeste file name String
     * @param sourceFile the source file
     * @return the file that is specified by the requested file name
     * @throws TargetSetupError
     */
    @VisibleForTesting
    protected File getRequestedFile(
            ITestDevice device, String requestedFileName, File sourceFile, File tmpDir)
            throws TargetSetupError {
        File requestedFile = null;
        String baseFileName = new File(requestedFileName).getName();
        String subdirPathName = new File(requestedFileName).getParent();

        if (sourceFile.getName().endsWith(".zip")) {
            try (ZipFile sourceZipFile = new ZipFile(sourceFile)) {
                File destDir =
                        FileUtil.createNamedTempDir(
                                tmpDir, FileUtil.getBaseName(sourceFile.getName()) + "_zip");
                File subdir = null;
                if (subdirPathName != null && !subdirPathName.isEmpty()) {
                    subdir = FileUtil.createNamedTempDir(destDir, subdirPathName);
                }
                requestedFile = new File(subdir != null ? subdir : destDir, baseFileName);
                ZipUtil2.extractFileFromZip(sourceZipFile, requestedFileName, requestedFile);
                if (!requestedFile.exists()) {
                    /* Let's search for the file within the zip archive in case of a regex
                     * filename before giving up. */
                    final Enumeration<ZipArchiveEntry> entries = sourceZipFile.getEntries();
                    while (entries.hasMoreElements()) {
                        final ZipArchiveEntry entry = entries.nextElement();
                        if (entry.isDirectory() || !entry.getName().matches(requestedFileName)) {
                            continue;
                        }
                        requestedFile =
                                new File(subdir != null ? subdir : destDir, entry.getName());
                        FileUtil.writeToFile(sourceZipFile.getInputStream(entry), requestedFile);
                        break;
                    }
                }
            } catch (IOException e) {
                throw new TargetSetupError(
                        String.format("Fail to get %s from %s", requestedFileName, sourceFile),
                        e,
                        device.getDeviceDescriptor());
            }
        } else if (sourceFile.isDirectory()) {
            requestedFile = FileUtil.findFile(sourceFile, requestedFileName);
        } else {
            requestedFile = sourceFile;
        }
        if (requestedFile == null || !requestedFile.exists()) {
            throw new TargetSetupError(
                    String.format(
                            "Requested file with file_name %s does not exist in provided %s.",
                            requestedFileName, sourceFile),
                    device.getDeviceDescriptor());
        }
        return requestedFile;
    }

    /**
     * Helper method to execute a fastboot command.
     *
     * @param device the {@link ITestDevice} to execute command on
     * @param cmdArgs the arguments to provide to fastboot
     * @return String the stderr output from command if non-empty. Otherwise returns the stdout Some
     *     fastboot commands are weird in that they dump output to stderr on success case
     * @throws DeviceNotAvailableException if device is not available
     * @throws TargetSetupError if fastboot command fails
     */
    private String executeFastbootCmd(ITestDevice device, String... cmdArgs)
            throws DeviceNotAvailableException, TargetSetupError {
        List<String> fastbootCmdArgs = new ArrayList<>();
        if ("flash".equals(cmdArgs[0])) {
            fastbootCmdArgs.addAll(mFlashOptions);
        }
        fastbootCmdArgs.addAll(Arrays.asList(cmdArgs));
        CLog.i(
                "Execute fastboot command '%s' on %s",
                String.join(" ", fastbootCmdArgs), device.getSerialNumber());
        CommandResult result =
                device.executeLongFastbootCommand(
                        fastbootCmdArgs.toArray(new String[fastbootCmdArgs.size()]));
        CLog.v("fastboot stdout: " + result.getStdout());
        CLog.v("fastboot stderr: " + result.getStderr());
        CommandStatus cmdStatus = result.getStatus();
        // fastboot command line output is in stderr even for successful run
        if (result.getStderr().contains("FAILED")) {
            // if output contains "FAILED", just override to failure
            cmdStatus = CommandStatus.FAILED;
        }
        if (cmdStatus != CommandStatus.SUCCESS) {
            throw new TargetSetupError(
                    String.format(
                            "fastboot command '%s' failed in device %s. stdout: %s, stderr: %s",
                            String.join(" ", fastbootCmdArgs),
                            device.getSerialNumber(),
                            result.getStdout(),
                            result.getStderr()),
                    device.getDeviceDescriptor(),
                    DeviceErrorIdentifier.ERROR_AFTER_FLASHING);
        }
        if (result.getStderr().length() > 0) {
            return result.getStderr();
        } else {
            return result.getStdout();
        }
    }
}
