Support bring up multiple different CF instances on one host

Bug: 209690575
Test: tradefed.sh run singleCommand host --class com.android.tradefed.device.cloud.GceManagerTest -n
Change-Id: Idad3a0ce36e2fd98ce94b68fd076aa5607399a4c
diff --git a/device_build_interfaces/com/android/tradefed/device/IConfigurableIp.java b/device_build_interfaces/com/android/tradefed/device/IConfigurableVirtualDevice.java
similarity index 61%
rename from device_build_interfaces/com/android/tradefed/device/IConfigurableIp.java
rename to device_build_interfaces/com/android/tradefed/device/IConfigurableVirtualDevice.java
index e3547e8..b449641 100644
--- a/device_build_interfaces/com/android/tradefed/device/IConfigurableIp.java
+++ b/device_build_interfaces/com/android/tradefed/device/IConfigurableVirtualDevice.java
@@ -16,11 +16,24 @@
 
 package com.android.tradefed.device;
 
-/** An interface to provide information about a possibly preconfigured IP */
-public interface IConfigurableIp {
+/**
+ * An interface to provide information about a possibly preconfigured virtual device info (host ip,
+ * host user, ports offset and etc.).
+ */
+public interface IConfigurableVirtualDevice {
 
     /** Returns the known associated IP if available, returns null if no known ip. */
     default String getKnownDeviceIp() {
         return null;
     }
+
+    /** Returns the known user if available, returns null if no known user. */
+    default String getKnownUser() {
+        return null;
+    }
+
+    /** Returns the known device num offset if available, returns 0 if device num offset not set. */
+    default Integer getDeviceNumOffset() {
+        return 0;
+    }
 }
diff --git a/device_build_interfaces/com/android/tradefed/device/TcpDevice.java b/device_build_interfaces/com/android/tradefed/device/TcpDevice.java
index 1cb774c..c25d75d 100644
--- a/device_build_interfaces/com/android/tradefed/device/TcpDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/TcpDevice.java
@@ -21,7 +21,7 @@
  * A placeholder {@link IDevice} used by {@link DeviceManager} to allocate when {@link
  * DeviceSelectionOptions#tcpDeviceRequested()} is <code>true</code>
  */
-public class TcpDevice extends StubDevice implements IConfigurableIp {
+public class TcpDevice extends StubDevice implements IConfigurableVirtualDevice {
 
     private String mKnownDeviceIp = null;
 
diff --git a/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
index b9f4967..858c7f0 100644
--- a/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
+++ b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
@@ -674,16 +674,26 @@
         return mUseOxygen;
     }
 
-    /** Returns the instance type of GCE virtual device that should be created */
+    /** Returns the instance user of GCE virtual device that should be created */
     public String getInstanceUser() {
         return mInstanceUser;
     }
 
+    /** Set the instance user of GCE virtual device that should be created. */
+    public void setInstanceUser(String instanceUser) {
+        mInstanceUser = instanceUser;
+    }
+
     /** Returns the remote port in instance that the adb server listens to */
     public int getRemoteAdbPort() {
         return mRemoteAdbPort;
     }
 
+    /** Set the remote port in instance that the adb server listens to */
+    public void setRemoteAdbPort(int remoteAdbPort) {
+        mRemoteAdbPort = remoteAdbPort;
+    }
+
     /** Returns the base image name to be used for the current instance */
     public String getBaseImage() {
         return mBaseImage;
diff --git a/global_configuration/com/android/tradefed/host/HostOptions.java b/global_configuration/com/android/tradefed/host/HostOptions.java
index ea4cbc0..1d0fe52 100644
--- a/global_configuration/com/android/tradefed/host/HostOptions.java
+++ b/global_configuration/com/android/tradefed/host/HostOptions.java
@@ -119,6 +119,11 @@
             description = "The network interface used to connect to test devices.")
     private String mNetworkInterface = null;
 
+    @Option(
+            name = "preconfigured-virtual-device-pool",
+            description = "Preconfigured virtual device pool. (Value format: $hostname:$user.)")
+    private List<String> mPreconfiguredVirtualDevicePool = new ArrayList<>();
+
     private Map<PermitLimitType, Semaphore> mConcurrentLocks = new HashMap<>();
 
     /** {@inheritDoc} */
@@ -195,6 +200,12 @@
 
     /** {@inheritDoc} */
     @Override
+    public Set<String> getKnownPreconfigureVirtualDevicePool() {
+        return new HashSet<>(mPreconfiguredVirtualDevicePool);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public boolean getUseZip64InPartialDownload() {
         return mUseZip64InPartialDownload;
     }
diff --git a/global_configuration/com/android/tradefed/host/IHostOptions.java b/global_configuration/com/android/tradefed/host/IHostOptions.java
index 05f974f..5e3283a 100644
--- a/global_configuration/com/android/tradefed/host/IHostOptions.java
+++ b/global_configuration/com/android/tradefed/host/IHostOptions.java
@@ -82,6 +82,9 @@
     /** Known remote-device associated with a specific IP. */
     Set<String> getKnownRemoteDeviceIpPool();
 
+    /** Known preconfigured virtual device pool. */
+    Set<String> getKnownPreconfigureVirtualDevicePool();
+
     /** Check if it should use the zip64 format in partial download or not. */
     boolean getUseZip64InPartialDownload();
 
diff --git a/javatests/com/android/tradefed/device/cloud/GceManagerTest.java b/javatests/com/android/tradefed/device/cloud/GceManagerTest.java
index 23ea730..fd9a71d 100644
--- a/javatests/com/android/tradefed/device/cloud/GceManagerTest.java
+++ b/javatests/com/android/tradefed/device/cloud/GceManagerTest.java
@@ -176,7 +176,8 @@
         try {
             reportFile = FileUtil.createTempFile("test-gce-cmd", "report");
             List<String> result =
-                    mGceManager.buildGceCmd(reportFile, mockBuildInfo, null, stubAttributes);
+                    mGceManager.buildGceCmd(
+                            reportFile, mockBuildInfo, null, null, null, stubAttributes);
             List<String> expected =
                     ArrayUtil.list(
                             mOptions.getAvdDriverBinary().getAbsolutePath(),
@@ -215,7 +216,8 @@
         setter.setOptionValue("gce-driver-service-account-json-key-path", "/path/to/key.json");
         try {
             reportFile = FileUtil.createTempFile("test-gce-cmd", "report");
-            List<String> result = mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null);
+            List<String> result =
+                    mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null, null, null);
             List<String> expected =
                     ArrayUtil.list(
                             mOptions.getAvdDriverBinary().getAbsolutePath(),
@@ -256,7 +258,8 @@
         setter.setOptionValue("instance-user", "foo");
         try {
             reportFile = FileUtil.createTempFile("test-gce-cmd", "report");
-            List<String> result = mGceManager.buildGceCmd(reportFile, mMockBuildInfo, "bar", null);
+            List<String> result =
+                    mGceManager.buildGceCmd(reportFile, mMockBuildInfo, "bar", null, null, null);
             List<String> expected =
                     ArrayUtil.list(
                             mOptions.getAvdDriverBinary().getAbsolutePath(),
@@ -310,7 +313,8 @@
                         }
                     };
             reportFile = FileUtil.createTempFile("test-gce-cmd", "report");
-            List<String> result = mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null);
+            List<String> result =
+                    mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null, null, null);
             List<String> expected =
                     ArrayUtil.list(
                             mOptions.getAvdDriverBinary().getAbsolutePath(),
@@ -358,7 +362,8 @@
                         }
                     };
             reportFile = FileUtil.createTempFile("test-gce-cmd", "report");
-            List<String> result = mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null);
+            List<String> result =
+                    mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null, null, null);
             List<String> expected =
                     ArrayUtil.list(
                             mOptions.getAvdDriverBinary().getAbsolutePath(),
@@ -378,6 +383,60 @@
         }
     }
 
