/*
 * Copyright (C) 2021 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 android.system.virtualmachine;

import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_CHANGED;
import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_INVALID_CONFIG;
import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_VERIFICATION_FAILED;
import static android.system.virtualmachine.VirtualMachineCallback.ERROR_UNKNOWN;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_CRASH;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_HANGUP;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_INFRASTRUCTURE_ERROR;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_KILLED;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_REBOOT;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_SHUTDOWN;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_START_FAILED;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_UNKNOWN;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_VIRTUALIZATION_SERVICE_DIED;

import static java.util.Objects.requireNonNull;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.annotation.WorkerThread;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.system.ErrnoException;
import android.system.OsConstants;
import android.system.virtualizationcommon.DeathReason;
import android.system.virtualizationcommon.ErrorCode;
import android.system.virtualizationservice.IVirtualMachine;
import android.system.virtualizationservice.IVirtualMachineCallback;
import android.system.virtualizationservice.IVirtualizationService;
import android.system.virtualizationservice.InputDevice;
import android.system.virtualizationservice.PartitionType;
import android.system.virtualizationservice.VirtualMachineAppConfig;
import android.system.virtualizationservice.VirtualMachineRawConfig;
import android.system.virtualizationservice.VirtualMachineState;
import android.util.JsonReader;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.MotionEvent;

import com.android.internal.annotations.GuardedBy;

import libcore.io.IoBridge;
import libcore.io.IoUtils;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.zip.ZipFile;

/**
 * Represents an VM instance, with its own configuration and state. Instances are persistent and are
 * created or retrieved via {@link VirtualMachineManager}.
 *
 * <p>The {@link #run} method actually starts up the VM and allows the payload code to execute. It
 * will continue until it exits or {@link #stop} is called. Updates on the state of the VM can be
 * received using {@link #setCallback}. The app can communicate with the VM using {@link
 * #connectToVsockServer} or {@link #connectVsock}.
 *
 * <p>The payload code running inside the VM has access to a set of native APIs; see the <a
 * href="https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/libs/libvm_payload/README.md">README
 * file</a> for details.
 *
 * <p>Each VM has a unique secret, computed from the APK that contains the code running in it, the
 * VM configuration, and a random per-instance salt. The secret can be accessed by the payload code
 * running inside the VM (using {@code AVmPayload_getVmInstanceSecret}) but is not made available
 * outside it.
 *
 * @hide
 */
@SystemApi
public class VirtualMachine implements AutoCloseable {
    /** The permission needed to create or run a virtual machine. */
    public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION =
            "android.permission.MANAGE_VIRTUAL_MACHINE";

    /**
     * The permission needed to create a virtual machine with more advanced configuration options.
     */
    public static final String USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION =
            "android.permission.USE_CUSTOM_VIRTUAL_MACHINE";

    /**
     * The lowest port number that can be used to communicate with the virtual machine payload.
     *
     * @see #connectToVsockServer
     * @see #connectVsock
     */
    @SuppressLint("MinMaxConstant") // Won't change: see man 7 vsock.
    public static final long MIN_VSOCK_PORT = 1024;

    /**
     * The highest port number that can be used to communicate with the virtual machine payload.
     *
     * @see #connectToVsockServer
     * @see #connectVsock
     */
    @SuppressLint("MinMaxConstant") // Won't change: see man 7 vsock.
    public static final long MAX_VSOCK_PORT = (1L << 32) - 1;

    private ParcelFileDescriptor mTouchSock;
    private ParcelFileDescriptor mKeySock;
    private ParcelFileDescriptor mMouseSock;
    private ParcelFileDescriptor mSwitchesSock;
    private ParcelFileDescriptor mTrackpadSock;

    private enum InputEventType {
        TOUCH,
        MOUSE,
        TRACKPAD
    }

    private BlockingQueue<Pair<InputEventType, MotionEvent>> mInputEventQueue =
            new LinkedBlockingQueue<>();

    /**
     * Status of a virtual machine
     *
     * @hide
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            prefix = "STATUS_",
            value = {STATUS_STOPPED, STATUS_RUNNING, STATUS_DELETED})
    public @interface Status {}

    /** The virtual machine has just been created, or {@link #stop} was called on it. */
    public static final int STATUS_STOPPED = 0;

    /** The virtual machine is running. */
    public static final int STATUS_RUNNING = 1;

    /**
     * The virtual machine has been deleted. This is an irreversible state. Once a virtual machine
     * is deleted all its secrets are permanently lost, and it cannot be run. A new virtual machine
     * with the same name and config may be created, with new and different secrets.
     */
    public static final int STATUS_DELETED = 2;

    private static final String TAG = "VirtualMachine";

    /** Name of the directory under the files directory where all VMs created for the app exist. */
    private static final String VM_DIR = "vm";

    /** Name of the persisted config file for a VM. */
    private static final String CONFIG_FILE = "config.xml";

    /** Name of the instance image file for a VM. (Not implemented) */
    private static final String INSTANCE_IMAGE_FILE = "instance.img";

    /** Name of the file for a VM containing Id. */
    private static final String INSTANCE_ID_FILE = "instance_id";

    /** Name of the idsig file for a VM */
    private static final String IDSIG_FILE = "idsig";

    /** Name of the idsig files for extra APKs. */
    private static final String EXTRA_IDSIG_FILE_PREFIX = "extra_idsig_";

    /** Size of the instance image. 10 MB. */
    private static final long INSTANCE_FILE_SIZE = 10 * 1024 * 1024;

    /** Name of the file backing the encrypted storage */
    private static final String ENCRYPTED_STORE_FILE = "storage.img";

    /** The package which owns this VM. */
    @NonNull private final String mPackageName;

    /** Name of this VM within the package. The name should be unique in the package. */
    @NonNull private final String mName;

    /** Path to the directory containing all the files related to this VM. */
    @NonNull private final File mVmRootPath;

    /**
     * Path to the config file for this VM. The config file is where the configuration is persisted.
     */
    @NonNull private final File mConfigFilePath;

    /** Path to the instance image file for this VM. */
    @NonNull private final File mInstanceFilePath;

    /** Path to the idsig file for this VM. */
    @NonNull private final File mIdsigFilePath;

    /** File that backs the encrypted storage - Will be null if not enabled. */
    @Nullable private final File mEncryptedStoreFilePath;

    /** File that contains the Id. This is NULL iff FEATURE_LLPVM is disabled */
    @Nullable private final File mInstanceIdPath;

    /**
     * Unmodifiable list of extra apks. Apks are specified by the vm config, and corresponding
     * idsigs are to be generated.
     */
    @NonNull private final List<ExtraApkSpec> mExtraApks;

    private class MemoryManagementCallbacks implements ComponentCallbacks2 {
        @Override
        public void onConfigurationChanged(@NonNull Configuration newConfig) {}

        @Override
        public void onLowMemory() {}

        @Override
        public void onTrimMemory(int level) {
            int percent;

            switch (level) {
                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
                    percent = 50;
                    break;
                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
                    percent = 30;
                    break;
                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
                    percent = 10;
                    break;
                case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
                case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
                case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
                    /* Release as much memory as we can. The app is on the LMKD LRU kill list. */
                    percent = 50;
                    break;
                default:
                    /* Treat unrecognised messages as generic low-memory warnings. */
                    percent = 30;
                    break;
            }

            synchronized (mLock) {
                try {
                    if (mVirtualMachine != null) {
                        long bytes = mConfig.getMemoryBytes();
                        mVirtualMachine.setMemoryBalloon(bytes * percent / 100);
                    }
                } catch (Exception e) {
                    /* Caller doesn't want our exceptions. Log them instead. */
                    Log.w(TAG, "TrimMemory failed: ", e);
                }
            }
        }
    }

    /** Running instance of virtmgr that hosts VirtualizationService for this VM. */
    @NonNull private final VirtualizationService mVirtualizationService;

    private final MemoryManagementCallbacks mMemoryManagementCallbacks;

    @NonNull private final Context mContext;

    // A note on lock ordering:
    // You can take mLock while holding VirtualMachineManager.sCreateLock, but not vice versa.
    // We never take any other lock while holding mCallbackLock; therefore you can
    // take mCallbackLock while holding any other lock.

    /** Lock protecting our mutable state (other than callbacks). */
    private final Object mLock = new Object();

    /** Lock protecting callbacks. */
    private final Object mCallbackLock = new Object();

    private final boolean mVmOutputCaptured;

    private final boolean mVmConsoleInputSupported;

    private final boolean mConnectVmConsole;

    private final Executor mConsoleExecutor = Executors.newSingleThreadExecutor();

    private ExecutorService mInputEventExecutor;

    /** The configuration that is currently associated with this VM. */
    @GuardedBy("mLock")
    @NonNull
    private VirtualMachineConfig mConfig;

    /** Handle to the "running" VM. */
    @GuardedBy("mLock")
    @Nullable
    private IVirtualMachine mVirtualMachine;

    @GuardedBy("mLock")
    @Nullable
    private ParcelFileDescriptor mConsoleOutReader;

    @GuardedBy("mLock")
    @Nullable
    private ParcelFileDescriptor mConsoleOutWriter;

    @GuardedBy("mLock")
    @Nullable
    private ParcelFileDescriptor mConsoleInReader;

    @GuardedBy("mLock")
    @Nullable
    private ParcelFileDescriptor mConsoleInWriter;

    @GuardedBy("mLock")
    @Nullable
    private ParcelFileDescriptor mTeeConsoleOutReader;

    @GuardedBy("mLock")
    @Nullable
    private ParcelFileDescriptor mTeeConsoleOutWriter;

    @GuardedBy("mLock")
    @Nullable
    private ParcelFileDescriptor mPtyFd;

    @GuardedBy("mLock")
    @Nullable
    private ParcelFileDescriptor mPtsFd;

