Keep track of launching cloud devices for devices table.

Change-Id: I3b5ac07d76877eba85960fe3922c443a91ad27c5
diff --git a/src/com/google/gct/testing/CloudConfigurationProviderImpl.java b/src/com/google/gct/testing/CloudConfigurationProviderImpl.java
index e71b84e..02dc01d 100644
--- a/src/com/google/gct/testing/CloudConfigurationProviderImpl.java
+++ b/src/com/google/gct/testing/CloudConfigurationProviderImpl.java
@@ -15,10 +15,12 @@
  */
 package com.google.gct.testing;
 
+import com.android.ddmlib.IDevice;
 import com.android.tools.idea.run.CloudConfiguration;
 import com.android.tools.idea.run.CloudConfiguration.Kind;
 import com.android.tools.idea.run.CloudConfigurationProvider;
 import com.android.tools.idea.sdk.IdeSdks;
+import com.google.api.client.util.Sets;
 import com.google.api.services.storage.Storage;
 import com.google.api.services.storage.model.Buckets;
 import com.google.api.services.testing.model.AndroidDevice;
@@ -37,6 +39,7 @@
 import com.google.gct.testing.results.GoogleCloudTestListener;
 import com.google.gct.testing.results.GoogleCloudTestResultsConnectionUtil;
 import com.google.gct.testing.results.GoogleCloudTestingResultParser;
+import com.google.gct.testing.ui.GhostCloudDevice;
 import com.intellij.execution.DefaultExecutionResult;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.ExecutionResult;
@@ -89,6 +92,9 @@
       }
     };
 
+
+  private final Set<GhostCloudDevice> ghostCloudDevices = Sets.newHashSet();
+
   public static Map<String, List<? extends CloudTestingType>> getAllDimensionTypes() {
     Map<String, List<? extends CloudTestingType>> dimensionTypes = new HashMap<String, List<? extends CloudTestingType>>();
     dimensionTypes.put(DeviceDimension.DISPLAY_NAME, DeviceDimension.getFullDomain());
@@ -280,37 +286,58 @@
                                                                                  "The returned cloud device is null\n\n");
     }
 
+    GhostCloudDevice ghostCloudDevice = new GhostCloudDevice(createdDevice);
+    synchronized (ghostCloudDevices) {
+      ghostCloudDevices.add(ghostCloudDevice);
+    }
     final long POLLING_INTERVAL = 10 * 1000; // 10 seconds
     final long INITIAL_TIMEOUT = 10 * 60 * 1000; // 10 minutes
     long stopTime = System.currentTimeMillis() + INITIAL_TIMEOUT;
     String sdkPath = IdeSdks.getAndroidSdkPath().getAbsolutePath() + "/platform-tools";
     File dir = new File(sdkPath);