+    /**
+     * Test {@link GceManager#buildGceCmd(File, IBuildInfo, String, String, Integer,
+     * MultiMap<String, String>)} with preconfigured virtual device.
+     */
+    @Test
+    public void testBuildGceCommand_withPreconfiguredVirtualDevice() throws Exception {
+        IBuildInfo mMockBuildInfo = mock(IBuildInfo.class);
+        when(mMockBuildInfo.getBuildAttributes())
+                .thenReturn(Collections.<String, String>emptyMap());
+        when(mMockBuildInfo.getBuildFlavor()).thenReturn("FLAVOR");
+        when(mMockBuildInfo.getBuildBranch()).thenReturn("BRANCH");
+        when(mMockBuildInfo.getBuildId()).thenReturn("BUILDID");
+
+        File reportFile = null;
+        OptionSetter setter = new OptionSetter(mOptions);
+        setter.setOptionValue("gce-driver-service-account-json-key-path", "/path/to/key.json");
+        setter.setOptionValue("gce-private-key-path", "/path/to/id_rsa");
+
+        try {
+            reportFile = FileUtil.createTempFile("test-gce-cmd", "report");
+            List<String> result =
+                    mGceManager.buildGceCmd(reportFile, mMockBuildInfo, "bar", "vsoc-1", 2, null);
+            List<String> expected =
+                    ArrayUtil.list(
+                            mOptions.getAvdDriverBinary().getAbsolutePath(),
+                            "create",
+                            "--build-target",
+                            "FLAVOR",
+                            "--branch",
+                            "BRANCH",
+                            "--build-id",
+                            "BUILDID",
+                            "--config_file",
+                            mGceManager.getAvdConfigFile().getAbsolutePath(),
+                            "--service-account-json-private-key-path",
+                            "/path/to/key.json",
+                            "--host",
+                            "bar",
+                            "--host-user",
+                            "vsoc-1",
+                            "--host-ssh-private-key-path",
+                            "/path/to/id_rsa",
+                            "--report_file",
+                            reportFile.getAbsolutePath(),
+                            "--base-instance-num",
+                            "3",
+                            "--launch-args=\"--base_instance_num=3\"",
+                            "-v");
+            assertEquals(expected, result);
+        } finally {
+            FileUtil.deleteFile(reportFile);
+        }
+    }
+
     /** Test {@link GceManager#buildGceCmd(File, IBuildInfo, String)}. */
     @Test
     public void testBuildGceCommandWithGceDriverParam() throws Exception {
@@ -394,7 +453,8 @@
         setter.setOptionValue("gce-driver-param", "--no-autoconnect");
         try {
             reportFile = FileUtil.createTempFile("test-gce-cmd", "report");
-            List<String> result = mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null);
+            List<String> result =
+                    mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null, null, null);
             List<String> expected =
                     ArrayUtil.list(
                             mOptions.getAvdDriverBinary().getAbsolutePath(),
@@ -444,7 +504,8 @@
                         }
                     };
             reportFile = FileUtil.createTempFile("test-gce-cmd", "report");