    @GuardedBy("mLock")
    @Nullable
    private String mPtsName;

    @GuardedBy("mLock")
    @Nullable
    private ParcelFileDescriptor mLogReader;

    @GuardedBy("mLock")
    @Nullable
    private ParcelFileDescriptor mLogWriter;

    @GuardedBy("mLock")
    private boolean mWasDeleted = false;

    /** The registered callback */
    @GuardedBy("mCallbackLock")
    @Nullable
    private VirtualMachineCallback mCallback;

    /** The executor on which the callback will be executed */
    @GuardedBy("mCallbackLock")
    @Nullable
    private Executor mCallbackExecutor;

    private static class ExtraApkSpec {
        public final File apk;
        public final File idsig;

        ExtraApkSpec(File apk, File idsig) {
            this.apk = apk;
            this.idsig = idsig;
        }
    }

    static {
        System.loadLibrary("virtualmachine_jni");
    }

    private VirtualMachine(
            @NonNull Context context,
            @NonNull String name,
            @NonNull VirtualMachineConfig config,
            @NonNull VirtualizationService service)
            throws VirtualMachineException {
        mPackageName = context.getPackageName();
        mName = requireNonNull(name, "Name must not be null");
        mConfig = requireNonNull(config, "Config must not be null");
        mVirtualizationService = service;

        File thisVmDir = getVmDir(context, mName);
        mVmRootPath = thisVmDir;
        mConfigFilePath = new File(thisVmDir, CONFIG_FILE);
        try {
            mInstanceIdPath =
                    (mVirtualizationService
                                    .getBinder()
                                    .isFeatureEnabled(IVirtualizationService.FEATURE_LLPVM_CHANGES))
                            ? new File(thisVmDir, INSTANCE_ID_FILE)
                            : null;
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
        mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
        mIdsigFilePath = new File(thisVmDir, IDSIG_FILE);
        mExtraApks = setupExtraApks(context, config, thisVmDir);
        mContext = context;
        mEncryptedStoreFilePath =
                (config.isEncryptedStorageEnabled())
                        ? new File(thisVmDir, ENCRYPTED_STORE_FILE)
                        : null;

        mVmOutputCaptured = config.isVmOutputCaptured();
        mVmConsoleInputSupported = config.isVmConsoleInputSupported();
        mConnectVmConsole = config.isConnectVmConsole();

        VirtualMachineCustomImageConfig customImageConfig;
        customImageConfig = config.getCustomImageConfig();
        if (customImageConfig == null || customImageConfig.useAutoMemoryBalloon()) {
            mMemoryManagementCallbacks = new MemoryManagementCallbacks();
        } else {
            mMemoryManagementCallbacks = null;
        }
    }

    /**
     * Creates a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
     * with the given name.
     *
     * <p>The new virtual machine will be in the same state as the descriptor indicates.
     *
     * <p>Once a virtual machine is imported it is persisted until it is deleted by calling {@link
     * #delete}. The imported virtual machine is in {@link #STATUS_STOPPED} state. To run the VM,
     * call {@link #run}.
     */
    @GuardedBy("VirtualMachineManager.sCreateLock")
    @NonNull
    static VirtualMachine fromDescriptor(
            @NonNull Context context,
            @NonNull String name,
            @NonNull VirtualMachineDescriptor vmDescriptor)
            throws VirtualMachineException {
        File vmDir = createVmDir(context, name);
        try {
            VirtualMachine vm;
            try (vmDescriptor) {
                VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
                vm = new VirtualMachine(context, name, config, VirtualizationService.getInstance());
                config.serialize(vm.mConfigFilePath);
                try {
                    vm.mInstanceFilePath.createNewFile();
                } catch (IOException e) {
                    throw new VirtualMachineException("failed to create instance image", e);
                }
                vm.importInstanceFrom(vmDescriptor.getInstanceImgFd());

                if (vmDescriptor.getEncryptedStoreFd() != null) {
                    try {
                        vm.mEncryptedStoreFilePath.createNewFile();
                    } catch (IOException e) {
                        throw new VirtualMachineException(
                                "failed to create encrypted storage image", e);
                    }
                    vm.importEncryptedStoreFrom(vmDescriptor.getEncryptedStoreFd());
                }
                if (vm.mInstanceIdPath != null) {
                    vm.importInstanceIdFrom(vmDescriptor.getInstanceIdFd());
                    vm.claimInstance();
                }
            }
            return vm;
        } catch (VirtualMachineException | RuntimeException e) {
            // If anything goes wrong, delete any files created so far and the VM's directory
            try {
                deleteRecursively(vmDir);
            } catch (Exception innerException) {
                e.addSuppressed(innerException);
            }
            throw e;
        }
    }

    /**
     * Creates a virtual machine with the given name and config. Once a virtual machine is created
     * it is persisted until it is deleted by calling {@link #delete}. The created virtual machine
     * is in {@link #STATUS_STOPPED} state. To run the VM, call {@link #run}.
     */
    @GuardedBy("VirtualMachineManager.sCreateLock")
    @NonNull
    static VirtualMachine create(
            @NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)
            throws VirtualMachineException {
        File vmDir = createVmDir(context, name);

        try {
            VirtualMachine vm =
                    new VirtualMachine(context, name, config, VirtualizationService.getInstance());
            config.serialize(vm.mConfigFilePath);
            try {
                vm.mInstanceFilePath.createNewFile();
            } catch (IOException e) {
                throw new VirtualMachineException("failed to create instance image", e);
            }
            if (config.isEncryptedStorageEnabled()) {
                try {
                    vm.mEncryptedStoreFilePath.createNewFile();
                } catch (IOException e) {
                    throw new VirtualMachineException(
                            "failed to create encrypted storage image", e);
                }
            }

            IVirtualizationService service = vm.mVirtualizationService.getBinder();

            if (vm.mInstanceIdPath != null) {
                try (FileOutputStream stream = new FileOutputStream(vm.mInstanceIdPath)) {
                    byte[] id = service.allocateInstanceId();
                    stream.write(id);
                } catch (FileNotFoundException e) {
                    throw new VirtualMachineException("instance_id file missing", e);
                } catch (IOException e) {
                    throw new VirtualMachineException("failed to persist instance_id", e);
                } catch (RemoteException e) {
                    throw e.rethrowAsRuntimeException();
                } catch (ServiceSpecificException | IllegalArgumentException e) {
                    throw new VirtualMachineException("failed to create instance_id", e);
                }
            }

            try {
                service.initializeWritablePartition(
                        ParcelFileDescriptor.open(vm.mInstanceFilePath, MODE_READ_WRITE),
                        INSTANCE_FILE_SIZE,
                        PartitionType.ANDROID_VM_INSTANCE);
            } catch (FileNotFoundException e) {
                throw new VirtualMachineException("instance image missing", e);
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            } catch (ServiceSpecificException | IllegalArgumentException e) {
                throw new VirtualMachineException("failed to create instance partition", e);
            }

            if (config.isEncryptedStorageEnabled()) {
                try {
                    service.initializeWritablePartition(
                            ParcelFileDescriptor.open(vm.mEncryptedStoreFilePath, MODE_READ_WRITE),
                            config.getEncryptedStorageBytes(),
                            PartitionType.ENCRYPTEDSTORE);
                } catch (FileNotFoundException e) {
                    throw new VirtualMachineException("encrypted storage image missing", e);
                } catch (RemoteException e) {
                    throw e.rethrowAsRuntimeException();
                } catch (ServiceSpecificException | IllegalArgumentException e) {
                    throw new VirtualMachineException(
                            "failed to create encrypted storage partition", e);
                }
            }
            return vm;
        } catch (VirtualMachineException | RuntimeException e) {
            // If anything goes wrong, delete any files created so far and the VM's directory
            try {
                vmInstanceCleanup(context, name);
            } catch (Exception innerException) {
                e.addSuppressed(innerException);
            }
            throw e;
        }
    }

    /** Loads a virtual machine that is already created before. */
    @GuardedBy("VirtualMachineManager.sCreateLock")
    @Nullable
    static VirtualMachine load(@NonNull Context context, @NonNull String name)
            throws VirtualMachineException {
        File thisVmDir = getVmDir(context, name);
        if (!thisVmDir.exists()) {
            // The VM doesn't exist.
            return null;
        }
        File configFilePath = new File(thisVmDir, CONFIG_FILE);
        VirtualMachineConfig config = VirtualMachineConfig.from(configFilePath);
        VirtualMachine vm =
                new VirtualMachine(context, name, config, VirtualizationService.getInstance());

        if (vm.mInstanceIdPath != null && !vm.mInstanceIdPath.exists()) {
            throw new VirtualMachineException("instance_id file missing");
        }
        if (!vm.mInstanceFilePath.exists()) {
            throw new VirtualMachineException("instance image missing");
        }
        if (config.isEncryptedStorageEnabled() && !vm.mEncryptedStoreFilePath.exists()) {
            throw new VirtualMachineException("Storage image missing");
        }
        return vm;
    }

    @GuardedBy("VirtualMachineManager.sCreateLock")
    void delete(Context context, String name) throws VirtualMachineException {
        synchronized (mLock) {
            checkStopped();
            // Once we explicitly delete a VM it must remain permanently in the deleted state;
            // if a new VM is created with the same name (and files) that's unrelated.
            mWasDeleted = true;
        }
        vmInstanceCleanup(context, name);
    }

    // Delete the full VM directory and notify VirtualizationService to remove this
    // VM instance for housekeeping.
    @GuardedBy("VirtualMachineManager.sCreateLock")
    static void vmInstanceCleanup(Context context, String name) throws VirtualMachineException {
        File vmDir = getVmDir(context, name);
        notifyInstanceRemoval(vmDir, VirtualizationService.getInstance());
        try {
            deleteRecursively(vmDir);
        } catch (IOException e) {
            throw new VirtualMachineException(e);
        }
    }

    private static void notifyInstanceRemoval(
            File vmDirectory, @NonNull VirtualizationService service) {
        File instanceIdFile = new File(vmDirectory, INSTANCE_ID_FILE);
        try {
            byte[] instanceId = Files.readAllBytes(instanceIdFile.toPath());
            service.getBinder().removeVmInstance(instanceId);
        } catch (Exception e) {
            // Deliberately ignoring error in removing VM instance. This potentially leads to
            // unaccounted instances in the VS' database. But, nothing much can be done by caller.
            Log.w(TAG, "Failed to notify VS to remove the VM instance", e);
        }
    }

    // Claim the instance. This notifies the global VS about the ownership of this
    // instance_id for housekeeping purpose.
    void claimInstance() throws VirtualMachineException {
        if (mInstanceIdPath != null) {
            IVirtualizationService service = mVirtualizationService.getBinder();
            try {
                byte[] instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
                service.claimVmInstance(instanceId);
            } catch (IOException e) {
                throw new VirtualMachineException("failed to read instance_id", e);
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            }
        }
    }

    @GuardedBy("VirtualMachineManager.sCreateLock")
    @NonNull
    private static File createVmDir(@NonNull Context context, @NonNull String name)
            throws VirtualMachineException {
        File vmDir = getVmDir(context, name);
        try {
            // We don't need to undo this even if VM creation fails.
            Files.createDirectories(vmDir.getParentFile().toPath());

            // The checking of the existence of this directory and the creation of it is done
            // atomically. If the directory already exists (i.e. the VM with the same name was
            // already created), FileAlreadyExistsException is thrown.
            Files.createDirectory(vmDir.toPath());
        } catch (FileAlreadyExistsException e) {
            throw new VirtualMachineException("virtual machine already exists", e);
        } catch (IOException e) {
            throw new VirtualMachineException("failed to create directory for VM", e);
        }
        return vmDir;
    }

    @NonNull
    private static File getVmDir(@NonNull Context context, @NonNull String name) {
        if (name.contains(File.separator) || name.equals(".") || name.equals("..")) {
            throw new IllegalArgumentException("Invalid VM name: " + name);
        }
        File vmRoot = new File(context.getDataDir(), VM_DIR);
        return new File(vmRoot, name);
    }

    /**
     * Returns the name of this virtual machine. The name is unique in the package and can't be
     * changed.
     *
     * @hide
     */
    @SystemApi
    @NonNull
    public String getName() {
        return mName;
    }

    /**
     * Returns the currently selected config of this virtual machine. There can be multiple virtual
     * machines sharing the same config. Even in that case, the virtual machines are completely
     * isolated from each other; they have different secrets. It is also possible that a virtual
     * machine can change its config, which can be done by calling {@link #setConfig}.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @hide
     */
    @SystemApi
    @WorkerThread
    @NonNull
    public VirtualMachineConfig getConfig() {
        synchronized (mLock) {
            return mConfig;
        }
    }

    /**
     * Returns the current status of this virtual machine.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @hide
     */
    @SystemApi
    @WorkerThread
    @Status
    public int getStatus() {
        IVirtualMachine virtualMachine;
        synchronized (mLock) {
            if (mWasDeleted) {
                return STATUS_DELETED;
            }
            virtualMachine = mVirtualMachine;
        }

        int status;
        if (virtualMachine == null) {
            status = STATUS_STOPPED;
        } else {
            try {
                status = stateToStatus(virtualMachine.getState());
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            }
        }
        if (status == STATUS_STOPPED && !mVmRootPath.exists()) {
            // A VM can quite happily keep running if its backing files have been deleted.
            // But once it stops, it's gone forever.
            synchronized (mLock) {
                dropVm();
            }
            return STATUS_DELETED;
        }
        return status;
    }

    private int stateToStatus(@VirtualMachineState int state) {
        switch (state) {
            case VirtualMachineState.STARTING:
            case VirtualMachineState.STARTED:
            case VirtualMachineState.READY:
            case VirtualMachineState.FINISHED:
                return STATUS_RUNNING;
            case VirtualMachineState.NOT_STARTED:
            case VirtualMachineState.DEAD:
            default:
                return STATUS_STOPPED;
        }
    }

    // Throw an appropriate exception if we have a running VM, or the VM has been deleted.
    @GuardedBy("mLock")
    private void checkStopped() throws VirtualMachineException {
        if (mWasDeleted || !mVmRootPath.exists()) {
            throw new VirtualMachineException("VM has been deleted");
        }
        if (mVirtualMachine == null) {
            return;
        }
        try {
            if (stateToStatus(mVirtualMachine.getState()) != STATUS_STOPPED) {
                throw new VirtualMachineException("VM is not in stopped state");
            }
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
        // It's stopped, but we still have a reference to it - we can fix that.
        dropVm();
    }

    /**
     * This should only be called when we know our VM has stopped; we no longer need to hold a
     * reference to it (this allows resources to be GC'd) and we no longer need to be informed of
     * memory pressure.
     */
    @GuardedBy("mLock")
    private void dropVm() {
        if (mMemoryManagementCallbacks != null) {
            mContext.unregisterComponentCallbacks(mMemoryManagementCallbacks);
        }
        mVirtualMachine = null;
    }

    /** If we have an IVirtualMachine in the running state return it, otherwise throw. */
    @GuardedBy("mLock")
    private IVirtualMachine getRunningVm() throws VirtualMachineException {
        try {
            if (mVirtualMachine != null
                    && stateToStatus(mVirtualMachine.getState()) == STATUS_RUNNING) {
                return mVirtualMachine;
            } else {
                if (mWasDeleted || !mVmRootPath.exists()) {
                    throw new VirtualMachineException("VM has been deleted");
                } else {
                    throw new VirtualMachineException("VM is not in running state");
                }
            }
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
    }

    /**
     * Registers the callback object to get events from the virtual machine. If a callback was
     * already registered, it is replaced with the new one.
     *
     * @hide
     */
    @SystemApi
    public void setCallback(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull VirtualMachineCallback callback) {
        synchronized (mCallbackLock) {
            mCallback = callback;
            mCallbackExecutor = executor;
        }
    }

    /**
     * Clears the currently registered callback.
     *
     * @hide
     */
    @SystemApi
    public void clearCallback() {
        synchronized (mCallbackLock) {
            mCallback = null;
            mCallbackExecutor = null;
        }
    }

    /** Executes a callback on the callback executor. */
    private void executeCallback(Consumer<VirtualMachineCallback> fn) {
        final VirtualMachineCallback callback;
        final Executor executor;
        synchronized (mCallbackLock) {
            callback = mCallback;
            executor = mCallbackExecutor;
        }
        if (callback == null || executor == null) {
            return;
        }
        final long restoreToken = Binder.clearCallingIdentity();
        try {
            executor.execute(() -> fn.accept(callback));
        } finally {
            Binder.restoreCallingIdentity(restoreToken);
        }
    }

    private android.system.virtualizationservice.VirtualMachineConfig
            createVirtualMachineConfigForRawFrom(VirtualMachineConfig vmConfig)
                    throws IllegalStateException, IOException {
        VirtualMachineRawConfig rawConfig = vmConfig.toVsRawConfig();

        // Handle input devices here
        List<InputDevice> inputDevices = new ArrayList<>();
        if (vmConfig.getCustomImageConfig() != null && rawConfig.displayConfig != null) {
            if (vmConfig.getCustomImageConfig().useTouch()) {
                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
                mTouchSock = pfds[0];
                InputDevice.MultiTouch t = new InputDevice.MultiTouch();
                t.width = rawConfig.displayConfig.width;
                t.height = rawConfig.displayConfig.height;
                t.pfd = pfds[1];
                inputDevices.add(InputDevice.multiTouch(t));
            }
            if (vmConfig.getCustomImageConfig().useKeyboard()) {
                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
                mKeySock = pfds[0];
                InputDevice.Keyboard k = new InputDevice.Keyboard();
                k.pfd = pfds[1];
                inputDevices.add(InputDevice.keyboard(k));
            }
            if (vmConfig.getCustomImageConfig().useMouse()) {
                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
                mMouseSock = pfds[0];
                InputDevice.Mouse m = new InputDevice.Mouse();
                m.pfd = pfds[1];
                inputDevices.add(InputDevice.mouse(m));
            }
            if (vmConfig.getCustomImageConfig().useSwitches()) {
                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
                mSwitchesSock = pfds[0];
                InputDevice.Switches s = new InputDevice.Switches();
                s.pfd = pfds[1];
                inputDevices.add(InputDevice.switches(s));
            }
            if (vmConfig.getCustomImageConfig().useTrackpad()) {
                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
                mTrackpadSock = pfds[0];
                InputDevice.Trackpad t = new InputDevice.Trackpad();
                // TODO(b/347253952): make it configurable
                t.width = 2380;
                t.height = 1369;
                t.pfd = pfds[1];
                inputDevices.add(InputDevice.trackpad(t));
            }
        }
        rawConfig.inputDevices = inputDevices.toArray(new InputDevice[0]);

        // Handle network support
        if (vmConfig.getCustomImageConfig() != null) {
            rawConfig.networkSupported = vmConfig.getCustomImageConfig().useNetwork();
        }

        return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
    }

    private static record InputEvent(short type, short code, int value) {}

    /** @hide */
    public boolean sendKeyEvent(KeyEvent event) {
        if (mKeySock == null) {
            Log.d(TAG, "mKeySock == null");
            return false;
        }
        // from include/uapi/linux/input-event-codes.h in the kernel.
        short EV_SYN = 0x00;
        short EV_KEY = 0x01;
        short SYN_REPORT = 0x00;
        boolean down = event.getAction() != MotionEvent.ACTION_UP;

        return writeEventsToSock(
                mKeySock,
                Arrays.asList(
                        new InputEvent(EV_KEY, (short) event.getScanCode(), down ? 1 : 0),
                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
    }

    /** @hide */
    public boolean sendMouseEvent(MotionEvent event) {
        try {
            mInputEventQueue.add(
                    Pair.create(InputEventType.MOUSE, MotionEvent.obtainNoHistory(event)));
            return true;
        } catch (Exception e) {
            Log.e(TAG, e.toString());
            return false;
        }
    }

    /** @hide */
    private boolean sendMouseEventInternal(MotionEvent event) {
        if (mMouseSock == null) {
            Log.d(TAG, "mMouseSock == null");
            return false;
        }
        // from include/uapi/linux/input-event-codes.h in the kernel.
        short EV_SYN = 0x00;
        short EV_REL = 0x02;
        short EV_KEY = 0x01;
        short REL_X = 0x00;
        short REL_Y = 0x01;
        short SYN_REPORT = 0x00;
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int y = (int) event.getY();
                return writeEventsToSock(
                        mMouseSock,
                        Arrays.asList(
                                new InputEvent(EV_REL, REL_X, x),
                                new InputEvent(EV_REL, REL_Y, y),
                                new InputEvent(EV_SYN, SYN_REPORT, 0)));
            case MotionEvent.ACTION_BUTTON_PRESS:
            case MotionEvent.ACTION_BUTTON_RELEASE:
                short BTN_LEFT = 0x110;
                short BTN_RIGHT = 0x111;
                short BTN_MIDDLE = 0x112;
                short keyCode;
                switch (event.getActionButton()) {
                    case MotionEvent.BUTTON_PRIMARY:
                        keyCode = BTN_LEFT;
                        break;
                    case MotionEvent.BUTTON_SECONDARY:
                        keyCode = BTN_RIGHT;
                        break;
                    case MotionEvent.BUTTON_TERTIARY:
                        keyCode = BTN_MIDDLE;
                        break;
                    default:
                        Log.d(TAG, event.toString());
                        return false;
                }
                return writeEventsToSock(
                        mMouseSock,
                        Arrays.asList(
                                new InputEvent(
                                        EV_KEY,
                                        keyCode,
                                        event.getAction() == MotionEvent.ACTION_BUTTON_PRESS
                                                ? 1
                                                : 0),
                                new InputEvent(EV_SYN, SYN_REPORT, 0)));
            case MotionEvent.ACTION_SCROLL:
                short REL_HWHEEL = 0x06;
                short REL_WHEEL = 0x08;
                int scrollX = (int) event.getAxisValue(MotionEvent.AXIS_HSCROLL);
                int scrollY = (int) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
                boolean status = true;
                if (scrollX != 0) {
                    status &=
                            writeEventsToSock(
                                    mMouseSock,
                                    Arrays.asList(
                                            new InputEvent(EV_REL, REL_HWHEEL, scrollX),
                                            new InputEvent(EV_SYN, SYN_REPORT, 0)));
                } else if (scrollY != 0) {
                    status &=
                            writeEventsToSock(
                                    mMouseSock,
                                    Arrays.asList(
                                            new InputEvent(EV_REL, REL_WHEEL, scrollY),
                                            new InputEvent(EV_SYN, SYN_REPORT, 0)));
                } else {
                    Log.d(TAG, event.toString());
                    return false;
                }
                return status;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_DOWN:
                // Ignored because it's handled by ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE
                return true;
            default:
                Log.d(TAG, event.toString());
                return false;
        }
    }

    /** @hide */
    public boolean sendMultiTouchEvent(MotionEvent event) {
        try {
            mInputEventQueue.add(
                    Pair.create(InputEventType.TOUCH, MotionEvent.obtainNoHistory(event)));
            return true;
        } catch (Exception e) {
            Log.e(TAG, e.toString());
            return false;
        }
    }

    /** @hide */
    private boolean sendMultiTouchEventInternal(MotionEvent event) {
        if (mTouchSock == null) {
            Log.d(TAG, "mTouchSock == null");
            return false;
        }
        // from include/uapi/linux/input-event-codes.h in the kernel.
        short EV_SYN = 0x00;
        short EV_ABS = 0x03;
        short EV_KEY = 0x01;
        short BTN_TOUCH = 0x14a;
        short ABS_X = 0x00;
        short ABS_Y = 0x01;
        short SYN_REPORT = 0x00;
        short ABS_MT_SLOT = 0x2f;
        short ABS_MT_POSITION_X = 0x35;
        short ABS_MT_POSITION_Y = 0x36;
        short ABS_MT_TRACKING_ID = 0x39;

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                List<InputEvent> events =
                        new ArrayList<>(
                                event.getPointerCount() * 6 /*InputEvent per a pointer*/
                                        + 1 /*SYN*/);
                for (int actionIdx = 0; actionIdx < event.getPointerCount(); actionIdx++) {
                    int pointerId = event.getPointerId(actionIdx);
                    int x = (int) event.getRawX(actionIdx);
                    int y = (int) event.getRawY(actionIdx);
                    events.add(new InputEvent(EV_ABS, ABS_MT_SLOT, pointerId));
                    events.add(new InputEvent(EV_ABS, ABS_MT_TRACKING_ID, pointerId));
                    events.add(new InputEvent(EV_ABS, ABS_MT_POSITION_X, x));
                    events.add(new InputEvent(EV_ABS, ABS_MT_POSITION_Y, y));
                    events.add(new InputEvent(EV_ABS, ABS_X, x));
                    events.add(new InputEvent(EV_ABS, ABS_Y, y));
                }
                events.add(new InputEvent(EV_SYN, SYN_REPORT, 0));
                return writeEventsToSock(mTouchSock, events);
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                break;
            default:
                return false;
        }

        boolean down =
                event.getActionMasked() == MotionEvent.ACTION_DOWN
                        || event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
        int actionIdx = event.getActionIndex();
        int pointerId = event.getPointerId(actionIdx);
        int x = (int) event.getRawX(actionIdx);
        int y = (int) event.getRawY(actionIdx);
        return writeEventsToSock(
                mTouchSock,
                Arrays.asList(
                        new InputEvent(EV_KEY, BTN_TOUCH, down ? 1 : 0),
                        new InputEvent(EV_ABS, ABS_MT_SLOT, pointerId),
                        new InputEvent(EV_ABS, ABS_MT_TRACKING_ID, down ? pointerId : -1),
                        new InputEvent(EV_ABS, ABS_MT_POSITION_X, x),
                        new InputEvent(EV_ABS, ABS_MT_POSITION_Y, y),
                        new InputEvent(EV_ABS, ABS_X, x),
                        new InputEvent(EV_ABS, ABS_Y, y),
                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
    }

    /** @hide */
    public boolean sendLidEvent(boolean close) {
        if (mSwitchesSock == null) {
            Log.d(TAG, "mSwitcheSock == null");
            return false;
        }

        // from include/uapi/linux/input-event-codes.h in the kernel.
        short EV_SYN = 0x00;
        short EV_SW = 0x05;
        short SW_LID = 0x00;
        short SYN_REPORT = 0x00;
        return writeEventsToSock(
                mSwitchesSock,
                Arrays.asList(
                        new InputEvent(EV_SW, SW_LID, close ? 1 : 0),
                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
    }

    /** @hide */
    public boolean sendTabletModeEvent(boolean tabletMode) {
        if (mSwitchesSock == null) {
            Log.d(TAG, "mSwitcheSock == null");
            return false;
        }

        // from include/uapi/linux/input-event-codes.h in the kernel.
        short EV_SYN = 0x00;
        short EV_SW = 0x05;
        short SW_TABLET_MODE = 0x01;
        short SYN_REPORT = 0x00;
        return writeEventsToSock(
                mSwitchesSock,
                Arrays.asList(
                        new InputEvent(EV_SW, SW_TABLET_MODE, tabletMode ? 1 : 0),
                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
    }

    /** @hide */
    public boolean sendTrackpadEvent(MotionEvent event) {
        try {
            mInputEventQueue.add(
                    Pair.create(InputEventType.TRACKPAD, MotionEvent.obtainNoHistory(event)));
            return true;
        } catch (Exception e) {
            Log.e(TAG, e.toString());
            return false;
        }
    }

    /** @hide */
    private boolean sendTrackpadEventInternal(MotionEvent event) {
        if (mTrackpadSock == null) {
            Log.d(TAG, "mTrackpadSock == null");
            return false;
        }
        // from include/uapi/linux/input-event-codes.h in the kernel.
        short EV_SYN = 0x00;
        short EV_ABS = 0x03;
        short EV_KEY = 0x01;
        short BTN_TOUCH = 0x14a;
        short BTN_TOOL_FINGER = 0x145;
        short BTN_TOOL_DOUBLETAP = 0x14d;
        short BTN_TOOL_TRIPLETAP = 0x14e;
        short BTN_TOOL_QUADTAP = 0x14f;
        short ABS_X = 0x00;
        short ABS_Y = 0x01;
        short SYN_REPORT = 0x00;
        short ABS_MT_SLOT = 0x2f;
        short ABS_MT_TOUCH_MAJOR = 0x30;
        short ABS_MT_TOUCH_MINOR = 0x31;
        short ABS_MT_WIDTH_MAJOR = 0x32;
        short ABS_MT_WIDTH_MINOR = 0x33;
        short ABS_MT_ORIENTATION = 0x34;
        short ABS_MT_POSITION_X = 0x35;
        short ABS_MT_POSITION_Y = 0x36;
        short ABS_MT_TOOL_TYPE = 0x37;
        short ABS_MT_BLOB_ID = 0x38;
        short ABS_MT_TRACKING_ID = 0x39;
        short ABS_MT_PRESSURE = 0x3a;
        short ABS_MT_DISTANCE = 0x3b;
        short ABS_MT_TOOL_X = 0x3c;
        short ABS_MT_TOOL_Y = 0x3d;
        short ABS_PRESSURE = 0x18;
        short ABS_TOOL_WIDTH = 0x1c;

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_BUTTON_PRESS:
            case MotionEvent.ACTION_BUTTON_RELEASE:
                short BTN_LEFT = 0x110;
                short keyCode;
                switch (event.getActionButton()) {
                    case MotionEvent.BUTTON_PRIMARY:
                        keyCode = BTN_LEFT;
                        break;
                    default:
                        Log.d(TAG, event.toString());
                        return false;
                }
                return writeEventsToSock(
                        mMouseSock,
                        Arrays.asList(
                                new InputEvent(
                                        EV_KEY,
                                        keyCode,
                                        event.getAction() == MotionEvent.ACTION_BUTTON_PRESS
                                                ? 1
                                                : 0),
                                new InputEvent(EV_SYN, SYN_REPORT, 0)));
            case MotionEvent.ACTION_MOVE:
                List<InputEvent> events =
                        new ArrayList<>(
                                event.getPointerCount() * 10 /*InputEvent per a pointer*/
                                        + 1 /*SYN*/);
                for (int actionIdx = 0; actionIdx < event.getPointerCount(); actionIdx++) {
                    int pointerId = event.getPointerId(actionIdx);
                    int x = (int) event.getRawX(actionIdx);
                    int y = (int) event.getRawY(actionIdx);
                    events.add(new InputEvent(EV_ABS, ABS_MT_SLOT, pointerId));
                    events.add(new InputEvent(EV_ABS, ABS_MT_TRACKING_ID, pointerId));
                    events.add(new InputEvent(EV_ABS, ABS_MT_POSITION_X, x));
                    events.add(new InputEvent(EV_ABS, ABS_MT_POSITION_Y, y));
                    events.add(
                            new InputEvent(
                                    EV_ABS,
                                    ABS_MT_TOUCH_MAJOR,
                                    (short) event.getTouchMajor(actionIdx)));
                    events.add(
                            new InputEvent(
                                    EV_ABS,
                                    ABS_MT_TOUCH_MINOR,
                                    (short) event.getTouchMinor(actionIdx)));
                    events.add(new InputEvent(EV_ABS, ABS_X, x));
                    events.add(new InputEvent(EV_ABS, ABS_Y, y));
                    events.add(
                            new InputEvent(
                                    EV_ABS,
                                    ABS_PRESSURE,
                                    (short) (255 * event.getPressure(actionIdx))));
                    events.add(
                            new InputEvent(
                                    EV_ABS,
                                    ABS_MT_PRESSURE,
                                    (short) (255 * event.getPressure(actionIdx))));
                }
                events.add(new InputEvent(EV_SYN, SYN_REPORT, 0));
                return writeEventsToSock(mTrackpadSock, events);
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                break;
            default:
                return false;
        }

        boolean down =
                event.getActionMasked() == MotionEvent.ACTION_DOWN
                        || event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
        int actionIdx = event.getActionIndex();
        int pointerId = event.getPointerId(actionIdx);
        int x = (int) event.getRawX(actionIdx);
        int y = (int) event.getRawY(actionIdx);
        return writeEventsToSock(
                mTrackpadSock,
                Arrays.asList(
                        new InputEvent(EV_KEY, BTN_TOUCH, down ? 1 : 0),
                        new InputEvent(
                                EV_KEY,
                                BTN_TOOL_FINGER,
                                down && event.getPointerCount() == 1 ? 1 : 0),
                        new InputEvent(
                                EV_KEY, BTN_TOOL_DOUBLETAP, event.getPointerCount() == 2 ? 1 : 0),
                        new InputEvent(
                                EV_KEY, BTN_TOOL_TRIPLETAP, event.getPointerCount() == 3 ? 1 : 0),
                        new InputEvent(
                                EV_KEY, BTN_TOOL_QUADTAP, event.getPointerCount() > 4 ? 1 : 0),
                        new InputEvent(EV_ABS, ABS_MT_SLOT, pointerId),
                        new InputEvent(EV_ABS, ABS_MT_TRACKING_ID, down ? pointerId : -1),
                        new InputEvent(EV_ABS, ABS_MT_TOOL_TYPE, 0 /* MT_TOOL_FINGER */),
                        new InputEvent(EV_ABS, ABS_MT_POSITION_X, x),
                        new InputEvent(EV_ABS, ABS_MT_POSITION_Y, y),
                        new InputEvent(
                                EV_ABS, ABS_MT_TOUCH_MAJOR, (short) event.getTouchMajor(actionIdx)),
                        new InputEvent(
                                EV_ABS, ABS_MT_TOUCH_MINOR, (short) event.getTouchMinor(actionIdx)),
                        new InputEvent(EV_ABS, ABS_X, x),
                        new InputEvent(EV_ABS, ABS_Y, y),
                        new InputEvent(
                                EV_ABS, ABS_PRESSURE, (short) (255 * event.getPressure(actionIdx))),
                        new InputEvent(
                                EV_ABS,
                                ABS_MT_PRESSURE,
                                (short) (255 * event.getPressure(actionIdx))),
                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
    }

    /** @hide */
    public long getMemoryBalloon() {
        long bytes = 0;

        if (mMemoryManagementCallbacks != null) {
            Log.d(TAG, "Auto balloon enabled in getMemoryBalloon");
            return bytes;
        }

        synchronized (mLock) {
            try {
                if (mVirtualMachine != null) {
                    bytes = mVirtualMachine.getMemoryBalloon();
                }
            } catch (RemoteException e) {
                Log.w(TAG, "Cannot getMemoryBalloon", e);
            }
        }

        return bytes;
    }

    /** @hide */
    public void setMemoryBalloon(long bytes) {
        if (mMemoryManagementCallbacks != null) {
            Log.d(TAG, "Auto balloon enabled in setMemoryBalloon");
            return;
        }

        synchronized (mLock) {
            try {
                if (mVirtualMachine != null) {
                    mVirtualMachine.setMemoryBalloon(bytes);
                }
            } catch (RemoteException e) {
                Log.w(TAG, "Cannot setMemoryBalloon", e);
            }
        }
    }

    private boolean writeEventsToSock(ParcelFileDescriptor sock, List<InputEvent> evtList) {
        ByteBuffer byteBuffer =
                ByteBuffer.allocate(8 /* (type: u16 + code: u16 + value: i32) */ * evtList.size());
        byteBuffer.clear();
        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
        for (InputEvent e : evtList) {
            byteBuffer.putShort(e.type);
            byteBuffer.putShort(e.code);
            byteBuffer.putInt(e.value);
        }
        try {
            IoBridge.write(
                    sock.getFileDescriptor(), byteBuffer.array(), 0, byteBuffer.array().length);
        } catch (IOException e) {
            Log.d(TAG, "cannot send event", e);
            return false;
        }
        return true;
    }

    private android.system.virtualizationservice.VirtualMachineConfig
            createVirtualMachineConfigForAppFrom(
                    VirtualMachineConfig vmConfig, IVirtualizationService service)
                    throws RemoteException, IOException, VirtualMachineException {
        VirtualMachineAppConfig appConfig = vmConfig.toVsConfig(mContext.getPackageManager());
        appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
        appConfig.name = mName;
        if (mInstanceIdPath != null) {
            appConfig.instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
        } else {
            // FEATURE_LLPVM_CHANGES is disabled, instance_id is not used.
            appConfig.instanceId = new byte[64];
        }
        if (mEncryptedStoreFilePath != null) {
            appConfig.encryptedStorageImage =
                    ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
        }

        if (!vmConfig.getExtraApks().isEmpty()) {
            // Extra APKs were specified directly, rather than via config file.
            // We've already populated the file names for the extra APKs and IDSigs
            // (via setupExtraApks). But we also need to open the APK files and add
            // fds for them to the payload config.
            // This isn't needed when the extra APKs are specified in a config file -
            // then
            // Virtualization Manager opens them itself.
            List<ParcelFileDescriptor> extraApkFiles = new ArrayList<>(mExtraApks.size());
            for (ExtraApkSpec extraApk : mExtraApks) {
                try {
                    extraApkFiles.add(ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY));
                } catch (FileNotFoundException e) {
                    throw new VirtualMachineException("Failed to open extra APK", e);
                }
            }
            appConfig.payload.getPayloadConfig().extraApks = extraApkFiles;
        }

        try {
            createIdSigsAndUpdateConfig(service, appConfig);
        } catch (FileNotFoundException e) {
            throw new VirtualMachineException("Failed to generate APK signature", e);
        }
        return android.system.virtualizationservice.VirtualMachineConfig.appConfig(appConfig);
    }

    /**
     * Runs this virtual machine. The returning of this method however doesn't mean that the VM has
     * actually started running or the OS has booted there. Such events can be notified by
     * registering a callback using {@link #setCallback} before calling {@code run()}. There is no
     * limit other than available memory that limits the number of virtual machines that can run at
     * the same time.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @throws VirtualMachineException if the virtual machine is not stopped or could not be
     *     started.
     * @hide
     */
    @SystemApi
    @WorkerThread
    @RequiresPermission(MANAGE_VIRTUAL_MACHINE_PERMISSION)
    public void run() throws VirtualMachineException {
        synchronized (mLock) {
            checkStopped();

            try {
                mIdsigFilePath.createNewFile();
                for (ExtraApkSpec extraApk : mExtraApks) {
                    extraApk.idsig.createNewFile();
                }
            } catch (IOException e) {
                // If the file already exists, exception is not thrown.
                throw new VirtualMachineException("Failed to create APK signature file", e);
            }

            IVirtualizationService service = mVirtualizationService.getBinder();

            try {
                if (mConnectVmConsole) {
                    createPtyConsole();
                }

                if (mVmOutputCaptured) {
                    createVmOutputPipes();
                }

                if (mVmConsoleInputSupported) {
                    createVmInputPipes();
                }

                ParcelFileDescriptor consoleOutFd = null;
                if (mConnectVmConsole && mVmOutputCaptured) {
                    // If we are enabling output pipes AND the host console, then we tee the console
                    // output to both.
                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
                    mTeeConsoleOutReader = pipe[0];
                    mTeeConsoleOutWriter = pipe[1];
                    consoleOutFd = mTeeConsoleOutWriter;
                    TeeWorker tee =
                            new TeeWorker(
                                    mName + " console",
                                    new FileInputStream(mTeeConsoleOutReader.getFileDescriptor()),
                                    List.of(
                                            new FileOutputStream(mPtyFd.getFileDescriptor()),
                                            new FileOutputStream(
                                                    mConsoleOutWriter.getFileDescriptor())));
                    // If the VM is stopped then the tee worker thread would get an EOF or read()
                    // error which would tear down itself.
                    mConsoleExecutor.execute(tee);
                } else if (mConnectVmConsole) {
                    consoleOutFd = mPtyFd;
                } else if (mVmOutputCaptured) {
                    consoleOutFd = mConsoleOutWriter;
                }
                mInputEventExecutor = Executors.newSingleThreadExecutor();
                mInputEventExecutor.execute(
                        () -> {
                            while (true) {
                                try {
                                    Pair<InputEventType, MotionEvent> event =
                                            mInputEventQueue.take();
                                    switch (event.first) {
                                        case TOUCH:
                                            sendMultiTouchEventInternal(event.second);
                                            break;
                                        case TRACKPAD:
                                            sendTrackpadEventInternal(event.second);
                                            break;
                                        case MOUSE:
                                            sendMouseEventInternal(event.second);
                                            break;
                                    }
                                    event.second.recycle();
                                } catch (Exception e) {
                                    Log.e(TAG, e.toString());
                                }
                            }
                        });
                ParcelFileDescriptor consoleInFd = null;
                if (mConnectVmConsole) {
                    consoleInFd = mPtyFd;
                } else if (mVmConsoleInputSupported) {
                    consoleInFd = mConsoleInReader;
                }

                VirtualMachineConfig vmConfig = getConfig();
                android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
                        vmConfig.getCustomImageConfig() != null
                                ? createVirtualMachineConfigForRawFrom(vmConfig)
                                : createVirtualMachineConfigForAppFrom(vmConfig, service);

                mVirtualMachine =
                        service.createVm(
                                vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter, null);
                mVirtualMachine.registerCallback(new CallbackTranslator(service));
                if (mMemoryManagementCallbacks != null) {
                    mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
                }
                if (mConnectVmConsole) {
                    mVirtualMachine.setHostConsoleName(getHostConsoleName());
                }
                mVirtualMachine.start();
            } catch (IOException e) {
                throw new VirtualMachineException("failed to persist files", e);
            } catch (IllegalStateException | ServiceSpecificException e) {
                throw new VirtualMachineException(e);
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            }
        }
    }

    private void createIdSigsAndUpdateConfig(
            IVirtualizationService service, VirtualMachineAppConfig appConfig)
            throws RemoteException, FileNotFoundException {
        // Fill the idsig file by hashing the apk
        service.createOrUpdateIdsigFile(
                appConfig.apk, ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_WRITE));

        for (ExtraApkSpec extraApk : mExtraApks) {
            service.createOrUpdateIdsigFile(
                    ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY),
                    ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_WRITE));
        }

        // Re-open idsig files in read-only mode
        appConfig.idsig = ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_ONLY);
        List<ParcelFileDescriptor> extraIdsigs = new ArrayList<>();
        for (ExtraApkSpec extraApk : mExtraApks) {
            extraIdsigs.add(ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_ONLY));
        }
        appConfig.extraIdsigs = extraIdsigs;
    }

    @GuardedBy("mLock")
    private void createVmOutputPipes() throws VirtualMachineException {
        try {
            if (mConsoleOutReader == null || mConsoleOutWriter == null) {
                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
                mConsoleOutReader = pipe[0];
                mConsoleOutWriter = pipe[1];
            }

            if (mLogReader == null || mLogWriter == null) {
                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
                mLogReader = pipe[0];
                mLogWriter = pipe[1];
            }
        } catch (IOException e) {
            throw new VirtualMachineException("Failed to create output stream for VM", e);
        }
    }

    @GuardedBy("mLock")
    private void createVmInputPipes() throws VirtualMachineException {
        try {
            if (mConsoleInReader == null || mConsoleInWriter == null) {
                if (mConnectVmConsole) {
                    // If we are enabling input pipes AND the host console, then we should just use
                    // the host pty peer end as the console write end.
                    createPtyConsole();
                    mConsoleInReader = mPtyFd.dup();
                    mConsoleInWriter = mPtsFd.dup();
                } else {
                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
                    mConsoleInReader = pipe[0];
                    mConsoleInWriter = pipe[1];
                }
            }
        } catch (IOException e) {
            throw new VirtualMachineException("Failed to create input stream for VM", e);
        }
    }

    @FunctionalInterface
    private static interface OpenPtyCallback {
        public void apply(FileDescriptor mfd, FileDescriptor sfd, byte[] name);
    }

    // Opens a pty and set the master end to raw mode and O_NONBLOCK.
    private static native void nativeOpenPtyRawNonblock(OpenPtyCallback resultCallback)
            throws IOException;

    @GuardedBy("mLock")
    private void createPtyConsole() throws VirtualMachineException {
        if (mPtyFd != null && mPtsFd != null) {
            return;
        }
        List<FileDescriptor> fd = new ArrayList<>(2);
        StringBuilder nameBuilder = new StringBuilder();
        try {
            try {
                nativeOpenPtyRawNonblock(
                        (FileDescriptor mfd, FileDescriptor sfd, byte[] ptsName) -> {
                            fd.add(mfd);
                            fd.add(sfd);
                            nameBuilder.append(new String(ptsName, StandardCharsets.UTF_8));
                        });
            } catch (Exception e) {
                fd.forEach(IoUtils::closeQuietly);
                throw e;
            }
        } catch (IOException e) {
            throw new VirtualMachineException(
                    "Failed to create host console to connect to the VM console", e);
        }
        mPtyFd = new ParcelFileDescriptor(fd.get(0));
        mPtsFd = new ParcelFileDescriptor(fd.get(1));
        mPtsName = nameBuilder.toString();
        Log.d(TAG, "Serial console device: " + mPtsName);
    }

    /**
     * Returns the name of the peer end (ptsname) of the host console. The host console is only
     * available if the {@link VirtualMachineConfig} specifies that a host console should
     * {@linkplain VirtualMachineConfig#isConnectVmConsole connect} to the VM console.
     *
     * @throws VirtualMachineException if the host pseudoterminal could not be created, or
     *     connecting to the VM console is not enabled.
     * @hide
     */
    @NonNull
    private String getHostConsoleName() throws VirtualMachineException {
        if (!mConnectVmConsole) {
            throw new VirtualMachineException("Host console is not enabled");
        }
        synchronized (mLock) {
            createPtyConsole();
            return mPtsName;
        }
    }

    /**
     * Returns the stream object representing the console output from the virtual machine. The
     * console output is only available if the {@link VirtualMachineConfig} specifies that it should
     * be {@linkplain VirtualMachineConfig#isVmOutputCaptured captured}.
     *
     * <p>If you turn on output capture, you must consume data from {@code getConsoleOutput} -
     * because otherwise the code in the VM may get blocked when the pipe buffer fills up.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @throws VirtualMachineException if the stream could not be created, or capturing is turned
     *     off.
     * @hide
     */
    @SystemApi
    @WorkerThread
    @NonNull
    public InputStream getConsoleOutput() throws VirtualMachineException {
        if (!mVmOutputCaptured) {
            throw new VirtualMachineException("Capturing vm outputs is turned off");
        }
        synchronized (mLock) {
            createVmOutputPipes();
            return new FileInputStream(mConsoleOutReader.getFileDescriptor());
        }
    }

    /**
     * Returns the stream object representing the console input to the virtual machine. The console
     * input is only available if the {@link VirtualMachineConfig} specifies that it should be
     * {@linkplain VirtualMachineConfig#isVmConsoleInputSupported supported}.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @throws VirtualMachineException if the stream could not be created, or console input is not
     *     supported.
     * @hide
     */
    @TestApi
    @WorkerThread
    @NonNull
    public OutputStream getConsoleInput() throws VirtualMachineException {
        if (!mVmConsoleInputSupported) {
            throw new VirtualMachineException("VM console input is not supported");
        }
        synchronized (mLock) {
            createVmInputPipes();
            return new FileOutputStream(mConsoleInWriter.getFileDescriptor());
        }
    }

    /**
     * Returns the stream object representing the log output from the virtual machine. The log
     * output is only available if the VirtualMachineConfig specifies that it should be {@linkplain
     * VirtualMachineConfig#isVmOutputCaptured captured}.
     *
     * <p>If you turn on output capture, you must consume data from {@code getLogOutput} - because
     * otherwise the code in the VM may get blocked when the pipe buffer fills up.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @throws VirtualMachineException if the stream could not be created, or capturing is turned
     *     off.
     * @hide
     */
    @SystemApi
    @WorkerThread
    @NonNull
    public InputStream getLogOutput() throws VirtualMachineException {
        if (!mVmOutputCaptured) {
            throw new VirtualMachineException("Capturing vm outputs is turned off");
        }
        synchronized (mLock) {
            createVmOutputPipes();
            return new FileInputStream(mLogReader.getFileDescriptor());
        }
    }

    /**
     * Stops this virtual machine. Stopping a virtual machine is like pulling the plug on a real
     * computer; the machine halts immediately. Software running on the virtual machine is not
     * notified of the event. Writes to {@linkplain
     * VirtualMachineConfig.Builder#setEncryptedStorageBytes encrypted storage} might not be
     * persisted, and the instance might be left in an inconsistent state.
     *
     * <p>For a graceful shutdown, you could request the payload to call {@code exit()}, e.g. via a
     * {@linkplain #connectToVsockServer binder request}, and wait for {@link
     * VirtualMachineCallback#onPayloadFinished} to be called.
     *
     * <p>A stopped virtual machine can be re-started by calling {@link #run()}.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @throws VirtualMachineException if the virtual machine is not running or could not be
     *     stopped.
     * @hide
     */
    @SystemApi
    @WorkerThread
    public void stop() throws VirtualMachineException {
        synchronized (mLock) {
            if (mVirtualMachine == null) {
                throw new VirtualMachineException("VM is not running");
            }
            try {
                mVirtualMachine.stop();
                dropVm();
                if (mInputEventExecutor != null) {
                    mInputEventExecutor.shutdownNow();
                }
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            } catch (ServiceSpecificException e) {
                throw new VirtualMachineException(e);
            }
        }
    }

    /** @hide */
    public void suspend() throws VirtualMachineException {
        synchronized (mLock) {
            if (mVirtualMachine == null) {
                throw new VirtualMachineException("VM is not running");
            }
            try {
                mVirtualMachine.suspend();
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            } catch (ServiceSpecificException e) {
                throw new VirtualMachineException(e);
            }
        }
    }

    /** @hide */
    public void resume() throws VirtualMachineException {
        synchronized (mLock) {
            if (mVirtualMachine == null) {
                throw new VirtualMachineException("VM is not running");
            }
            try {
                mVirtualMachine.resume();
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            } catch (ServiceSpecificException e) {
                throw new VirtualMachineException(e);
            }
        }
    }

    /**
     * Stops this virtual machine, if it is running.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @see #stop()
     * @hide
     */
    @SystemApi
    @WorkerThread
    @Override
    public void close() {
        synchronized (mLock) {
            if (mVirtualMachine == null) {
                return;
            }
            try {
                if (stateToStatus(mVirtualMachine.getState()) == STATUS_RUNNING) {
                    mVirtualMachine.stop();
                    dropVm();
                }
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            } catch (ServiceSpecificException e) {
                // Deliberately ignored; this almost certainly means the VM exited just as
                // we tried to stop it.
                Log.i(TAG, "Ignoring error on close()", e);
            }
        }
    }

    private static void deleteRecursively(File dir) throws IOException {
        // Note: This doesn't follow symlinks, which is important. Instead they are just deleted
        // (and Files.delete deletes the link not the target).
        Files.walkFileTree(
                dir.toPath(),
                new SimpleFileVisitor<>() {
                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                            throws IOException {
                        Files.delete(file);
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult postVisitDirectory(Path dir, IOException e)
                            throws IOException {
                        // Directory is deleted after we've visited (deleted) all its contents, so
                        // it
                        // should be empty by now.
                        Files.delete(dir);
                        return FileVisitResult.CONTINUE;
                    }
                });
    }

    /**
     * Changes the config of this virtual machine to a new one. This can be used to adjust things
     * like the number of CPU and size of the RAM, depending on the situation (e.g. the size of the
     * application to run on the virtual machine, etc.)
     *
     * <p>The new config must be {@linkplain VirtualMachineConfig#isCompatibleWith compatible with}
     * the existing config.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @return the old config
     * @throws VirtualMachineException if the virtual machine is not stopped, or the new config is
     *     incompatible.
     * @hide
     */
    @SystemApi
    @WorkerThread
    @NonNull
    public VirtualMachineConfig setConfig(@NonNull VirtualMachineConfig newConfig)
            throws VirtualMachineException {
        synchronized (mLock) {
            VirtualMachineConfig oldConfig = mConfig;
            if (!oldConfig.isCompatibleWith(newConfig)) {
                throw new VirtualMachineException("incompatible config");
            }
            checkStopped();

            if (oldConfig != newConfig) {
                // Delete any existing file before recreating; that ensures any
                // VirtualMachineDescriptor that refers to the old file does not see the new config.
                mConfigFilePath.delete();
                newConfig.serialize(mConfigFilePath);
                mConfig = newConfig;
            }
            return oldConfig;
        }
    }

    @Nullable
    private static native IBinder nativeConnectToVsockServer(IBinder vmBinder, int port);

    /**
     * Connect to a VM's binder service via vsock and return the root IBinder object. Guest VMs are
     * expected to set up vsock servers in their payload. After the host app receives the {@link
     * VirtualMachineCallback#onPayloadReady}, it can use this method to establish a connection to
     * the guest VM.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @throws VirtualMachineException if the virtual machine is not running or the connection
     *     failed.
     * @hide
     */
    @SystemApi
    @WorkerThread
    @NonNull
    public IBinder connectToVsockServer(
            @IntRange(from = MIN_VSOCK_PORT, to = MAX_VSOCK_PORT) long port)
            throws VirtualMachineException {

        synchronized (mLock) {
            IBinder iBinder =
                    nativeConnectToVsockServer(getRunningVm().asBinder(), validatePort(port));
            if (iBinder == null) {
                throw new VirtualMachineException("Failed to connect to vsock server");
            }
            return iBinder;
        }
    }

    /**
     * Opens a vsock connection to the VM on the given port.
     *
     * <p>The caller is responsible for closing the returned {@code ParcelFileDescriptor}.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @throws VirtualMachineException if connecting fails.
     * @hide
     */
    @SystemApi
    @WorkerThread
    @NonNull
    public ParcelFileDescriptor connectVsock(
            @IntRange(from = MIN_VSOCK_PORT, to = MAX_VSOCK_PORT) long port)
            throws VirtualMachineException {
        synchronized (mLock) {
            try {
                return getRunningVm().connectVsock(validatePort(port));
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            } catch (ServiceSpecificException e) {
                throw new VirtualMachineException(e);
            }
        }
    }

    private int validatePort(long port) {
        // Ports below 1024 are "privileged" (payload code can't bind to these), and port numbers
        // are 32-bit unsigned numbers at the OS level, even though we pass them as 32-bit signed
        // numbers internally.
        if (port < MIN_VSOCK_PORT || port > MAX_VSOCK_PORT) {
            throw new IllegalArgumentException("Bad port " + port);
        }
        return (int) port;
    }

    /**
     * Returns the root directory where all files related to this {@link VirtualMachine} (e.g.
     * {@code instance.img}, {@code apk.idsig}, etc) are stored.
     *
     * @hide
     */
    @TestApi
    @NonNull
    public File getRootDir() {
        return mVmRootPath;
    }

    /**
     * Enables the VM to request attestation in testing mode.
     *
     * <p>This function provisions a key pair for the VM attestation testing, a fake certificate
     * will be associated to the fake key pair when the VM requests attestation in testing mode.
     *
     * <p>The provisioned key pair can only be used in subsequent calls to {@link
     * AVmPayload_requestAttestationForTesting} within a running VM.
     *
     * @hide
     */
    @TestApi
    @RequiresPermission(USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
    public void enableTestAttestation() throws VirtualMachineException {
        try {
            mVirtualizationService.getBinder().enableTestAttestation();
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
    }

    /**
     * Captures the current state of the VM in a {@link VirtualMachineDescriptor} instance. The VM
     * needs to be stopped to avoid inconsistency in its state representation.
     *
     * <p>The state of the VM is not actually copied until {@link
     * VirtualMachineManager#importFromDescriptor} is called. It is recommended that the VM not be
     * started until that operation is complete.
     *
     * <p>NOTE: This method may block and should not be called on the main thread.
     *
     * @return a {@link VirtualMachineDescriptor} instance that represents the VM's state.
     * @throws VirtualMachineException if the virtual machine is not stopped, or the state could not
     *     be captured.
     * @hide
     */
    @SystemApi
    @WorkerThread
    @NonNull
    public VirtualMachineDescriptor toDescriptor() throws VirtualMachineException {
        synchronized (mLock) {
            checkStopped();
            try {
                return new VirtualMachineDescriptor(
                        ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
                        mInstanceIdPath != null
                                ? ParcelFileDescriptor.open(mInstanceIdPath, MODE_READ_ONLY)
                                : null,
                        ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY),
                        mEncryptedStoreFilePath != null
                                ? ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_ONLY)
                                : null);
            } catch (IOException e) {
                throw new VirtualMachineException(e);
            }
        }
    }

    @Override
    public String toString() {
        VirtualMachineConfig config = getConfig();
        String payloadConfigPath = config.getPayloadConfigPath();
        String payloadBinaryName = config.getPayloadBinaryName();

        StringBuilder result = new StringBuilder();
        result.append("VirtualMachine(").append("name:").append(getName()).append(", ");
        if (payloadBinaryName != null) {
            result.append("payload:").append(payloadBinaryName).append(", ");
        }
        if (payloadConfigPath != null) {
            result.append("config:").append(payloadConfigPath).append(", ");
        }
        result.append("package: ").append(mPackageName).append(")");
        return result.toString();
    }

    /**
     * Reads the payload config inside the application, parses extra APK information, and then
     * creates corresponding idsig file paths.
     */
    private static List<ExtraApkSpec> setupExtraApks(
            @NonNull Context context, @NonNull VirtualMachineConfig config, @NonNull File vmDir)
            throws VirtualMachineException {
        String configPath = config.getPayloadConfigPath();
        List<String> extraApks = config.getExtraApks();
        if (configPath != null) {
            return setupExtraApksFromConfigFile(context, vmDir, configPath);
        } else if (!extraApks.isEmpty()) {
            return setupExtraApksFromList(context, vmDir, extraApks);
        } else {
            return Collections.emptyList();
        }
    }

    private static List<ExtraApkSpec> setupExtraApksFromConfigFile(
            Context context, File vmDir, String configPath) throws VirtualMachineException {
        try (ZipFile zipFile = new ZipFile(context.getPackageCodePath())) {
            InputStream inputStream = zipFile.getInputStream(zipFile.getEntry(configPath));
            List<String> apkList =
                    parseExtraApkListFromPayloadConfig(
                            new JsonReader(new InputStreamReader(inputStream)));

            List<ExtraApkSpec> extraApks = new ArrayList<>(apkList.size());
            for (int i = 0; i < apkList.size(); ++i) {
                extraApks.add(
                        new ExtraApkSpec(
                                new File(apkList.get(i)),
                                new File(vmDir, EXTRA_IDSIG_FILE_PREFIX + i)));
            }

            return extraApks;
        } catch (IOException e) {
            throw new VirtualMachineException("Couldn't parse extra apks from the vm config", e);
        }
    }

    private static List<String> parseExtraApkListFromPayloadConfig(JsonReader reader)
            throws VirtualMachineException {
        /*
         * JSON schema from packages/modules/Virtualization/microdroid/libs/libmicrodroid_payload_metadata/config/src/lib.rs:
         *
         * <p>{ "extra_apks": [ { "path": "/system/app/foo.apk", }, ... ], ... }
         */
        try {
            List<String> apks = new ArrayList<>();

            reader.beginObject();
            while (reader.hasNext()) {
                if (reader.nextName().equals("extra_apks")) {
                    reader.beginArray();
                    while (reader.hasNext()) {
                        reader.beginObject();
                        String name = reader.nextName();
                        if (name.equals("path")) {
                            apks.add(reader.nextString());
                        } else {
                            reader.skipValue();
                        }
                        reader.endObject();
                    }
                    reader.endArray();
                } else {
                    reader.skipValue();
                }
            }
            reader.endObject();
            return apks;
        } catch (IOException e) {
            throw new VirtualMachineException(e);
        }
    }

    private static List<ExtraApkSpec> setupExtraApksFromList(
            Context context, File vmDir, List<String> extraApkInfo) throws VirtualMachineException {
        int count = extraApkInfo.size();
        List<ExtraApkSpec> extraApks = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            String packageName = extraApkInfo.get(i);
            ApplicationInfo appInfo;
            try {
                appInfo =
                        context.getPackageManager()
                                .getApplicationInfo(
                                        packageName, PackageManager.ApplicationInfoFlags.of(0));
            } catch (PackageManager.NameNotFoundException e) {
                throw new VirtualMachineException("Extra APK package not found", e);
            }

            extraApks.add(
                    new ExtraApkSpec(
                            new File(appInfo.sourceDir),
                            new File(vmDir, EXTRA_IDSIG_FILE_PREFIX + i)));
        }
        return extraApks;
    }

    private void importInstanceIdFrom(@NonNull ParcelFileDescriptor instanceIdFd)
            throws VirtualMachineException {
        try (FileChannel idOutput = new FileOutputStream(mInstanceIdPath).getChannel();
                FileChannel idInput = new AutoCloseInputStream(instanceIdFd).getChannel()) {
            idOutput.transferFrom(idInput, /* position= */ 0, idInput.size());
        } catch (IOException e) {
            throw new VirtualMachineException("failed to copy instance_id", e);
        }
    }

    private void importInstanceFrom(@NonNull ParcelFileDescriptor instanceFd)
            throws VirtualMachineException {
        try (FileChannel instance = new FileOutputStream(mInstanceFilePath).getChannel();
                FileChannel instanceInput = new AutoCloseInputStream(instanceFd).getChannel()) {
            instance.transferFrom(instanceInput, /* position= */ 0, instanceInput.size());
        } catch (IOException e) {
            throw new VirtualMachineException("failed to transfer instance image", e);
        }
    }

    private void importEncryptedStoreFrom(@NonNull ParcelFileDescriptor encryptedStoreFd)
            throws VirtualMachineException {
        try (FileChannel storeOutput = new FileOutputStream(mEncryptedStoreFilePath).getChannel();
                FileChannel storeInput = new AutoCloseInputStream(encryptedStoreFd).getChannel()) {
            storeOutput.transferFrom(storeInput, /* position= */ 0, storeInput.size());
        } catch (IOException e) {
            throw new VirtualMachineException("failed to transfer encryptedstore image", e);
        }
    }

    /** Map the raw AIDL (& binder) callbacks to what the client expects. */
    private class CallbackTranslator extends IVirtualMachineCallback.Stub {
        private final IVirtualizationService mService;
        private final DeathRecipient mDeathRecipient;

        // The VM should only be observed to die once
        private final AtomicBoolean mOnDiedCalled = new AtomicBoolean(false);

        public CallbackTranslator(IVirtualizationService service) throws RemoteException {
            this.mService = service;
            this.mDeathRecipient = () -> reportStopped(STOP_REASON_VIRTUALIZATION_SERVICE_DIED);
            service.asBinder().linkToDeath(mDeathRecipient, 0);
        }

        @Override
        public void onPayloadStarted(int cid) {
            executeCallback((cb) -> cb.onPayloadStarted(VirtualMachine.this));
        }

        @Override
        public void onPayloadReady(int cid) {
            executeCallback((cb) -> cb.onPayloadReady(VirtualMachine.this));
        }

        @Override
        public void onPayloadFinished(int cid, int exitCode) {
            executeCallback((cb) -> cb.onPayloadFinished(VirtualMachine.this, exitCode));
        }

        @Override
        public void onError(int cid, int errorCode, String message) {
            int translatedError = getTranslatedError(errorCode);
            executeCallback((cb) -> cb.onError(VirtualMachine.this, translatedError, message));
        }

        @Override
        public void onDied(int cid, int reason) {
            int translatedReason = getTranslatedReason(reason);
            reportStopped(translatedReason);
            mService.asBinder().unlinkToDeath(mDeathRecipient, 0);
        }

        private void reportStopped(@VirtualMachineCallback.StopReason int reason) {
            if (mOnDiedCalled.compareAndSet(false, true)) {
                executeCallback((cb) -> cb.onStopped(VirtualMachine.this, reason));
            }
        }

        @VirtualMachineCallback.ErrorCode
        private int getTranslatedError(int reason) {
            switch (reason) {
                case ErrorCode.PAYLOAD_VERIFICATION_FAILED:
                    return ERROR_PAYLOAD_VERIFICATION_FAILED;
                case ErrorCode.PAYLOAD_CHANGED:
                    return ERROR_PAYLOAD_CHANGED;
                case ErrorCode.PAYLOAD_INVALID_CONFIG:
                    return ERROR_PAYLOAD_INVALID_CONFIG;
                default:
                    return ERROR_UNKNOWN;
            }
        }

        @VirtualMachineCallback.StopReason
        private int getTranslatedReason(int reason) {
            switch (reason) {
                case DeathReason.INFRASTRUCTURE_ERROR:
                    return STOP_REASON_INFRASTRUCTURE_ERROR;
                case DeathReason.KILLED:
                    return STOP_REASON_KILLED;
                case DeathReason.SHUTDOWN:
                    return STOP_REASON_SHUTDOWN;
                case DeathReason.START_FAILED:
                    return STOP_REASON_START_FAILED;
                case DeathReason.REBOOT:
                    return STOP_REASON_REBOOT;
                case DeathReason.CRASH:
                    return STOP_REASON_CRASH;
                case DeathReason.PVM_FIRMWARE_PUBLIC_KEY_MISMATCH:
                    return STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH;
                case DeathReason.PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED:
                    return STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED;
                case DeathReason.MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE:
                    return STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE;
                case DeathReason.MICRODROID_PAYLOAD_HAS_CHANGED:
                    return STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED;
                case DeathReason.MICRODROID_PAYLOAD_VERIFICATION_FAILED:
                    return STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED;
                case DeathReason.MICRODROID_INVALID_PAYLOAD_CONFIG:
                    return STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG;
                case DeathReason.MICRODROID_UNKNOWN_RUNTIME_ERROR:
                    return STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR;
                case DeathReason.HANGUP:
                    return STOP_REASON_HANGUP;
                default:
                    return STOP_REASON_UNKNOWN;
            }
        }
    }

    /**
     * Duplicates {@code InputStream} data to multiple {@code OutputStream}. Like the "tee" command.
     *
     * <p>Supports non-blocking writes to the output streams by ignoring EAGAIN error.
     */
    private static class TeeWorker implements Runnable {
        private final String mName;
        private final InputStream mIn;
        private final List<OutputStream> mOuts;

        TeeWorker(String name, InputStream in, Collection<OutputStream> outs) {
            mName = name;
            mIn = in;
            mOuts = new ArrayList<>(outs);
        }

        @Override
        public void run() {
            byte[] buffer = new byte[2048];
            try {
                while (!Thread.interrupted()) {
                    int len = mIn.read(buffer);
                    if (len < 0) {
                        break;
                    }
                    for (OutputStream out : mOuts) {
                        try {
                            out.write(buffer, 0, len);
                        } catch (IOException e) {
                            // EAGAIN is expected because the file description has O_NONBLOCK flag.
                            if (!isErrnoError(e, OsConstants.EAGAIN)) {
                                throw e;
                            }
                        }
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "Tee " + mName, e);
            }
        }

        private static ErrnoException asErrnoException(Throwable e) {
            if (e instanceof ErrnoException) {
                return (ErrnoException) e;
            } else if (e instanceof IOException) {
                // Try to unwrap ErrnoException#rethrowAsIOException()
                return asErrnoException(e.getCause());
            }
            return null;
        }

        private static boolean isErrnoError(Exception e, int expectedValue) {
            ErrnoException errno = asErrnoException(e);
            return errno != null && errno.errno == expectedValue;
        }
    }
}