-    while (System.currentTimeMillis() < stopTime) {
-      try {
+    try {
+      while (System.currentTimeMillis() < stopTime) {
         createdDevice = getTest().projects().devices().get(cloudProjectId, createdDevice.getId()).execute();
         System.out.println("Polling for device... (time: " + System.currentTimeMillis() + ", status: " + createdDevice.getState() + ")");
         if (createdDevice.getState().equals("READY")) {
-        //if (createdDevice.getDeviceDetails().getConnectionInfo() != null) {
+          //if (createdDevice.getDeviceDetails().getConnectionInfo() != null) {
           String ipAddress = createdDevice.getDeviceDetails().getConnectionInfo().getIpAddress();
           Integer adbPort = createdDevice.getDeviceDetails().getConnectionInfo().getAdbPort();
           System.out.println("Device ready with IP address:port " + ipAddress + ":" + adbPort);
           Runtime rt = Runtime.getRuntime();
-          //Process startServer = rt.exec("./adb start-server", null, dir);
-          //startServer.waitFor();
           Process connect = rt.exec("./adb connect " + ipAddress + ":" + adbPort, null, dir);
           connect.waitFor();
+          // Do not wait for "finally" to remove the ghost device
+          // to minimize the time both a ghost device and an actual cloud device are present in the devices table.
+          synchronized (ghostCloudDevices) {
+            ghostCloudDevices.remove(ghostCloudDevice);
+          }
           return;
         }
         Thread.sleep(POLLING_INTERVAL);
-      } catch (IOException e) {
-        showCloudDevicePollingError(e, createdDevice.getId());
-      } catch (InterruptedException e) {
-        showCloudDevicePollingError(e, createdDevice.getId());
+      }
+      CloudTestingUtils.showErrorMessage(null, "Timed out connecting to a cloud device", "Timed out connecting to a cloud device!\n" +
+                                                                                         "Timed out connecting to a cloud device:\n\n" +
+                                                                                         createdDevice.getId());
+    } catch (IOException e) {
+      showCloudDevicePollingError(e, createdDevice.getId());
+    } catch (InterruptedException e) {
+      showCloudDevicePollingError(e, createdDevice.getId());
+    } finally {
+      synchronized (ghostCloudDevices) {
+        ghostCloudDevices.remove(ghostCloudDevice);
       }
     }
-    CloudTestingUtils.showErrorMessage(null, "Timed out connecting to a cloud device", "Timed out connecting to a cloud device!\n" +
-                                                                                       "Timed out connecting to a cloud device:\n\n" +
-                                                                                       createdDevice.getId());
+  }
+
+  @NotNull
+  @Override
+  public Collection<IDevice> getLaunchingCloudDevices() {
+    synchronized (ghostCloudDevices) {
+      HashSet<IDevice> launchingCloudDevices = Sets.newHashSet();
+      launchingCloudDevices.addAll(ghostCloudDevices);
+      return launchingCloudDevices;
+    }
   }
 
   private void showCloudDevicePollingError(Exception e, String deviceId) {
diff --git a/src/com/google/gct/testing/ui/GhostCloudDevice.java b/src/com/google/gct/testing/ui/GhostCloudDevice.java
new file mode 100644
index 0000000..5f2c81d
--- /dev/null
+++ b/src/com/google/gct/testing/ui/GhostCloudDevice.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.gct.testing.ui;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ddmlib.*;
+import com.android.ddmlib.log.LogReceiver;
+import com.google.api.services.testing.model.AndroidDevice;
+import com.google.api.services.testing.model.Device;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+public class GhostCloudDevice implements IDevice {
+
+  private final Device device;
+
+
+  public GhostCloudDevice(Device device) {
+    this.device = device;
+  }
+
+  @NonNull
+  @Override
+  public String getSerialNumber() {
+    AndroidDevice androidDevice = device.getAndroidDevice();
+    return (androidDevice.getAndroidModelId() + "-" + androidDevice.getAndroidVersionId() + "-" +
+           androidDevice.getLocale() + "-" + androidDevice.getOrientation()).toLowerCase();
+  }
+
+  @Nullable
+  @Override
+  public String getAvdName() {
+    return null;
+  }
+
+  @Override
+  public DeviceState getState() {
+    return DeviceState.OFFLINE;
+  }
+
+  @Override
+  public Map<String, String> getProperties() {
+    return null;
+  }
+
+  @Override
+  public int getPropertyCount() {
+    return 0;
+  }
+
+  @Nullable
+  @Override
+  public String getProperty(@NonNull String name) {
+    if (name.equals(IDevice.PROP_BUILD_API_LEVEL)) {
+      return device.getAndroidDevice().getAndroidVersionId();
+    }
+    return null;
+  }
+
+  @Override
+  public boolean arePropertiesSet() {
+    return false;
+  }
+
+  @Override
+  public String getPropertySync(String name)
+    throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
+    return null;
+  }
+
+  @Override
+  public String getPropertyCacheOrSync(String name)
+    throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
+    return null;
+  }
+
+  @Override
+  public boolean supportsFeature(@NonNull Feature feature) {
+    return true;
+  }
+
+  @Override
+  public boolean supportsFeature(@NonNull HardwareFeature feature) {
+    if (feature != IDevice.HardwareFeature.WATCH) {
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public String getMountPoint(String name) {
+    return null;
+  }
+
+  @Override
+  public boolean isOnline() {
+    return false;
+  }
+
+  @Override
+  public boolean isEmulator() {
+    return false;
+  }
+
+  @Override
+  public boolean isOffline() {
+    return true;
+  }
+
+  @Override
+  public boolean isBootLoader() {
+    return false;
+  }
+
+  @Override
+  public boolean hasClients() {
+    return false;
+  }
+
+  @Override
+  public Client[] getClients() {
+    return new Client[0];
+  }
+
+  @Override
+  public Client getClient(String applicationName) {
+    return null;
+  }
+
+  @Override
+  public SyncService getSyncService() throws TimeoutException, AdbCommandRejectedException, IOException {
+    return null;
+  }
+
+  @Override
+  public FileListingService getFileListingService() {
+    return null;
+  }
+
+  @Override
+  public RawImage getScreenshot() throws TimeoutException, AdbCommandRejectedException, IOException {
+    return null;
+  }
+
+  @Override
+  public RawImage getScreenshot(long timeout, TimeUnit unit) throws TimeoutException, AdbCommandRejectedException, IOException {
+    return null;
+  }
+
+  @Override
+  public void startScreenRecorder(@NonNull String remoteFilePath,
+                                  @NonNull ScreenRecorderOptions options,
+                                  @NonNull IShellOutputReceiver receiver)
+    throws TimeoutException, AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException {
+
+  }
+
+  @Override
+  public void executeShellCommand(String command, IShellOutputReceiver receiver, int maxTimeToOutputResponse)
+    throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
+
+  }
+
+  @Override
+  public void executeShellCommand(String command, IShellOutputReceiver receiver)
+    throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
+
+  }
+
+  @Override
+  public void runEventLogService(LogReceiver receiver) throws TimeoutException, AdbCommandRejectedException, IOException {
+
+  }
+
+  @Override
+  public void runLogService(String logname, LogReceiver receiver) throws TimeoutException, AdbCommandRejectedException, IOException {
+
+  }
+
+  @Override
+  public void createForward(int localPort, int remotePort) throws TimeoutException, AdbCommandRejectedException, IOException {
+
+  }
+
+  @Override
+  public void createForward(int localPort, String remoteSocketName, DeviceUnixSocketNamespace namespace)
+    throws TimeoutException, AdbCommandRejectedException, IOException {
+
+  }
+
+  @Override
+  public void removeForward(int localPort, int remotePort) throws TimeoutException, AdbCommandRejectedException, IOException {
+
+  }
+
+  @Override
+  public void removeForward(int localPort, String remoteSocketName, DeviceUnixSocketNamespace namespace)
+    throws TimeoutException, AdbCommandRejectedException, IOException {
+
+  }
+
+  @Override
+  public String getClientName(int pid) {
+    return null;
+  }
+
+  @Override
+  public void pushFile(String local, String remote) throws IOException, AdbCommandRejectedException, TimeoutException, SyncException {
+
+  }
+
+  @Override
+  public void pullFile(String remote, String local) throws IOException, AdbCommandRejectedException, TimeoutException, SyncException {
+
+  }
+
+  @Override
+  public String installPackage(String packageFilePath, boolean reinstall, String... extraArgs) throws InstallException {
+    return null;
+  }
+
+  @Override
+  public void installPackages(List<String> apkFilePaths, int timeOutInMs, boolean reinstall, String... extraArgs) throws InstallException {
+
+  }
+
+  @Override
+  public String syncPackageToDevice(String localFilePath) throws TimeoutException, AdbCommandRejectedException, IOException, SyncException {
+    return null;
+  }
+
+  @Override
+  public String installRemotePackage(String remoteFilePath, boolean reinstall, String... extraArgs) throws InstallException {
+    return null;
+  }
+
+  @Override
+  public void removeRemotePackage(String remoteFilePath) throws InstallException {
+
+  }
+
+  @Override
+  public String uninstallPackage(String packageName) throws InstallException {
+    return null;
+  }
+
+  @Override
+  public void reboot(String into) throws TimeoutException, AdbCommandRejectedException, IOException {
+
+  }
+
+  @Override
+  public Integer getBatteryLevel() throws TimeoutException, AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException {
+    return null;
+  }
+
+  @Override
+  public Integer getBatteryLevel(long freshnessMs)
+    throws TimeoutException, AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException {
+    return null;
+  }
+
+  @NonNull
+  @Override
+  public Future<Integer> getBattery() {
+    return null;
+  }
+
+  @NonNull
+  @Override
+  public Future<Integer> getBattery(long freshnessTime, @NonNull TimeUnit timeUnit) {
+    return null;
+  }
+
+  @NonNull
+  @Override
+  public List<String> getAbis() {
+    return null;
+  }
+
+  @Override
+  public int getDensity() {
+    return 0;
+  }
+
+  @Override
+  public String getLanguage() {
+    return null;
+  }
+
+  @Override
+  public String getRegion() {
+    return null;
+  }
+
+  @Override
+  public String getName() {
+    return "Cloud device: " + device.getId();
+  }
+
+  @Override
+  public void executeShellCommand(String command, IShellOutputReceiver receiver, long maxTimeToOutputResponse, TimeUnit maxTimeUnits)
+    throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
+
+  }
+
+  @NonNull
+  @Override
+  public Future<String> getSystemProperty(@NonNull String name) {
+    return null;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    GhostCloudDevice that = (GhostCloudDevice)o;
+
+    if (device == null) {
+      return that.device == null;
+    }
+
+    if (that.device == null) {
+      return false;
+    }
+
+    return device.getId().equals(that.device.getId());
+  }
+
+  @Override
+  public int hashCode() {
+    return device != null ? device.getId().hashCode() : 0;
+  }
+}