-            List<String> result = mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null);
+            List<String> result =
+                    mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null, null, null);
             List<String> expected =
                     ArrayUtil.list(
                             mOptions.getAvdDriverBinary().getAbsolutePath(),
@@ -492,6 +553,8 @@
                             File reportFile,
                             IBuildInfo b,
                             String ipDevice,
+                            String user,
+                            Integer offset,
                             MultiMap<String, String> attributes) {
                         List<String> tmp = new ArrayList<String>();
                         tmp.add("");
@@ -535,7 +598,8 @@
             setter.setOptionValue("gce-driver-param", "--kernel-build-id");
             setter.setOptionValue("gce-driver-param", "KERNELBUILDID");
             reportFile = FileUtil.createTempFile("test-gce-cmd", "report");
-            List<String> result = mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null);
+            List<String> result =
+                    mGceManager.buildGceCmd(reportFile, mMockBuildInfo, null, null, null, null);
             List<String> expected =
                     ArrayUtil.list(
                             mOptions.getAvdDriverBinary().getAbsolutePath(),
@@ -579,6 +643,8 @@
                             File reportFile,
                             IBuildInfo b,
                             String ipDevice,
+                            String user,
+                            Integer offset,
                             MultiMap<String, String> attributes) {
                         String valid =
                                 " {\n"
@@ -635,6 +701,8 @@
                             File reportFile,
                             IBuildInfo b,
                             String ipDevice,
+                            String user,
+                            Integer offset,
                             MultiMap<String, String> attributes) {
                         // We delete the potential report file to create an issue.
                         FileUtil.deleteFile(reportFile);
@@ -675,6 +743,8 @@
                             File reportFile,
                             IBuildInfo b,
                             String ipDevice,
+                            String user,
+                            Integer offset,
                             MultiMap<String, String> attributes) {
                         String validFail =
                                 " {\n"
@@ -1028,6 +1098,8 @@
                             File reportFile,
                             IBuildInfo b,
                             String ipDevice,
+                            String user,
+                            Integer offset,
                             MultiMap<String, String> attributes) {
                         // We delete the potential report file to create an issue.
                         FileUtil.deleteFile(reportFile);
diff --git a/javatests/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java b/javatests/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
index a0d9050..c533590 100644
--- a/javatests/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
+++ b/javatests/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
@@ -183,6 +183,8 @@
                                     File reportFile,
                                     IBuildInfo b,
                                     String ipDevice,
+                                    String user,
+                                    Integer offset,
                                     MultiMap<String, String> attributes) {
                                 FileUtil.deleteFile(reportFile);
                                 List<String> tmp = new ArrayList<String>();
@@ -406,7 +408,7 @@
                                 "acloud error",
                                 GceStatus.BOOT_FAIL))
                 .when(mGceHandler)
