Add intelligent VNC keep alive thread.

The thread will start VNC and will restart it when it crashes.
Also, it will clean up after the user closes the VNC Viewer
manually.

Change-Id: I16e40e92fcfc2ab3e82ea8ef55765d485046261e
diff --git a/src/com/google/gct/testing/CloudConfigurationProviderImpl.java b/src/com/google/gct/testing/CloudConfigurationProviderImpl.java
index 23d7386..f0f04e7 100644
--- a/src/com/google/gct/testing/CloudConfigurationProviderImpl.java
+++ b/src/com/google/gct/testing/CloudConfigurationProviderImpl.java
@@ -25,7 +25,6 @@
 import com.android.tools.idea.run.CloudConfigurationProvider;
 import com.android.tools.idea.sdk.IdeSdks;
 import com.android.tools.idea.stats.UsageTracker;
-import com.glavsoft.viewer.Viewer;
 import com.google.api.client.util.Maps;
 import com.google.api.client.util.Sets;
 import com.google.api.services.storage.Storage;
@@ -50,6 +49,7 @@
 import com.google.gct.testing.util.CloudTestingTracking;
 import com.google.gct.testing.vnc.BlankVncViewer;
 import com.google.gct.testing.vnc.BlankVncViewerCallback;
+import com.google.gct.testing.vnc.VncKeepAliveThreadImpl;
 import com.intellij.execution.DefaultExecutionResult;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.ExecutionResult;
@@ -362,7 +362,7 @@
     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);
+    File workingDir = new File(sdkPath);
     try {
       while (System.currentTimeMillis() < stopTime) {
         synchronized (ghostCloudDevices) {
@@ -391,7 +391,7 @@
           String deviceAddress = ipAddress + ":" + adbPort;
           System.out.println("Device ready with IP address:port " + deviceAddress);
           Runtime rt = Runtime.getRuntime();
-          Process connect = rt.exec("./adb connect " + deviceAddress, null, dir);
+          Process connect = rt.exec("./adb connect " + deviceAddress, null, workingDir);
           connect.waitFor();
           serialNumberToConfigurationInstance.put(deviceAddress, configurationInstance);
           // Do not wait for "finally" to remove the ghost device
@@ -401,11 +401,11 @@
           }
           blankVncViewer.closeWindow();
           // Make sure the device is unlocked.
-          Process unlock = rt.exec("./adb -s " + deviceAddress + " wait-for-device shell input keyevent 82" , null, dir);
+          Process unlock = rt.exec("./adb -s " + deviceAddress + " wait-for-device shell input keyevent 82" , null, workingDir);
           unlock.waitFor();
           // Open the VNC window for the cloud device.
           String[] viewerArgs = new String[]{"-port=" + vncPort, "-host=" + ipAddress, "-password=" + vncPassword, "-fullScreen=false"};
-          Viewer.showViewer(viewerArgs, configurationName);
+          VncKeepAliveThreadImpl.startVnc(viewerArgs, configurationName, cloudProjectId, deviceId, deviceAddress, workingDir);
           return;
         }
         Thread.sleep(POLLING_INTERVAL);
diff --git a/src/com/google/gct/testing/vnc/VncKeepAliveThreadImpl.java b/src/com/google/gct/testing/vnc/VncKeepAliveThreadImpl.java
new file mode 100644
index 0000000..8838237
--- /dev/null
+++ b/src/com/google/gct/testing/vnc/VncKeepAliveThreadImpl.java
@@ -0,0 +1,101 @@
+/*
+ * 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.vnc;
+
+import com.glavsoft.viewer.Viewer;
+import com.glavsoft.viewer.VncKeepAliveThread;
+import com.glavsoft.viewer.cli.Parser;
+
+import javax.swing.*;
+import java.io.File;
+
+import static com.google.gct.testing.launcher.CloudAuthenticator.getTest;
+
+public class VncKeepAliveThreadImpl extends VncKeepAliveThread {
+  private final Parser parser;
+  private final String configurationName;
+  private final String cloudProjectId;
+  private final String cloudDeviceId;
+  private final String deviceAddress;
+  private final File workingDir;
+  private volatile boolean hasCrashed = false;
+
+
+  public static void startVnc(String[] args, String configurationName, String cloudProjectId, String cloudDeviceId, String deviceAddress,
+                              File workingDir) {
+    Parser parser = Viewer.prepareParser(args);
+    if (parser == null) {
+      return;
+    }
+    new VncKeepAliveThreadImpl(parser, configurationName, cloudProjectId, cloudDeviceId, deviceAddress, workingDir).start();
+  }
+
+  public VncKeepAliveThreadImpl(Parser parser, String configurationName, String cloudProjectId, String cloudDeviceId, String deviceAddress,
+                                File workingDir) {
+    this.parser = parser;
+    this.configurationName = configurationName;
+    this.cloudProjectId = cloudProjectId;
+    this.cloudDeviceId = cloudDeviceId;
+    this.deviceAddress = deviceAddress;
+    this.workingDir = workingDir;
+  }
+
+  @Override
+  public void run() {
+    SwingUtilities.invokeLater(new Viewer(this, parser, configurationName));
+
+    while (!Thread.currentThread().isInterrupted() && deviceIsReady()) {
+      try {
+        getTest().projects().devices().keepalive(cloudProjectId, cloudDeviceId).execute();
+      } catch (Exception e) {
+        e.printStackTrace();
+      }
+      // Restart the viewer if it accidentally crashed.
+      if (hasCrashed) {
+        hasCrashed = false;
+        System.out.println("Restarting TightVNC Viewer");
+        SwingUtilities.invokeLater(new Viewer(this, parser, configurationName));
+      }
+      try {
+        Thread.sleep(1 * 1000); // 1 second
+      } catch (InterruptedException e) {
+        break;
+      }
+    }
+    try {
+      // Delete the cloud device after the viewer is closed.
+      getTest().projects().devices().delete(cloudProjectId, cloudDeviceId).execute();
+      // Disconnect adb from the deleted device (otherwise, it will keep showing the stale cloud device).
+      Runtime.getRuntime().exec("./adb disconnect " + deviceAddress, null, workingDir);
+    } catch (Exception exception) {
+      exception.printStackTrace();
+    }
+  }
+
+  private boolean deviceIsReady() {
+    try {
+      return getTest().projects().devices().get(cloudProjectId, cloudDeviceId).execute().getState().equals("READY");
+    } catch (Exception e) {
+      return true; // Do not stop the keep alive thread for intermittent connection failures.
+    }
+  }
+
+  @Override
+  public void setCrashed() {
+    hasCrashed = true;
+  }
+
+}