-                .startGce(null, null);
+                .startGce(null, null, null, null);
 
         try {
             mTestDevice.launchGce(new BuildInfo(), null);
@@ -446,7 +448,7 @@
                 };
         doReturn(new GceAvdInfo("ins-name", null, null, "acloud error", GceStatus.BOOT_FAIL))
                 .when(mGceHandler)
-                .startGce(null, null);
+                .startGce(null, null, null, null);
 
         when(mMockStateMonitor.waitForDeviceNotAvailable(Mockito.anyLong())).thenReturn(true);
 
@@ -538,7 +540,7 @@
                                     null,
                                     GceStatus.SUCCESS))
                     .when(mGceHandler)
-                    .startGce(null, null);
+                    .startGce(null, null, null, null);
 
             // Run device a first time
             mTestDevice.preInvocationSetup(mMockBuildInfo, null);
@@ -639,7 +641,7 @@
                                     null,
                                     GceStatus.SUCCESS))
                     .when(mGceHandler)
-                    .startGce(null, null);
+                    .startGce(null, null, null, null);
 
             // Run device a first time
             mTestDevice.preInvocationSetup(mMockBuildInfo, null);
@@ -726,7 +728,7 @@
                                     null,
                                     GceStatus.SUCCESS))
                     .when(mGceHandler)
-                    .startGce(null, null);
+                    .startGce(null, null, null, null);
 
             CommandResult bugreportzResult = new CommandResult(CommandStatus.SUCCESS);
             bugreportzResult.setStdout("OK: bugreportz-file");
@@ -852,7 +854,7 @@
                         null,
                         null,
                         GceStatus.SUCCESS);
-        doReturn(gceAvd).when(mGceHandler).startGce(null, null);
+        doReturn(gceAvd).when(mGceHandler).startGce(null, null, null, null);
         OutputStream stdout = null;
         OutputStream stderr = null;
         CommandResult powerwashCmdResult = new CommandResult(CommandStatus.SUCCESS);
@@ -918,7 +920,7 @@
                         null,
                         null,
                         GceStatus.SUCCESS);
-        doReturn(gceAvd).when(mGceHandler).startGce(null, null);
+        doReturn(gceAvd).when(mGceHandler).startGce(null, null, null, null);
         OutputStream stdout = null;
         OutputStream stderr = null;
         CommandResult locateCmdResult = new CommandResult(CommandStatus.SUCCESS);
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index f7e8977..dbfc9d4 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -545,6 +545,32 @@
             index++;
         }
 
+        Map<String, List<String>> preconfigureHostUsers = new HashMap<>();
+        for (String preconfigureDevice :
+                getGlobalConfig().getHostOptions().getKnownPreconfigureVirtualDevicePool()) {
+            // Expect the preconfigureDevice string in a certain format($hostname:$user).
+            //  hostname.google.com:vsoc-1
+            String[] parts = preconfigureDevice.split(":", 2);
+            preconfigureHostUsers.putIfAbsent(parts[0], new ArrayList<>());
+            preconfigureHostUsers.get(parts[0]).add(parts[1]);
+        }
+        for (Map.Entry<String, List<String>> hostUsers : preconfigureHostUsers.entrySet()) {
+            for (int i = 0; i < hostUsers.getValue().size(); i++) {
+                addAvailableDevice(
+                        new RemoteAvdIDevice(
+                                String.format(
+                                        "%s-%d-%s-%s",
+                                        GCE_DEVICE_SERIAL_PREFIX,
+                                        index,
+                                        hostUsers.getKey(),
+                                        hostUsers.getValue().get(i)),
+                                hostUsers.getKey(),
+                                hostUsers.getValue().get(i),
+                                i));
+                index++;
+            }
+        }
+
         index = mNumRemoteDevicesSupported;
         for (String ip : getGlobalConfig().getHostOptions().getKnownRemoteDeviceIpPool()) {
             addAvailableDevice(
diff --git a/src/com/android/tradefed/device/RemoteAndroidDevice.java b/src/com/android/tradefed/device/RemoteAndroidDevice.java
index 7fbd336..9a86960 100644
--- a/src/com/android/tradefed/device/RemoteAndroidDevice.java
+++ b/src/com/android/tradefed/device/RemoteAndroidDevice.java
@@ -53,6 +53,8 @@
     private File mAdbConnectLogs = null;
     private String mInitialSerial;
     private String mInitialIpDevice;
+    private String mInitialUser;
+    private Integer mInitialDeviceNumOffset;
 
     /**
      * Creates a {@link RemoteAndroidDevice}.
@@ -64,8 +66,11 @@
     public RemoteAndroidDevice(IDevice device, IDeviceStateMonitor stateMonitor,
             IDeviceMonitor allocationMonitor) {
         super(device, stateMonitor, allocationMonitor);
-        if (getIDevice() instanceof IConfigurableIp) {
-            mInitialIpDevice = ((IConfigurableIp) getIDevice()).getKnownDeviceIp();
+        if (getIDevice() instanceof IConfigurableVirtualDevice) {
+            mInitialIpDevice = ((IConfigurableVirtualDevice) getIDevice()).getKnownDeviceIp();
+            mInitialUser = ((IConfigurableVirtualDevice) getIDevice()).getKnownUser();
+            mInitialDeviceNumOffset =
+                    ((IConfigurableVirtualDevice) getIDevice()).getDeviceNumOffset();
         }
         mInitialSerial = getSerialNumber();
     }
@@ -328,6 +333,16 @@
         return mInitialIpDevice;
     }
 
+    /** Returns the initial known user if any. Returns null if no initial known user. */
+    protected String getInitialUser() {
+        return mInitialUser;
+    }
+
+    /** Returns the known device num offset if any. Returns null if not available. */
+    protected Integer getInitialDeviceNumOffset() {
+        return mInitialDeviceNumOffset;
+    }
+
     /** Returns the initial serial name of the device. */
     protected String getInitialSerial() {
         return mInitialSerial;
diff --git a/src/com/android/tradefed/device/RemoteAvdIDevice.java b/src/com/android/tradefed/device/RemoteAvdIDevice.java
index 18d2d19..846fbec 100644
--- a/src/com/android/tradefed/device/RemoteAvdIDevice.java
+++ b/src/com/android/tradefed/device/RemoteAvdIDevice.java
@@ -23,6 +23,9 @@
  */
 public class RemoteAvdIDevice extends TcpDevice {
 
+    protected String mUser = null;
+    protected Integer mDeviceNumOffset = null;
+
     /** @param serial placeholder for the real serial */
     public RemoteAvdIDevice(String serial) {
         super(serial);
@@ -31,4 +34,22 @@
     public RemoteAvdIDevice(String serial, String knowDeviceIp) {
         super(serial, knowDeviceIp);
     }
+
+    public RemoteAvdIDevice(String serial, String knowDeviceIp, String user, Integer offset) {
+        super(serial, knowDeviceIp);
+        this.mUser = user;
+        this.mDeviceNumOffset = offset;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getKnownUser() {
+        return mUser;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Integer getDeviceNumOffset() {
+        return mDeviceNumOffset;
+    }
 }
diff --git a/src/com/android/tradefed/device/cloud/GceManager.java b/src/com/android/tradefed/device/cloud/GceManager.java
index 2fa6146..501e79d 100644
--- a/src/com/android/tradefed/device/cloud/GceManager.java
+++ b/src/com/android/tradefed/device/cloud/GceManager.java
@@ -151,11 +151,11 @@
     }
 
     public GceAvdInfo startGce() throws TargetSetupError {
-        return startGce(null, null);
+        return startGce(null, null, null, null);
     }
 
     /**
-     * Attempt to start a gce instance
+     * Attempt to start a gce instance.
      *
      * @param ipDevice the initial IP of the GCE instance to run AVD in, <code>null</code> if not
      *     applicable
@@ -166,6 +166,25 @@
      */
     public GceAvdInfo startGce(String ipDevice, MultiMap<String, String> attributes)
             throws TargetSetupError {
+        return startGce(ipDevice, null, null, attributes);
+    }
+
+    /**
+     * Attempt to start a gce instance
+     *
+     * @param ipDevice the initial IP of the GCE instance to run AVD in, <code>null</code> if not
+     *     applicable
+     * @param user the host running user of AVD, <code>null</code> if not applicable
+     * @param offset the device num offset of the AVD in the host, <code>null</code> if not
+     *     applicable
+     * @param attributes attributes associated with current invocation, used for passing applicable
+     *     information down to the GCE instance to be added as VM metadata
+     * @return a {@link GceAvdInfo} describing the GCE instance. Could be a BOOT_FAIL instance.
+     * @throws TargetSetupError
+     */
+    public GceAvdInfo startGce(
+            String ipDevice, String user, Integer offset, MultiMap<String, String> attributes)
+            throws TargetSetupError {
         mGceAvdInfo = null;
         // For debugging purposes bypass.
         if (mGceHost != null && mGceInstanceName != null) {
@@ -189,7 +208,12 @@
         File reportFile = null;
         try {
             reportFile = FileUtil.createTempFile("gce_avd_driver", ".json");
-            List<String> gceArgs = buildGceCmd(reportFile, mBuildInfo, ipDevice, attributes);
+            // Override the instance name by specified user
+            if (user != null) {
+                getTestDeviceOptions().setInstanceUser(user);
+            }
+            List<String> gceArgs =
+                    buildGceCmd(reportFile, mBuildInfo, ipDevice, user, offset, attributes);
 
             long driverTimeoutMs = getTestDeviceOptions().getGceCmdTimeout();
             if (!getTestDeviceOptions().allowGceCmdTimeoutOverride()) {
@@ -296,7 +320,12 @@
 
     /** Build and return the command to launch GCE. Exposed for testing. */
     protected List<String> buildGceCmd(
-            File reportFile, IBuildInfo b, String ipDevice, MultiMap<String, String> attributes) {
+            File reportFile,
+            IBuildInfo b,
+            String ipDevice,
+            String user,
+            Integer offset,
+            MultiMap<String, String> attributes) {
         File avdDriverFile = getTestDeviceOptions().getAvdDriverBinary();
         if (!avdDriverFile.exists()) {
             throw new HarnessRuntimeException(
@@ -394,16 +423,30 @@
             gceArgs.add("--service-account-json-private-key-path");
             gceArgs.add(getTestDeviceOptions().getServiceAccountJsonKeyFile().getAbsolutePath());
         }
+
         if (ipDevice != null) {
             gceArgs.add("--host");
             gceArgs.add(ipDevice);
             gceArgs.add("--host-user");
-            gceArgs.add(getTestDeviceOptions().getInstanceUser());
+            if (user != null) {
+                gceArgs.add(user);
+            } else {
+                gceArgs.add(getTestDeviceOptions().getInstanceUser());
+            }
             gceArgs.add("--host-ssh-private-key-path");
             gceArgs.add(getTestDeviceOptions().getSshPrivateKeyPath().getAbsolutePath());
         }
         gceArgs.add("--report_file");
         gceArgs.add(reportFile.getAbsolutePath());
+
+        // Add base-instance-num args with offset, and override the remote adb port.
+        // When offset is 1, base-instance-num=2 and virtual device adb forward port is 6521.
+        if (offset != null) {
+            getTestDeviceOptions().setRemoteAdbPort(6520 + offset);
+            gceArgs.add("--base-instance-num");
+            gceArgs.add(String.valueOf(offset + 1));
+            gceArgs.add("--launch-args=\"" + "--base_instance_num=" + (offset + 1) + "\"");
+        }
         switch (getTestDeviceOptions().getGceDriverLogLevel()) {
             case DEBUG:
                 gceArgs.add("-v");
diff --git a/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java b/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
index dc97da4..6a920ec 100644
--- a/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
+++ b/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
@@ -26,7 +26,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.IDeviceMonitor;
 import com.android.tradefed.device.IDeviceStateMonitor;
-import com.android.tradefed.device.IConfigurableIp;
+import com.android.tradefed.device.IConfigurableVirtualDevice;
 import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.device.TestDevice;
 import com.android.tradefed.device.TestDeviceOptions;
@@ -175,10 +175,20 @@
         TargetSetupError exception = null;
         for (int attempt = 0; attempt < getOptions().getGceMaxAttempt(); attempt++) {
             try {
+                CLog.i(
+                        "Launch AVD on %s by user %s (Device offset: %d).",
+                        ((IConfigurableVirtualDevice) getIDevice()).getKnownDeviceIp(),
+                        ((IConfigurableVirtualDevice) getIDevice()).getKnownUser(),
+                        ((IConfigurableVirtualDevice) getIDevice()).getDeviceNumOffset());
+
                 mGceAvd =
                         getGceHandler()
                                 .startGce(
-                                        ((IConfigurableIp) getIDevice()).getKnownDeviceIp(),
+                                        ((IConfigurableVirtualDevice) getIDevice())
+                                                .getKnownDeviceIp(),
+                                        ((IConfigurableVirtualDevice) getIDevice()).getKnownUser(),
+                                        ((IConfigurableVirtualDevice) getIDevice())
+                                                .getDeviceNumOffset(),
                                         attributes);
                 if (mGceAvd != null) {
                     break;
diff --git a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
index 0d58038..01e39fd 100644
--- a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
@@ -228,7 +228,12 @@
             mGceAvd = null;
 
             if (getInitialSerial() != null) {
-                setIDevice(new RemoteAvdIDevice(getInitialSerial(), getInitialIp()));
+                setIDevice(
+                        new RemoteAvdIDevice(
+                                getInitialSerial(),
+                                getInitialIp(),
+                                getInitialUser(),
+                                getInitialDeviceNumOffset()));
             }
             setFastbootEnabled(false);
 
@@ -272,7 +277,13 @@
         TargetSetupError exception = null;
         for (int attempt = 0; attempt < getOptions().getGceMaxAttempt(); attempt++) {
             try {
-                mGceAvd = getGceHandler().startGce(getInitialIp(), attributes);
+                mGceAvd =
+                        getGceHandler()
+                                .startGce(
+                                        getInitialIp(),
+                                        getInitialUser(),
+                                        getInitialDeviceNumOffset(),
+                                        attributes);
                 if (mGceAvd != null) {
                     break;
                 }
diff --git a/src/com/android/tradefed/device/cloud/VmRemoteDevice.java b/src/com/android/tradefed/device/cloud/VmRemoteDevice.java
index d52f4a5..a5aeab9 100644
--- a/src/com/android/tradefed/device/cloud/VmRemoteDevice.java
+++ b/src/com/android/tradefed/device/cloud/VmRemoteDevice.java
@@ -15,11 +15,11 @@
  */
 package com.android.tradefed.device.cloud;
 
-import com.android.tradefed.device.IConfigurableIp;
+import com.android.tradefed.device.IConfigurableVirtualDevice;
 import com.android.tradefed.device.StubDevice;
 
 /** A Remote virtual device that we will manage from inside the Virtual Machine. */
-public class VmRemoteDevice extends StubDevice implements IConfigurableIp {
+public class VmRemoteDevice extends StubDevice implements IConfigurableVirtualDevice {
 
     private String mKnownDeviceIp = null;