Merge "Remove the "install client libraries" action." into idea133
diff --git a/google-cloud-tools.iml b/google-cloud-tools.iml
index 2613b4a..6d1ab7c 100644
--- a/google-cloud-tools.iml
+++ b/google-cloud-tools.iml
@@ -26,7 +26,6 @@
     <orderEntry type="module" module-name="gradle" />
     <orderEntry type="module" module-name="jetgroovy" />
     <orderEntry type="library" scope="TEST" name="mockito" level="project" />
-    <orderEntry type="module" module-name="login" />
     <orderEntry type="module-library">
       <library>
         <CLASSES>
@@ -54,6 +53,8 @@
         <SOURCES />
       </library>
     </orderEntry>
+    <orderEntry type="module" module-name="google-login" />
+    <orderEntry type="module" module-name="bootstrap" />
   </component>
 </module>
 
diff --git a/lib-working/google-gct-login-context-pg.jar b/lib-working/google-gct-login-context-pg.jar
deleted file mode 100644
index a0e8f00..0000000
--- a/lib-working/google-gct-login-context-pg.jar
+++ /dev/null
Binary files differ
diff --git a/lib-working/javax.servlet-api-3.1.0.jar b/lib-working/javax.servlet-api-3.1.0.jar
deleted file mode 100644
index 6b14c3d..0000000
--- a/lib-working/javax.servlet-api-3.1.0.jar
+++ /dev/null
Binary files differ
diff --git a/login/login.iml b/login/google-login.iml
similarity index 61%
rename from login/login.iml
rename to login/google-login.iml
index 897f97a..da52fcf 100644
--- a/login/login.iml
+++ b/login/google-login.iml
@@ -5,15 +5,19 @@
     <content url="file://$MODULE_DIR$">
       <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
       <sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/resources" isTestSource="false" />
     </content>
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />
     <orderEntry type="module" module-name="openapi" />
+    <orderEntry type="module" module-name="platform-impl" />
     <orderEntry type="library" name="Guava" level="project" />
-    <orderEntry type="module-library" scope="TEST">
+    <orderEntry type="library" name="google-api-java-client" level="project" />
+    <orderEntry type="library" name="jcip" level="project" />
+    <orderEntry type="module-library">
       <library>
         <CLASSES>
-          <root url="jar://$APPLICATION_HOME_DIR$/lib/junit.jar!/" />
+          <root url="jar://$MODULE_DIR$/lib/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar!/" />
         </CLASSES>
         <JAVADOC />
         <SOURCES />
@@ -22,7 +26,7 @@
     <orderEntry type="module-library">
       <library>
         <CLASSES>
-          <root url="jar://$APPLICATION_HOME_DIR$/lib/jcip-annotations.jar!/" />
+          <root url="jar://$MODULE_DIR$/lib/google-gct-login-context-pg.jar!/" />
         </CLASSES>
         <JAVADOC />
         <SOURCES />
@@ -31,19 +35,27 @@
     <orderEntry type="module-library">
       <library>
         <CLASSES>
-          <root url="jar://$MODULE_DIR$/../lib-working/google.login.jar!/" />
+          <root url="jar://$MODULE_DIR$/lib/google-http-client-jackson-1.18.0-rc.jar!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES />
+      </library>
+    </orderEntry>
+    <orderEntry type="module-library">
+      <library>
+        <CLASSES>
+          <root url="jar://$MODULE_DIR$/lib/google.gdt.eclipse.login.common.jar!/" />
         </CLASSES>
         <JAVADOC />
         <SOURCES>
-          <root url="jar://$MODULE_DIR$/../lib-working/google.login.jar!/" />
+          <root url="jar://$MODULE_DIR$/lib/google.gdt.eclipse.login.common.jar!/" />
         </SOURCES>
       </library>
     </orderEntry>
-    <orderEntry type="library" name="google-api-java-client" level="project" />
     <orderEntry type="module-library">
       <library>
         <CLASSES>
-          <root url="jar://$MODULE_DIR$/../lib-working/google-http-client-jackson-1.18.0-rc.jar!/" />
+          <root url="jar://$MODULE_DIR$/lib/jackson-core-asl-1.9.11.jar!/" />
         </CLASSES>
         <JAVADOC />
         <SOURCES />
@@ -52,40 +64,13 @@
     <orderEntry type="module-library">
       <library>
         <CLASSES>
-          <root url="jar://$MODULE_DIR$/../lib-working/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar!/" />
+          <root url="jar://$MODULE_DIR$/lib/javax.servlet-api-3.0.1.jar!/" />
         </CLASSES>
         <JAVADOC />
         <SOURCES />
       </library>
     </orderEntry>
-    <orderEntry type="module-library">
-      <library>
-        <CLASSES>
-          <root url="jar://$MODULE_DIR$/../lib-working/jackson-core-asl-1.9.11.jar!/" />
-        </CLASSES>
-        <JAVADOC />
-        <SOURCES />
-      </library>
-    </orderEntry>
-    <orderEntry type="module-library">
-      <library>
-        <CLASSES>
-          <root url="jar://$MODULE_DIR$/../lib-working/javax.servlet-api-3.1.0.jar!/" />
-        </CLASSES>
-        <JAVADOC />
-        <SOURCES />
-      </library>
-    </orderEntry>
-    <orderEntry type="module" module-name="platform-impl" />
-    <orderEntry type="module-library">
-      <library>
-        <CLASSES>
-          <root url="jar://$MODULE_DIR$/../lib-working/google-gct-login-context-pg.jar!/" />
-        </CLASSES>
-        <JAVADOC />
-        <SOURCES />
-      </library>
-    </orderEntry>
+    <orderEntry type="library" scope="TEST" name="JUnit3" level="project" />
   </component>
 </module>
 
diff --git a/lib-working/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar b/login/lib/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar
similarity index 100%
rename from lib-working/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar
rename to login/lib/google-api-services-oauth2-v2-rev70-1.18.0-rc.jar
Binary files differ
diff --git a/login/lib/google-gct-login-context-pg.jar b/login/lib/google-gct-login-context-pg.jar
new file mode 100644
index 0000000..0a7b382
--- /dev/null
+++ b/login/lib/google-gct-login-context-pg.jar
Binary files differ
diff --git a/lib-working/google-http-client-jackson-1.18.0-rc.jar b/login/lib/google-http-client-jackson-1.18.0-rc.jar
similarity index 100%
rename from lib-working/google-http-client-jackson-1.18.0-rc.jar
rename to login/lib/google-http-client-jackson-1.18.0-rc.jar
Binary files differ
diff --git a/lib-working/google.login.jar b/login/lib/google.gdt.eclipse.login.common.jar
similarity index 100%
rename from lib-working/google.login.jar
rename to login/lib/google.gdt.eclipse.login.common.jar
Binary files differ
diff --git a/lib-working/jackson-core-asl-1.9.11.jar b/login/lib/jackson-core-asl-1.9.11.jar
similarity index 100%
rename from lib-working/jackson-core-asl-1.9.11.jar
rename to login/lib/jackson-core-asl-1.9.11.jar
Binary files differ
diff --git a/login/lib/javax.servlet-api-3.0.1.jar b/login/lib/javax.servlet-api-3.0.1.jar
new file mode 100644
index 0000000..4e2edcc
--- /dev/null
+++ b/login/lib/javax.servlet-api-3.0.1.jar
Binary files differ
diff --git a/login/resources/icons/google.png b/login/resources/icons/google.png
new file mode 100644
index 0000000..3f7dfe4
--- /dev/null
+++ b/login/resources/icons/google.png
Binary files differ
diff --git a/login/resources/icons/google@2x.png b/login/resources/icons/google@2x.png
new file mode 100644
index 0000000..ad846a4
--- /dev/null
+++ b/login/resources/icons/google@2x.png
Binary files differ
diff --git a/login/resources/icons/googleFavicon.png b/login/resources/icons/googleFavicon.png
new file mode 100644
index 0000000..e9c3aba
--- /dev/null
+++ b/login/resources/icons/googleFavicon.png
Binary files differ
diff --git a/login/resources/icons/googleFavicon@2x.png b/login/resources/icons/googleFavicon@2x.png
new file mode 100644
index 0000000..59cdbf9
--- /dev/null
+++ b/login/resources/icons/googleFavicon@2x.png
Binary files differ
diff --git a/resources/icons/loginAvatar.png b/login/resources/icons/loginAvatar.png
similarity index 100%
rename from resources/icons/loginAvatar.png
rename to login/resources/icons/loginAvatar.png
Binary files differ
diff --git a/resources/icons/loginAvatar@2x.png b/login/resources/icons/loginAvatar@2x.png
similarity index 100%
rename from resources/icons/loginAvatar@2x.png
rename to login/resources/icons/loginAvatar@2x.png
Binary files differ
diff --git a/login/src/META-INF/plugin.xml b/login/src/META-INF/plugin.xml
new file mode 100644
index 0000000..1bd7803
--- /dev/null
+++ b/login/src/META-INF/plugin.xml
@@ -0,0 +1,48 @@
+<!--
+  ~ Copyright (C) 2014 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.
+  -->
+<idea-plugin version="2">
+  <id>com.google.gct.login</id>
+  <name>Google Login</name>
+  <version>1.0</version>
+  <vendor>Google</vendor>
+
+  <description>Provides Google authentication support.</description>
+
+  <!-- please see http://confluence.jetbrains.net/display/IDEADEV/Build+Number+Ranges for description -->
+  <idea-version since-build="107.105"/>
+
+  <application-components>
+  </application-components>
+
+  <project-components>
+  </project-components>
+
+  <actions>
+    <action id="GoogleLogin.LoginService"
+            class="com.google.gct.login.ui.GoogleLoginAction"
+            text="Google Login">
+      <add-to-group group-id="MainToolBar" anchor="first"  />
+    </action>
+  </actions>
+
+  <extensions defaultExtensionNs="com.intellij">
+  </extensions>
+
+  <extensionPoints>
+    <extensionPoint name="googleLoginListener" interface="com.google.gct.login.GoogleLoginListener"/>
+  </extensionPoints>
+
+</idea-plugin>
\ No newline at end of file
diff --git a/login/src/com/google/gct/login/CancellableServerReceiver.java b/login/src/com/google/gct/login/CancellableServerReceiver.java
new file mode 100644
index 0000000..dfe1cd8
--- /dev/null
+++ b/login/src/com/google/gct/login/CancellableServerReceiver.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2014 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.login;
+
+import com.google.api.client.extensions.java6.auth.oauth2.VerificationCodeReceiver;
+import com.google.api.client.repackaged.com.google.common.base.Throwables;
+import com.google.api.client.repackaged.org.mortbay.jetty.Connector;
+import com.google.api.client.repackaged.org.mortbay.jetty.Request;
+import com.google.api.client.repackaged.org.mortbay.jetty.Server;
+import com.google.api.client.repackaged.org.mortbay.jetty.handler.AbstractHandler;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A cancellable Server Receiver.
+ */
+class CancellableServerReceiver implements VerificationCodeReceiver {
+  private static final String CALLBACK_PATH = "/Callback";
+
+  /** Server or {@code null} before {@link #getRedirectUri()}. */
+  private Server server;
+
+  /** Verification code or {@code null} for none. */
+  String code;
+
+  /** Error code or {@code null} for none. */
+  String error;
+
+  /** Lock on the code and error. */
+  final Lock lock = new ReentrantLock();
+
+  /** Condition for receiving an authorization response. */
+  final Condition gotAuthorizationResponse = lock.newCondition();
+
+  /** Port to use or {@code -1} to select an unused port in {@link #getRedirectUri()}. */
+  private int port;
+
+  /** Host name to use. */
+  private final String host;
+
+  /**
+   * Constructor that starts the server on {@code "localhost"} selects an unused port.
+   *
+   * <p>
+   * Use {@link Builder} if you need to specify any of the optional parameters.
+   * </p>
+   */
+  public CancellableServerReceiver() {
+    this("localhost", -1);
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param host Host name to use
+   * @param port Port to use or {@code -1} to select an unused port
+   */
+  CancellableServerReceiver(String host, int port) {
+    this.host = host;
+    this.port = port;
+  }
+
+  @Override
+  public String getRedirectUri() throws IOException {
+    if (port == -1) {
+      port = getUnusedPort();
+    }
+    server = new Server(port);
+    for (Connector c : server.getConnectors()) {
+      c.setHost(host);
+    }
+    server.addHandler(new CallbackHandler());
+    try {
+      server.start();
+    } catch (Exception e) {
+      Throwables.propagateIfPossible(e);
+      throw new IOException(e);
+    }
+    return "http://" + host + ":" + port + CALLBACK_PATH;
+  }
+
+  @Override
+  public String waitForCode() throws IOException {
+    lock.lock();
+    try {
+      while (code == null && error == null) {
+        gotAuthorizationResponse.awaitUninterruptibly();
+      }
+      if (error != null) {
+        throw new IOException("User authorization failed (" + error + ")");
+      }
+      return code;
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  @Override
+  public void stop() throws IOException {
+    if (server != null) {
+      try {
+        server.stop();
+      } catch (Exception e) {
+        Throwables.propagateIfPossible(e);
+        throw new IOException(e);
+      }
+      lock.lock();
+      try {
+        error = "Request cancelled.";
+        code = null;
+        gotAuthorizationResponse.signal();
+      }
+      finally {
+        lock.unlock();
+      }
+      server = null;
+    }
+  }
+
+  /** Returns the host name to use. */
+  public String getHost() {
+    return host;
+  }
+
+  /**
+   * Returns the port to use or {@code -1} to select an unused port in {@link #getRedirectUri()}.
+   */
+  public int getPort() {
+    return port;
+  }
+
+  private static int getUnusedPort() throws IOException {
+    Socket s = new Socket();
+    s.bind(null);
+    try {
+      return s.getLocalPort();
+    } finally {
+      s.close();
+    }
+  }
+
+  /**
+   * Builder.
+   *
+   * <p>
+   * Implementation is not thread-safe.
+   * </p>
+   */
+  public static final class Builder {
+
+    /** Host name to use. */
+    private String host = "localhost";
+
+    /** Port to use or {@code -1} to select an unused port. */
+    private int port = -1;
+
+    /** Builds the {@link LocalServerReceiver}. */
+    public CancellableServerReceiver build() {
+      return new CancellableServerReceiver(host, port);
+    }
+
+    /** Returns the host name to use. */
+    public String getHost() {
+      return host;
+    }
+
+    /** Sets the host name to use. */
+    public Builder setHost(String host) {
+      this.host = host;
+      return this;
+    }
+
+    /** Returns the port to use or {@code -1} to select an unused port. */
+    public int getPort() {
+      return port;
+    }
+
+    /** Sets the port to use or {@code -1} to select an unused port. */
+    public Builder setPort(int port) {
+      this.port = port;
+      return this;
+    }
+  }
+
+  /**
+   * Jetty handler that takes the verifier token passed over from the OAuth provider and stashes it
+   * where {@link #waitForCode} will find it.
+   */
+  class CallbackHandler extends AbstractHandler {
+
+    @Override
+    public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException {
+      if (!CALLBACK_PATH.equals(target)) {
+        return;
+      }
+      writeLandingHtml(response);
+      response.flushBuffer();
+      ((Request)request).setHandled(true);
+      lock.lock();
+      try {
+        error = request.getParameter("error");
+        code = request.getParameter("code");
+        gotAuthorizationResponse.signal();
+      }
+      finally {
+        lock.unlock();
+      }
+    }
+
+    private void writeLandingHtml(HttpServletResponse response) throws IOException {
+      response.setStatus(HttpServletResponse.SC_OK);
+      response.setContentType("text/html");
+
+      PrintWriter doc = response.getWriter();
+      doc.println("<html>");
+      doc.println("<head><title>OAuth 2.0 Authentication Token Received</title></head>");
+      doc.println("<body>");
+      doc.println("Received verification code. Closing...");
+      doc.println("<script type='text/javascript'>");
+      // We open "" in the same window to trigger JS ownership of it, which lets
+      // us then close it via JS, at least in Chrome.
+      doc.println("window.setTimeout(function() {");
+      doc.println("    window.open('https://developers.google.com/', '_self', '');}, 100);");
+      doc.println("</script>");
+      doc.println("</body>");
+      doc.println("</HTML>");
+      doc.flush();
+    }
+  }
+}
diff --git a/login/src/com/google/gct/login/CredentialedUser.java b/login/src/com/google/gct/login/CredentialedUser.java
index 957f03b..8a42a6f 100644
--- a/login/src/com/google/gct/login/CredentialedUser.java
+++ b/login/src/com/google/gct/login/CredentialedUser.java
@@ -44,7 +44,7 @@
     googleLoginState = null;
   }
 
-  public CredentialedUser(GoogleLoginState state, final IGoogleLoginUpdateUser updateUserCallback) {
+  public CredentialedUser(GoogleLoginState state, final IGoogleLoginCompletedCallback updateUserCallback) {
     this.email = state.getEmail();
     googleLoginState = state;
     credential = googleLoginState.makeCredential();
@@ -95,7 +95,7 @@
     this.isActive = isActive;
   }
 
-  private void initializeUserInfo(Userinfoplus userInfo, final IGoogleLoginUpdateUser updateUserCallback) {
+  private void initializeUserInfo(Userinfoplus userInfo, final IGoogleLoginCompletedCallback updateUserCallback) {
     if(userInfo == null) {
       name = null;
       image = null;
@@ -105,7 +105,7 @@
         @Override
         public void setProperty(Image newImage) {
           image = newImage;
-          updateUserCallback.updateUser();
+          updateUserCallback.onLoginCompleted();
         }
       };
       GoogleLoginUtils.getUserPicture(userInfo, pictureCallback);
diff --git a/login/src/com/google/gct/login/CredentialedUserRoster.java b/login/src/com/google/gct/login/CredentialedUserRoster.java
index d383ca2..7d50c70 100644
--- a/login/src/com/google/gct/login/CredentialedUserRoster.java
+++ b/login/src/com/google/gct/login/CredentialedUserRoster.java
@@ -15,10 +15,12 @@
  */
 package com.google.gct.login;
 
+import com.google.common.collect.Lists;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.util.HashMap;
+import java.util.Collection;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
@@ -27,16 +29,21 @@
  * {@link CredentialedUser} objects.
  */
 public class CredentialedUserRoster {
-  private final Map<String, CredentialedUser> allUsers = new HashMap<String, CredentialedUser>();
+  private final LinkedHashMap<String, CredentialedUser> allUsers = new LinkedHashMap<String, CredentialedUser>();
   private CredentialedUser activeUser;
+  private Collection<GoogleLoginListener> listeners;
+
+  public CredentialedUserRoster() {
+    listeners = Lists.newLinkedList();
+  }
 
   /**
    * Returns a copy of the map of the current logged in users.
    * @return Copy of current logged in users.
    */
-  public Map<String, CredentialedUser> getAllUsers() {
+  public LinkedHashMap<String, CredentialedUser> getAllUsers() {
     synchronized (this) {
-      Map<String, CredentialedUser> clone = new HashMap<String, CredentialedUser>();
+      LinkedHashMap<String, CredentialedUser> clone = new LinkedHashMap<String, CredentialedUser>();
       clone.putAll(allUsers);
       return clone;
     }
@@ -82,6 +89,7 @@
       activeUser = allUsers.get(userEmail);
       activeUser.setActive(true);
       GoogleLoginPrefs.saveActiveUser(userEmail);
+      notifyLoginStatusChange();
     }
   }
 
@@ -94,6 +102,7 @@
         activeUser.setActive(false);
         activeUser = null;
         GoogleLoginPrefs.removeActiveUser();
+        notifyLoginStatusChange();
       }
     }
   }
@@ -151,8 +160,29 @@
       }
 
       allUsers.remove(userEmail);
+      notifyLoginStatusChange();
       return true;
     }
   }
 
+  /**
+   * Register a specified {@link GoogleLoginListener} to be notified of changes to the
+   * logged-in state.
+   *
+   * @param listener the specified {@code GoogleLoginListener}
+   */
+  void addLoginListener(GoogleLoginListener listener) {
+    synchronized(listeners) {
+      listeners.add(listener);
+    }
+  }
+
+  private void notifyLoginStatusChange() {
+    synchronized(listeners) {
+      for (GoogleLoginListener listener : listeners) {
+        listener.statusChanged();
+      }
+    }
+  }
+
 }
diff --git a/login/src/com/google/gct/login/GoogleLogin.java b/login/src/com/google/gct/login/GoogleLogin.java
index 549a5e9..50d6c67 100644
--- a/login/src/com/google/gct/login/GoogleLogin.java
+++ b/login/src/com/google/gct/login/GoogleLogin.java
@@ -30,21 +30,28 @@
 import com.google.gdt.eclipse.login.common.OAuthData;
 import com.google.gdt.eclipse.login.common.OAuthDataStore;
 import com.google.gdt.eclipse.login.common.UiFacade;
-
 import com.google.gdt.eclipse.login.common.VerificationCodeHolder;
 import com.intellij.ide.BrowserUtil;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.application.ModalityState;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.progress.util.ProgressIndicatorBase;
 import com.intellij.openapi.ui.DialogWrapper;
 import com.intellij.openapi.ui.Messages;
 
+import com.intellij.openapi.util.IconLoader;
+import com.intellij.openapi.wm.ex.ProgressIndicatorEx;
 import net.jcip.annotations.Immutable;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import javax.swing.Icon;
 import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.SortedSet;
 
@@ -71,6 +78,7 @@
     this.uiFacade = new AndroidUiFacade();
     this.users = new CredentialedUserRoster();
     this.dataStore =  new AndroidPreferencesOAuthDataStore();
+    addLoginListenersFromExtensionPoints();
   }
 
   /**
@@ -90,7 +98,7 @@
    * @throws InvalidThreadTypeException
    */
   public static void promptToLogIn() throws InvalidThreadTypeException {
-    promptToLogIn(null);
+    promptToLogIn(null, null);
   }
 
   /**
@@ -98,12 +106,15 @@
    * if there is current no active user. Does nothing if there is an active
    * user. This function must be called from the event dispatch thread (EDT).
    * @param message  If not null, this message would be the title of the dialog.
+   * @param callback if not null, then this callback is called when the login
+   * either succeeds or fails.
    * @throws InvalidThreadTypeException
    */
-  public static void promptToLogIn(final String message) throws InvalidThreadTypeException {
+  public static void promptToLogIn(final String message, @Nullable final IGoogleLoginCompletedCallback callback)
+    throws InvalidThreadTypeException {
     if (!instance.isLoggedIn()) {
       if(ApplicationManager.getApplication().isDispatchThread()) {
-        getInstance().logIn(message);
+        getInstance().logIn(message, callback);
       } else {
         throw new InvalidThreadTypeException("promptToLogin");
       }
@@ -275,8 +286,7 @@
    * See {@link #logIn(String)}.
    */
   public void logIn() {
-    users.removeActiveUser();
-    logIn(null);
+    logIn(null, null);
   }
 
   /**
@@ -291,28 +301,67 @@
    *          as accessing Google API services. It should say something like
    *          "Importing a project from Google Project Hosting requires signing
    *          in."
+   * @param callback if not null, then this callback is called when the login
+   * either succeeds or fails.
    */
-  public void logIn(final String message) {
+  public void logIn(final String message, @Nullable final IGoogleLoginCompletedCallback callback) {
+    users.removeActiveUser();
+    uiFacade.notifyStatusIndicator();
+
     final GoogleLoginState state = createGoogleLoginState();
 
-    ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
+    new Task.Modal(null, "Please sign in via the opened browser...", true) {
+      private boolean loggedIn = false;
+
       @Override
-      public void run() {
-        boolean loggedIn = state.logInWithLocalServer(message);
+      public void run(@NotNull ProgressIndicator indicator) {
+        indicator.setIndeterminate(true);
+        if (!(indicator instanceof ProgressIndicatorEx)) {
+          return;
+        }
 
+        ((ProgressIndicatorEx)indicator).addStateDelegate(new ProgressIndicatorBase() {
+          @Override
+          public void cancel() {
+            assert uiFacade != null;
+            uiFacade.stop();
+            super.cancel();
+          }
+        });
+
+        loggedIn = state.logInWithLocalServer(message);
+      }
+
+      @Override
+      public void onCancel() {
+        notifyOnComplete();
+      }
+
+      @Override
+      public void onSuccess() {
+        notifyOnComplete();
+      }
+
+      private void notifyOnComplete() {
         // TODO: add user preference to chose to use pop-up copy and paste dialog
-
         if(loggedIn) {
-          IGoogleLoginUpdateUser callback = new IGoogleLoginUpdateUser() {
+          IGoogleLoginCompletedCallback localCallback = new IGoogleLoginCompletedCallback() {
+
             @Override
-            public void updateUser() {
+            public void onLoginCompleted() {
               uiFacade.notifyStatusIndicator();
+              if(callback != null) {
+                callback.onLoginCompleted();
+              }
             }
           };
-          users.addUser(new CredentialedUser(state, callback));
+          users.addUser(new CredentialedUser(state, localCallback));
+        }
+        else if (callback != null) {
+          callback.onLoginCompleted();
         }
       }
-    });
+    }.queue();
   }
 
   /**
@@ -392,7 +441,7 @@
    * Returns a copy of the map of the current logged in users.
    * @return Copy of current logged in users.
    */
-  public Map<String, CredentialedUser> getAllUsers() {
+  public LinkedHashMap<String, CredentialedUser> getAllUsers() {
     return users.getAllUsers();
   }
 
@@ -419,12 +468,11 @@
   /**
    * Gets all the implementations of  {@link GoogleLoginListener} and registers them to
    * <code>state</code>.
-   * @param state the {@link GoogleLoginState} for which we want to register listeners to.
    */
-  private static void addLoginListenersFromExtensionPoints(GoogleLoginState state) {
+  private void addLoginListenersFromExtensionPoints() {
     GoogleLoginListener[] loginListeners = Extensions.getExtensions(GoogleLoginListener.EP_NAME);
     for(GoogleLoginListener listener : loginListeners) {
-      state.addLoginListener(listener);
+      users.addLoginListener(listener);
     }
   }
 
@@ -441,8 +489,6 @@
         new AndroidPreferencesOAuthDataStore(),
         uiFacade,
         new AndroidLoggerFacade());
-
-    addLoginListenersFromExtensionPoints(state);
     return state;
   }
 
@@ -504,6 +550,8 @@
    */
   private class AndroidUiFacade implements UiFacade {
     private GoogleLoginActionButton myButton;
+    private final static String GOOGLE_IMG = "/icons/googleFavicon@2x.png";
+    private volatile CancellableServerReceiver receiver = null;
 
     @Override
     public String obtainVerificationCodeFromUserInteraction(String title, GoogleAuthorizationCodeRequestUrl authCodeRequestUrl) {
@@ -516,9 +564,21 @@
       return Strings.emptyToNull(dialog.getVerificationCode());
     }
 
+    public void stop() {
+      CancellableServerReceiver localreceiver = receiver;
+      if (localreceiver != null) {
+        try {
+          localreceiver.stop();
+        }
+        catch(IOException e) {
+          logErrorAndDisplayDialog("Google Login", e);
+        }
+      }
+    }
+
     @Override
     public VerificationCodeHolder obtainVerificationCodeFromExternalUserInteraction(String title) {
-      VerificationCodeReceiver receiver = new LocalServerReceiver();
+      receiver = new CancellableServerReceiver();
       String redirectUrl;
       try {
         redirectUrl = receiver.getRedirectUri();
@@ -543,6 +603,9 @@
         logErrorAndDisplayDialog(title == null? "Google Login" : title, e);
         return null;
       }
+      finally {
+        receiver = null;
+      }
 
       return new VerificationCodeHolder(verificationCode, redirectUrl);
     }
@@ -554,7 +617,16 @@
 
     @Override
     public boolean askYesOrNo(String title, String message) {
-      return (Messages.showYesNoDialog(message, title, Messages.getQuestionIcon()) == Messages.YES);
+      String updatedMessage = message;
+      if(message.equals("Are you sure you want to sign out?")) {
+        CredentialedUser activeUser = getActiveUser();
+        String name = activeUser.getName().isEmpty() ? "" : activeUser.getName() + " ";
+        updatedMessage = "Are you sure you want to sign out " + name
+          + "(" + activeUser.getEmail() + ")?";
+      }
+
+      Icon icon = IconLoader.getIcon(GOOGLE_IMG);
+      return (Messages.showYesNoDialog(updatedMessage, title, icon) == Messages.YES);
     }
 
     @Override
@@ -600,7 +672,8 @@
     }
 
     public void initializeUsers() {
-      SortedSet<String> allUsers = GoogleLoginPrefs.getStoredUsers();
+      String activeUserString = GoogleLoginPrefs.getActiveUser();
+      List<String> allUsers = GoogleLoginPrefs.getStoredUsers();
       for(String aUser : allUsers) {
         // Add a new user, so that loadOAuth called from the GoogleLoginState constructor
         // will be able to create a customized key to get that user's OAuth data
@@ -609,9 +682,9 @@
 
         // CredentialedUser's credentials will be updated from the persistent storage in GoogleLoginState constructor
         GoogleLoginState delegate = createGoogleLoginState();
-        IGoogleLoginUpdateUser callback = new IGoogleLoginUpdateUser() {
+        IGoogleLoginCompletedCallback callback = new IGoogleLoginCompletedCallback() {
           @Override
-          public void updateUser() {
+          public void onLoginCompleted() {
             uiFacade.notifyStatusIndicator();
           }
         };
@@ -619,7 +692,6 @@
         users.addUser(new CredentialedUser(delegate, callback));
       }
 
-      String activeUserString = GoogleLoginPrefs.getActiveUser();
       if(activeUserString == null) {
         users.removeActiveUser();
       } else {
diff --git a/login/src/com/google/gct/login/GoogleLoginListener.java b/login/src/com/google/gct/login/GoogleLoginListener.java
index 11c4f4c..560f5f0 100644
--- a/login/src/com/google/gct/login/GoogleLoginListener.java
+++ b/login/src/com/google/gct/login/GoogleLoginListener.java
@@ -15,13 +15,17 @@
  */
 package com.google.gct.login;
 
-import com.google.gdt.eclipse.login.common.LoginListener;
 import com.intellij.openapi.extensions.ExtensionPointName;
 
 /**
  * Listener for changes in the login status.
  */
-public interface GoogleLoginListener extends LoginListener {
+public interface GoogleLoginListener {
   public static ExtensionPointName<GoogleLoginListener> EP_NAME =
-    new ExtensionPointName<GoogleLoginListener>("com.google.gct.googleLoginListener");
+    new ExtensionPointName<GoogleLoginListener>("com.google.gct.login.googleLoginListener");
+
+  /**
+   * Called when the login or active status of the user changes.
+   */
+  void statusChanged();
 }
diff --git a/login/src/com/google/gct/login/GoogleLoginPrefs.java b/login/src/com/google/gct/login/GoogleLoginPrefs.java
index 8795238..1ad2b9f 100644
--- a/login/src/com/google/gct/login/GoogleLoginPrefs.java
+++ b/login/src/com/google/gct/login/GoogleLoginPrefs.java
@@ -23,6 +23,8 @@
 import com.intellij.openapi.diagnostic.Logger;
 import org.jetbrains.annotations.NotNull;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.prefs.BackingStoreException;
@@ -42,9 +44,6 @@
   private static String preferencesPath = PREFERENCES_PATH;
 
   private static final String OAUTH_DATA_EMAIL_KEY = "credentials_email";
-  private static final String OAUTH_DATA_ACCESS_TOKEN_KEY = "credentials_access_token";
-  private static final String OAUTH_DATA_ACCESS_TOKEN_EXPIRY_TIME_KEY =
-    "credentials_access_token_expiry_time";
   private static final String OAUTH_DATA_REFRESH_TOKEN_KEY = "credentials_refresh_token";
   private static final String ICON_ONLY_KEY = "icon_only";
   private static final String LOGOUT_ON_EXIT_KEY = "logout_on_exit";
@@ -61,11 +60,7 @@
   public static void saveOAuthData(OAuthData credentials) {
     Preferences prefs = getPrefs();
     String userEmail = credentials.getStoredEmail();
-    prefs.put(getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_KEY, userEmail), credentials.getAccessToken());
     prefs.put(getCustomUserKey(OAUTH_DATA_REFRESH_TOKEN_KEY, userEmail), credentials.getRefreshToken());
-    prefs.put(
-      getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_EXPIRY_TIME_KEY, userEmail),
-      Long.toString(credentials.getAccessTokenExpiryTime()));
 
     // we save the scopes so that if the user updates the plugin and the
     // scopes change, we can force the plugin to log out.
@@ -89,7 +84,6 @@
   public static OAuthData loadOAuthData() {
     Preferences prefs = getPrefs();
 
-    String accessToken = prefs.get(getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_KEY), null);
     String refreshToken = prefs.get(getCustomUserKey(OAUTH_DATA_REFRESH_TOKEN_KEY), null);
     String storedEmail = prefs.get(getCustomUserKey(OAUTH_DATA_EMAIL_KEY), null);
     String storedScopesString = prefs.get(getCustomUserKey(OAUTH_SCOPES_KEY), "");
@@ -99,13 +93,7 @@
     for (String scope : storedScopesString.split(DELIMITER)) {
       storedScopes.add(scope);
     }
-    long accessTokenExpiryTime = 0;
-    String accessTokenExpiryTimeString = prefs.get(getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_EXPIRY_TIME_KEY), null);
-    if (accessTokenExpiryTimeString != null) {
-      accessTokenExpiryTime = Long.parseLong(accessTokenExpiryTimeString);
-    }
-    return new OAuthData(
-      accessToken, refreshToken, storedEmail, storedScopes, accessTokenExpiryTime);
+    return new OAuthData(null, refreshToken, storedEmail, storedScopes, 0);
   }
 
   /**
@@ -118,11 +106,9 @@
     }
 
     Preferences prefs = getPrefs();
-    prefs.remove(getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_KEY));
     prefs.remove(getCustomUserKey(OAUTH_DATA_REFRESH_TOKEN_KEY));
     prefs.remove(getCustomUserKey(OAUTH_DATA_EMAIL_KEY));
     prefs.remove(getCustomUserKey(OAUTH_SCOPES_KEY));
-    prefs.remove(getCustomUserKey(OAUTH_DATA_ACCESS_TOKEN_EXPIRY_TIME_KEY));
     removeUser(prefs, activeUser.getEmail());
     flushPrefs(prefs);
   }
@@ -163,10 +149,10 @@
    * @return the stored list of users.
    */
   @NotNull
-  public static SortedSet<String> getStoredUsers() {
+  public static List<String> getStoredUsers() {
     Preferences prefs = getPrefs();
     String allUsersString = prefs.get(USERS, "");
-    SortedSet<String> allUsers = new TreeSet<String>();
+    List<String> allUsers = new ArrayList<String>();
     if(allUsersString.isEmpty()) {
       return allUsers;
     }
@@ -252,7 +238,7 @@
       return;
     }
 
-    SortedSet<String> allUsers = new TreeSet<String>();
+    List<String> allUsers = new ArrayList<String>();
     Splitter splitter = Splitter.on(DELIMITER).omitEmptyStrings();
     for (String scope : splitter.split(allUsersString)) {
       allUsers.add(scope);
@@ -269,7 +255,7 @@
 
   private static void removeUser(Preferences prefs, String user) {;
     String allUsersString = prefs.get(USERS, "");
-    SortedSet<String> allUsers = new TreeSet<String>();
+    List<String> allUsers = new ArrayList<String>();
     for (String scope : allUsersString.split(DELIMITER)) {
       allUsers.add(scope);
     }
diff --git a/login/src/com/google/gct/login/GoogleLoginUtils.java b/login/src/com/google/gct/login/GoogleLoginUtils.java
index 5204ef2..a4a8fce 100644
--- a/login/src/com/google/gct/login/GoogleLoginUtils.java
+++ b/login/src/com/google/gct/login/GoogleLoginUtils.java
@@ -69,6 +69,7 @@
       @Override
       public void run() {
         Image image = Toolkit.getDefaultToolkit().getImage(newUrl);
+        Toolkit.getDefaultToolkit().prepareImage(image, -1, -1, null);
         pictureCallback.setProperty(image);
       }
     });
diff --git a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java b/login/src/com/google/gct/login/IGoogleLoginCompletedCallback.java
similarity index 77%
rename from login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
rename to login/src/com/google/gct/login/IGoogleLoginCompletedCallback.java
index 771081b..5661916 100644
--- a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
+++ b/login/src/com/google/gct/login/IGoogleLoginCompletedCallback.java
@@ -15,6 +15,12 @@
  */
 package com.google.gct.login;
 
-public interface IGoogleLoginUpdateUser {
-  public void updateUser();
+/**
+ * Callback for when a login is completed.
+ */
+public interface IGoogleLoginCompletedCallback {
+  /**
+   * Called when log in is complete.
+   */
+  public void onLoginCompleted();
 }
diff --git a/login/src/com/google/gct/login/OAuthScopeRegistry.java b/login/src/com/google/gct/login/OAuthScopeRegistry.java
index ca2fe2e..47b9686 100644
--- a/login/src/com/google/gct/login/OAuthScopeRegistry.java
+++ b/login/src/com/google/gct/login/OAuthScopeRegistry.java
@@ -32,6 +32,7 @@
   static {
     SortedSet<String> scopes = new TreeSet<String>();
     scopes.add("https://www.googleapis.com/auth/userinfo#email");
+    scopes.add("https://www.googleapis.com/auth/appengine.admin");
     sScopes = Collections.unmodifiableSortedSet(scopes);
   }
 
diff --git a/login/src/com/google/gct/login/ui/GoogleLoginActionButton.java b/login/src/com/google/gct/login/ui/GoogleLoginActionButton.java
index 6782f01..21b0628 100644
--- a/login/src/com/google/gct/login/ui/GoogleLoginActionButton.java
+++ b/login/src/com/google/gct/login/ui/GoogleLoginActionButton.java
@@ -31,14 +31,24 @@
 /**
  * The Google Login button that appears on the main toolbar.
  */
-public class GoogleLoginActionButton extends ActionButton {
+public final class GoogleLoginActionButton extends ActionButton {
   private Icon defaultIcon;
   private final static String SIGN_IN_MESSAGE = "Sign in to Google...";
   private final static String DEFAULT_AVATAR = "/icons/loginAvatar.png";
+  private final static String SHOW_LOGIN_BUTTON_PROPERTY = "show.google.login.button";
+
 
   public GoogleLoginActionButton(AnAction action, Presentation presentation, String place, @NotNull Dimension minimumSize) {
     super(action, presentation, place, minimumSize);
 
+    // The Google login toolbar item is hidden by default temporarily.
+    // To view the login button, add "-Dshow.google.login.button=true" to the JVM options
+    boolean showLoginButton = Boolean.getBoolean(SHOW_LOGIN_BUTTON_PROPERTY);
+    if(!showLoginButton) {
+      setVisible(false);
+      return;
+    }
+
     GoogleLogin.getInstance().setLoginMenuItemContribution(this);
     defaultIcon = IconLoader.getIcon(DEFAULT_AVATAR);
     updateUi();
diff --git a/login/src/com/google/gct/login/ui/GoogleLoginUsersPanel.java b/login/src/com/google/gct/login/ui/GoogleLoginUsersPanel.java
index da72f86..a98ce85 100644
--- a/login/src/com/google/gct/login/ui/GoogleLoginUsersPanel.java
+++ b/login/src/com/google/gct/login/ui/GoogleLoginUsersPanel.java
@@ -17,6 +17,7 @@
 
 import com.google.gct.login.CredentialedUser;
 import com.google.gct.login.GoogleLogin;
+import com.intellij.ide.BrowserUtil;
 import com.intellij.ui.components.JBList;
 import com.intellij.ui.components.JBScrollPane;
 
@@ -32,39 +33,143 @@
 import javax.swing.event.ListSelectionListener;
 
 import java.awt.BorderLayout;
+import java.awt.Cursor;
+import java.awt.Dimension;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.util.Map;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseMotionListener;
+import java.util.LinkedHashMap;
 
 /**
  * The Google Login Panel that displays the currently logged in users and buttons to
  * add a new user and sign out a logged in user.
  */
 public class GoogleLoginUsersPanel extends JPanel implements ListSelectionListener {
+  private static final String PLAY_CONSOLE_URL = "https://play.google.com/apps/publish/#ProfilePlace";
+  private static final String CLOUD_CONSOLE_URL = "https://console.developers.google.com/accountsettings";
+  private final static String LEARN_MORE_URL = "https://developers.google.com/cloud/devtools/android_studio_templates/";
   private JBList list;
   private DefaultListModel listModel;
-
   private static final int MAX_VISIBLE_ROW_COUNT = 3;
   private static final String addAccountString = "Add Account";
+  private static final String signInString = "Sign In";
   private static final String signOutString = "Sign Out";
   private JButton signOutButton;
   private JButton addAccountButton;
+  private boolean valueChanged = false;
+  private boolean ignoreSelection = false;
 
   public GoogleLoginUsersPanel() {
     super(new BorderLayout());
 
     int indexToSelect = initializeUsers();
+    final UsersListCellRenderer usersListCellRenderer = new UsersListCellRenderer();
 
     //Create the list that displays the users and put it in a scroll pane.
-    list = new JBList(listModel);
+    list = new JBList(listModel) {
+      @Override
+      public Dimension getPreferredScrollableViewportSize() {
+        int numUsers = listModel.size();
+        Dimension superPreferredSize = super.getPreferredScrollableViewportSize();
+        if(numUsers <= 1) {
+          return superPreferredSize;
+        }
+
+        if(GoogleLogin.getInstance().getActiveUser() == null){
+          return superPreferredSize;
+        } else if(!isActiveUserInVisibleArea()) {
+          return superPreferredSize;
+        } else {
+          // if there is an active user in the visible area
+          int usersToShow = numUsers > MAX_VISIBLE_ROW_COUNT ? MAX_VISIBLE_ROW_COUNT : numUsers;
+          int scrollHeight = ((usersToShow - 1) *  usersListCellRenderer.getMainPanelHeight())
+            + usersListCellRenderer.getActivePanelHeight();
+          return new Dimension((int)superPreferredSize.getWidth(), scrollHeight);
+        }
+      }
+    };
+
     list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
     list.setSelectedIndex(indexToSelect);
     list.addListSelectionListener(this);
     list.setVisibleRowCount(getVisibleRowCount());
-    list.setCellRenderer(new UsersListCellRenderer());
+    list.setCellRenderer(usersListCellRenderer);
     JBScrollPane listScrollPane = new JBScrollPane(list);
 
-    addAccountButton = new JButton(addAccountString);
+    list.addMouseListener(new MouseAdapter() {
+      @Override
+      public void mouseClicked(MouseEvent mouseEvent) {
+        list.updateUI();
+
+        if(listModel.getSize() == 1 && (listModel.get(0) instanceof NoUsersListItem)) {
+          // When there are no users available
+          if(usersListCellRenderer.inLearnMoreUrl(mouseEvent.getPoint())){
+            BrowserUtil.browse(LEARN_MORE_URL);
+          }
+        } else {
+          // When users are available
+          if(!valueChanged) {
+            // Clicking on an already active user
+            int index = list.locationToIndex(mouseEvent.getPoint());
+            if (index >= 0) {
+              boolean inPlayUrl = usersListCellRenderer.inPlayConsoleUrl(mouseEvent.getPoint(), index);
+              if(inPlayUrl){
+                BrowserUtil.browse(PLAY_CONSOLE_URL);
+              } else {
+                boolean inCloudUrl = usersListCellRenderer.inCloudConsoleUrl(mouseEvent.getPoint(), index);
+                if(inCloudUrl) {
+                  BrowserUtil.browse(CLOUD_CONSOLE_URL);
+                }
+              }
+            }
+          }
+        }
+        valueChanged = false;
+      }
+    });
+
+    list.addMouseMotionListener(new MouseMotionListener() {
+      @Override
+      public void mouseMoved(MouseEvent mouseEvent) {
+        // Determine if the user under the cursor is an active user, a non-active user or a non-user
+        int index = list.locationToIndex(mouseEvent.getPoint());
+        if (index >= 0) {
+          // If current object is the non-user list item, use default cursor
+          Object currentObject = listModel.get(index);
+          if(currentObject instanceof NoUsersListItem) {
+            if(usersListCellRenderer.inLearnMoreUrl(mouseEvent.getPoint())) {
+              list.setCursor(new Cursor(Cursor.HAND_CURSOR));
+            } else {
+              list.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
+            }
+            return;
+          }
+
+          if (((UsersListItem)currentObject).isActiveUser()) {
+            // Active user
+            boolean inPlayUrl = usersListCellRenderer.inPlayConsoleUrl(mouseEvent.getPoint(), index);
+            boolean inCloudUrl = usersListCellRenderer.inCloudConsoleUrl(mouseEvent.getPoint(), index);
+            if (inPlayUrl || inCloudUrl) {
+              list.setCursor(new Cursor(Cursor.HAND_CURSOR));
+            } else {
+              list.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
+            }
+          } else {
+            // For non-active user
+            list.setCursor(new Cursor(Cursor.HAND_CURSOR));
+          }
+        }
+      }
+
+      @Override
+      public void mouseDragged(MouseEvent e) {
+      }
+    });
+
+    boolean noUsersAvailable = (listModel.getSize() == 1) && (listModel.get(0) instanceof NoUsersListItem);
+    addAccountButton = new JButton(noUsersAvailable ? signInString : addAccountString);
     AddAccountListener addAccountListener = new AddAccountListener();
     addAccountButton.addActionListener(addAccountListener);
     addAccountButton.setHorizontalAlignment(SwingConstants.LEFT);
@@ -75,7 +180,13 @@
     if(list.isSelectionEmpty()) {
       signOutButton.setEnabled(false);
     } else {
-      signOutButton.setEnabled(true);
+      // If list contains the NoUsersListItem place holder
+      // sign out button should be hidden
+      if(noUsersAvailable) {
+        signOutButton.setVisible(false);
+      } else {
+        signOutButton.setEnabled(true);
+      }
     }
 
     //Create a panel to hold the buttons
@@ -96,18 +207,7 @@
   class SignOutListener implements ActionListener {
     @Override
     public void actionPerformed(ActionEvent e) {
-      //This method can be called only if there's a valid selection
-      int index = list.getSelectedIndex();
-
-      boolean signedOut = GoogleLogin.getInstance().logOut();
-      if(signedOut) {
-        // remove logged out user
-        listModel.remove(index);
-        if (listModel.getSize() == 0) {
-          signOutButton.setEnabled(false);
-
-        }
-      }
+      GoogleLogin.getInstance().logOut();
     }
   }
 
@@ -119,20 +219,18 @@
     public void actionPerformed(ActionEvent e) {
       GoogleLogin.getInstance().logIn();
     }
-
-    protected boolean alreadyInList(String name) {
-      return listModel.contains(name);
-    }
   }
 
   //This method is required by ListSelectionListener.
   @Override
   public void valueChanged(ListSelectionEvent e) {
+    if(ignoreSelection) {
+      return;
+    }
+    valueChanged = true;
     if (e.getValueIsAdjusting() == false) {
-
       if (list.getSelectedIndex() == -1) {
         signOutButton.setEnabled(false);
-
       } else {
         signOutButton.setEnabled(true);
 
@@ -141,6 +239,19 @@
         if(!selectedUser.isActiveUser()) {
           GoogleLogin.getInstance().setActiveUser(selectedUser.getUserEmail());
         }
+
+        // Change order of elements in the list so that the
+        // active user becomes the first element in the list
+        ignoreSelection = true;
+        try {
+          listModel.remove(list.getSelectedIndex());
+          listModel.add(0, selectedUser);
+
+          // Re-select the active user
+          list.setSelectedIndex(0);
+        } finally {
+          ignoreSelection = false;
+        }
       }
     }
   }
@@ -150,7 +261,7 @@
   }
 
   private int initializeUsers() {
-    Map<String, CredentialedUser> allUsers = GoogleLogin.getInstance().getAllUsers();
+    LinkedHashMap<String, CredentialedUser> allUsers = GoogleLogin.getInstance().getAllUsers();
     listModel = new DefaultListModel();
 
     int activeUserIndex = allUsers.size();
@@ -161,6 +272,17 @@
       }
     }
 
+    if(listModel.getSize() == 0) {
+      // Add no user panel
+      listModel.addElement(NoUsersListItem.INSTANCE);
+    } else if ((activeUserIndex != 0) && (activeUserIndex < listModel.getSize())) {
+      // Change order of elements in the list so that the
+      // active user becomes the first element in the list
+      UsersListItem activeUser = (UsersListItem)listModel.remove(activeUserIndex);
+      listModel.add(0, activeUser);
+      activeUserIndex = 0;
+    }
+
     return activeUserIndex;
   }
 
@@ -178,4 +300,16 @@
       return  size;
     }
   }
+
+  private boolean isActiveUserInVisibleArea() {
+    int max = listModel.getSize() < MAX_VISIBLE_ROW_COUNT ?
+      listModel.getSize() : MAX_VISIBLE_ROW_COUNT;
+
+    for(int i = 0; i < max; i++){
+      if(((UsersListItem)listModel.get(i)).isActiveUser()) {
+        return true;
+      }
+    }
+    return false;
+  }
 }
diff --git a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java b/login/src/com/google/gct/login/ui/NoUsersListItem.java
similarity index 68%
copy from login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
copy to login/src/com/google/gct/login/ui/NoUsersListItem.java
index 771081b..26b1427 100644
--- a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
+++ b/login/src/com/google/gct/login/ui/NoUsersListItem.java
@@ -13,8 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.gct.login;
+package com.google.gct.login.ui;
 
-public interface IGoogleLoginUpdateUser {
-  public void updateUser();
+/**
+ * A place holder for when no user exist. This allows us to create
+ * a customized panel when no users exist.
+ */
+public class NoUsersListItem {
+  public static NoUsersListItem INSTANCE = new NoUsersListItem();
+
+  private NoUsersListItem() {
+  }
 }
diff --git a/login/src/com/google/gct/login/ui/UsersListCellRenderer.java b/login/src/com/google/gct/login/ui/UsersListCellRenderer.java
index d897609..e6fee90 100644
--- a/login/src/com/google/gct/login/ui/UsersListCellRenderer.java
+++ b/login/src/com/google/gct/login/ui/UsersListCellRenderer.java
@@ -16,10 +16,12 @@
 package com.google.gct.login.ui;
 
 import com.intellij.ui.JBColor;
-import com.intellij.ui.components.JBCheckBox;
 import com.intellij.util.ui.UIUtil;
 
+import javax.swing.BorderFactory;
+import javax.swing.Box;
 import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JList;
@@ -29,95 +31,284 @@
 import java.awt.Color;
 import java.awt.Component;
 import java.awt.Dimension;
+import java.awt.FlowLayout;
 import java.awt.Font;
-import java.awt.Graphics;
+import java.awt.FontMetrics;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
 import java.awt.GridLayout;
 import java.awt.Image;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.net.URL;
+
 
 /**
  * A custom cell render for {@link GoogleLoginUsersPanel#list} that manages
  * how each user item in the Google Login panel would be displayed.
  */
 public class UsersListCellRenderer extends JComponent implements ListCellRenderer {
-  private final Color ACTIVE_COLOR = JBColor.LIGHT_GRAY;
-  private final Font PLAIN_NAME_FONT;
-  private final Font PLAIN_EMAIL_FONT;
+  private final static String CLOUD_LABEL_TEXT = "Open Google Developers Console";
+  private final static String PLAY_LABEL_TEXT = "Open Play Developer Console";
+  private final static String DEFAULT_AVATAR = "/icons/loginAvatar@2x.png";
+  private final static String GOOGLE_IMG = "/icons/google.png";
+  private final static String SIGN_IN_TEXT = "<HTML> Sign in with your Google account to start <br> adding "
+    + "Cloud functionality to your <br> Android applications from Android Studio. </HTML>";
+  private final static String LEARN_MORE_TEXT = "Learn more";
+  private final Color LEARN_MORE_COLOR;
+  private final Color SIGN_IN_COLOR;
+  private final Color ACTIVE_COLOR;
+  private final Color INACTIVE_COLOR;
+  private final int PLAIN_USER_IMAGE_WIDTH = 48;
+  private final int PLAIN_USER_IMAGE_HEIGHT = 48;
+  private final int ACTIVE_USER_IMAGE_WIDTH = 96;
+  private final int ACTIVE_USER_IMAGE_HEIGHT = 96;
+  private final int GOOGLE_IMAGE_WIDTH = 96;
+  private final int GOOGLE_IMAGE_HEIGHT = 35;
+  private final int GOOGLE_IMAGE_NORTH = 18;
+  private final int GOOGLE_IMAGE_WEST = 18;
+  private final int WELCOME_LABEL_NORTH = 15;
+  private final int WELCOME_LABEL_SOUTH = 25;
+  private final int WELCOME_LABEL_EAST = 38;
+  private final int USER_LABEL_VERTICAL_STRUT = 3;
+  private final int HGAP = 10;
+  private final int VGAP = 10;
+  private final int GENERAL_FONT_HEIGHT;
+  private final Font NAME_FONT;
+  private final Font GENERAL_FONT;
   private final Dimension MAIN_PANEL_DIMENSION;
+  private final Dimension ACTIVE_MAIN_PANEL_DIMENSION;
+  private final Dimension CLOUD_LABEL_DIMENSION;
+  private final Dimension PLAY_LABEL_DIMENSION;
+  private final Dimension LEARN_MORE_LABEL_DIMENSION;
 
   public UsersListCellRenderer() {
-    PLAIN_NAME_FONT = new Font("Helvetica", Font.BOLD, 13);
-    PLAIN_EMAIL_FONT = new Font("Helvetica", Font.PLAIN, 13);;
+    NAME_FONT = new Font("Helvetica", Font.BOLD, 13);
+    GENERAL_FONT = new Font("Helvetica", Font.PLAIN, 13);
     MAIN_PANEL_DIMENSION = new Dimension(250, 68);
+    ACTIVE_MAIN_PANEL_DIMENSION = new Dimension(250, 116);
+    SIGN_IN_COLOR = new Color(666666);
+    LEARN_MORE_COLOR = new Color(666);
+
+    ACTIVE_COLOR = new Color(0xffffff);
+    INACTIVE_COLOR = new Color(0xf5f5f5);
+
+    FontMetrics fontMetrics = getFontMetrics(GENERAL_FONT);
+    GENERAL_FONT_HEIGHT = fontMetrics.getHeight();
+    CLOUD_LABEL_DIMENSION = new Dimension(fontMetrics.stringWidth(CLOUD_LABEL_TEXT), GENERAL_FONT_HEIGHT);
+    PLAY_LABEL_DIMENSION = new Dimension(fontMetrics.stringWidth(PLAY_LABEL_TEXT), GENERAL_FONT_HEIGHT);
+    LEARN_MORE_LABEL_DIMENSION = new Dimension(fontMetrics.stringWidth(LEARN_MORE_TEXT), GENERAL_FONT_HEIGHT);
   }
 
   @Override
   public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+    if(value instanceof NoUsersListItem) {
+      return createNoUserDisplay();
+    }
+
     if(!(value instanceof UsersListItem)) {
       return null;
     }
+    UsersListItem usersListItem = (UsersListItem)value;
 
-    JPanel mainPanel = new JPanel();
-    setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
-    mainPanel.setPreferredSize(MAIN_PANEL_DIMENSION);
+    boolean calcIsSelected;
+    if (list.getSelectedIndex() == index) {
+      calcIsSelected = true;
+    } else {
+      calcIsSelected = false;
+    }
+
+    JPanel mainPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, HGAP, VGAP));
+    mainPanel.setMinimumSize(calcIsSelected ? ACTIVE_MAIN_PANEL_DIMENSION : MAIN_PANEL_DIMENSION);
     mainPanel.setAlignmentX(LEFT_ALIGNMENT);
-    // TODO: make mainPanel components left-justified
 
     // Update colors
-    final Color bg = isSelected ? ACTIVE_COLOR : UIUtil.getListBackground();
-    final Color fg = isSelected ? UIUtil.getListSelectionForeground() : UIUtil.getListForeground();
+    final Color bg = calcIsSelected ? ACTIVE_COLOR : INACTIVE_COLOR;
+    final Color fg = calcIsSelected ? UIUtil.getListSelectionForeground() : UIUtil.getListForeground();
     mainPanel.setBackground(bg);
     mainPanel.setForeground(fg);
 
+    // TODO: add step to cache scaled image
+    Image image = usersListItem.getUserPicture();
+    if(image == null){
+      // use default image
+      URL url = UsersListCellRenderer.class.getResource(DEFAULT_AVATAR);
+      image = Toolkit.getDefaultToolkit().getImage(url);
+    }
 
-    Image imageIcon = ((UsersListItem)value).getUserPicture();
-    mainPanel.add(new ImagePanel(imageIcon));
-    mainPanel.add(createTextDisplay(isSelected, (UsersListItem)value));
+    int imageWidth = calcIsSelected ? ACTIVE_USER_IMAGE_WIDTH : PLAIN_USER_IMAGE_WIDTH;
+    int imageHeight = calcIsSelected ? ACTIVE_USER_IMAGE_HEIGHT : PLAIN_USER_IMAGE_HEIGHT;
+    Image scaledImage = image.getScaledInstance(imageWidth, imageHeight, java.awt.Image.SCALE_SMOOTH);
 
-    // TODO: add Separator to bottom of panel
+    JComponent textPanel;
+    if (calcIsSelected) {
+      textPanel =  createActiveTextDisplay(usersListItem);
+    } else {
+      textPanel =  createTextDisplay(calcIsSelected, usersListItem);
+    }
+
+    mainPanel.add(new JLabel(new ImageIcon(scaledImage)));
+    mainPanel.add(textPanel);
+    mainPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, UIUtil.getBorderColor()));
 
     return mainPanel;
   }
 
-  protected JComponent createTextDisplay(boolean isSelected, UsersListItem usersListItem) {
-    final JPanel panel = new JPanel();
-    panel.setLayout(new GridLayout(2,1));
+  public boolean inPlayConsoleUrl(Point point, int activeIndex) {
+    // 2 is for the number of labels before this one
+    double playYStart = VGAP + ACTIVE_USER_IMAGE_HEIGHT - PLAY_LABEL_DIMENSION.getHeight()
+      - CLOUD_LABEL_DIMENSION.getHeight() - 2 + (MAIN_PANEL_DIMENSION.getHeight() * activeIndex)
+      + USER_LABEL_VERTICAL_STRUT;
+    double playYEnd = playYStart + PLAY_LABEL_DIMENSION.getHeight();
+    double playXStart = ACTIVE_USER_IMAGE_WIDTH + HGAP + VGAP;
+    double playXEnd = playXStart + PLAY_LABEL_DIMENSION.getWidth();
 
-    final Color bg = isSelected ? ACTIVE_COLOR : UIUtil.getListBackground();
+    if((point.getX() > playXStart) && (point.getX() < playXEnd)
+       && (point.getY() > playYStart) && (point.getY() < playYEnd)) {
+      return true;
+    }
+
+    return false;
+  }
+
+  public boolean inCloudConsoleUrl(Point point, int activeIndex) {
+    // 3 is for the number of labels before this one
+    double playYStart = VGAP + ACTIVE_USER_IMAGE_HEIGHT - CLOUD_LABEL_DIMENSION.getHeight()
+      - 3 + (MAIN_PANEL_DIMENSION.getHeight() * activeIndex) + (USER_LABEL_VERTICAL_STRUT * 2);
+    double playYEnd = playYStart + CLOUD_LABEL_DIMENSION.getHeight();
+    double playXStart = ACTIVE_USER_IMAGE_WIDTH + HGAP + VGAP;
+    double playXEnd = playXStart + CLOUD_LABEL_DIMENSION.getWidth();
+
+    if((point.getX() > playXStart) && (point.getX() < playXEnd)
+       && (point.getY() > playYStart) && (point.getY() < playYEnd)) {
+      return true;
+    }
+
+    return false;
+  }
+
+  public boolean inLearnMoreUrl(Point point) {
+    // 3 is for the number of labels and row of texts
+    double urlYStart = GOOGLE_IMAGE_NORTH + GOOGLE_IMAGE_HEIGHT + WELCOME_LABEL_NORTH
+      + (GENERAL_FONT_HEIGHT * 3) + 3;
+    double urlYEnd = urlYStart + LEARN_MORE_LABEL_DIMENSION.getHeight();
+    double urlXStart = GOOGLE_IMAGE_WEST;
+    double urlXEnd = urlXStart + LEARN_MORE_LABEL_DIMENSION.getWidth();
+
+    if((point.getX() > urlXStart) && (point.getX() < urlXEnd)
+       && (point.getY() > urlYStart) && (point.getY() < urlYEnd)) {
+      return true;
+    }
+
+    return false;
+  }
+
+  public int getMainPanelHeight() {
+    return (int)MAIN_PANEL_DIMENSION.getHeight();
+  }
+
+  public int getActivePanelHeight() {
+    return (int)ACTIVE_MAIN_PANEL_DIMENSION.getHeight();
+  }
+
+  private JComponent createTextDisplay(boolean isSelected, UsersListItem usersListItem) {
+    final JPanel panel = new JPanel();
+    panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+
+    final Color bg = isSelected ? ACTIVE_COLOR : INACTIVE_COLOR;
     final Color fg = isSelected ? UIUtil.getListSelectionForeground() : UIUtil.getListForeground();
     panel.setBackground(bg);
     panel.setForeground(fg);
 
     JLabel nameLabel = new JLabel( usersListItem.getUserName());
-    nameLabel.setFont(PLAIN_NAME_FONT);
+    nameLabel.setFont(NAME_FONT);
+    nameLabel.setForeground(JBColor.BLACK);
     panel.add(nameLabel);
+    panel.add(Box.createVerticalStrut(USER_LABEL_VERTICAL_STRUT));
 
     JLabel emailLabel = new JLabel(usersListItem.getUserEmail());
-    emailLabel.setFont(PLAIN_EMAIL_FONT);
+    emailLabel.setFont(GENERAL_FONT);
     panel.add(emailLabel);
 
     return panel;
   }
 
-  private class ImagePanel extends JPanel {
-    private Image img;
-    private final int IMAGE_STARTING_POINT_X = 10;
-    private final int IMAGE_STARTING_POINT_Y = 10;
-    private final Dimension PANEL_DIMENSION = new Dimension(68, 68);
-    private final Dimension PLAIN_IMAGE_SIZE = new Dimension(48, 48);
+  private JComponent createActiveTextDisplay(UsersListItem usersListItem) {
+    JPanel mainPanel = new JPanel();
+    mainPanel.setLayout(new GridBagLayout());
 
-    public ImagePanel(Image image) {
-      img = image;
-      setPreferredSize(PANEL_DIMENSION);
-      setMinimumSize(PANEL_DIMENSION);
-      setMaximumSize(PANEL_DIMENSION);
-      setSize(PANEL_DIMENSION);
-      setLayout(null);
-    }
+    mainPanel.setBackground(ACTIVE_COLOR);
+    mainPanel.setForeground(UIUtil.getListSelectionForeground());
+    mainPanel.setPreferredSize(new Dimension(220, ACTIVE_USER_IMAGE_HEIGHT));
 
-    @Override
-    public void paintComponent(Graphics graphics) {
-       graphics.drawImage(img, IMAGE_STARTING_POINT_X, IMAGE_STARTING_POINT_Y,
-        PLAIN_IMAGE_SIZE.width, PLAIN_IMAGE_SIZE.height, null);
-    }
+    JPanel bottomPanel = new JPanel();
+    bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.PAGE_AXIS));
+    bottomPanel.setBackground(ACTIVE_COLOR);
+    bottomPanel.setForeground(UIUtil.getListSelectionForeground());
+    bottomPanel.setPreferredSize(new Dimension(220, (GENERAL_FONT_HEIGHT * 2) + USER_LABEL_VERTICAL_STRUT));
+
+    JLabel playLabel = new JLabel(PLAY_LABEL_TEXT);
+    playLabel.setFont(GENERAL_FONT);
+    playLabel.setForeground(JBColor.BLUE);
+    playLabel.setPreferredSize(PLAY_LABEL_DIMENSION);
+    bottomPanel.add(playLabel, BOTTOM_ALIGNMENT);
+    bottomPanel.add(Box.createVerticalStrut(USER_LABEL_VERTICAL_STRUT));
+
+    JLabel cloudLabel = new JLabel(CLOUD_LABEL_TEXT);
+    cloudLabel.setFont(GENERAL_FONT);
+    cloudLabel.setForeground(JBColor.BLUE);
+    cloudLabel.setPreferredSize(CLOUD_LABEL_DIMENSION);
+    bottomPanel.add(cloudLabel, BOTTOM_ALIGNMENT);
+
+    GridBagConstraints topConstraints = new GridBagConstraints();
+    topConstraints.gridx = 0;
+    topConstraints.gridy = 0;
+    topConstraints.anchor = GridBagConstraints.NORTHWEST;
+
+    GridBagConstraints bottomConstraints = new GridBagConstraints();
+    bottomConstraints.gridx = 0;
+    bottomConstraints.gridy = 1;
+    bottomConstraints.weightx = 1;
+    bottomConstraints.weighty = 5;
+    bottomConstraints.anchor = GridBagConstraints.SOUTHWEST;
+
+    JComponent topPanel = createTextDisplay(true, usersListItem);
+    mainPanel.add(topPanel, topConstraints);
+    mainPanel.add(bottomPanel, bottomConstraints);
+    return mainPanel;
+  }
+
+  private JPanel createNoUserDisplay() {
+    JPanel mainPanel = new JPanel();
+    BoxLayout layout = new BoxLayout(mainPanel, BoxLayout.Y_AXIS);
+    mainPanel.setLayout(layout);
+    mainPanel.setBackground(JBColor.WHITE);
+    mainPanel.setBorder(BorderFactory.createEmptyBorder(0, GOOGLE_IMAGE_WEST, 0, 0));
+
+    URL url = UsersListCellRenderer.class.getResource(GOOGLE_IMG);
+    Image image = Toolkit.getDefaultToolkit().getImage(url);
+    Image scaledImage = image.getScaledInstance(
+      GOOGLE_IMAGE_WIDTH, GOOGLE_IMAGE_HEIGHT, java.awt.Image.SCALE_SMOOTH);
+    JLabel imageLabel = new JLabel(new ImageIcon(scaledImage));
+
+    JLabel signInLabel = new JLabel(SIGN_IN_TEXT);
+    signInLabel.setFont(GENERAL_FONT);
+    signInLabel.setForeground(SIGN_IN_COLOR);
+    Dimension textSize = signInLabel.getPreferredSize();
+    signInLabel.setPreferredSize(new Dimension((int)textSize.getWidth() + WELCOME_LABEL_EAST, (int)textSize.getHeight()));
+
+    JLabel urlLabel = new JLabel(LEARN_MORE_TEXT);
+    urlLabel.setFont(GENERAL_FONT);
+    urlLabel.setForeground(LEARN_MORE_COLOR);
+    urlLabel.setPreferredSize(LEARN_MORE_LABEL_DIMENSION);
+
+    mainPanel.add(Box.createVerticalStrut(GOOGLE_IMAGE_NORTH));
+    mainPanel.add(imageLabel);
+    mainPanel.add(Box.createVerticalStrut(WELCOME_LABEL_NORTH));
+    mainPanel.add(signInLabel);
+    mainPanel.add(urlLabel);
+    mainPanel.add(Box.createVerticalStrut(WELCOME_LABEL_SOUTH));
+
+    return mainPanel;
   }
 }
diff --git a/resources/templates/GcmEndpoints/root/build.gradle.ftl b/resources/templates/GcmEndpoints/root/build.gradle.ftl
index cc39703..70ae1f9 100644
--- a/resources/templates/GcmEndpoints/root/build.gradle.ftl
+++ b/resources/templates/GcmEndpoints/root/build.gradle.ftl
@@ -1,6 +1,3 @@
-// Currently, the appengine gradle plugin's appengine devappserver launch doesn't interact well with Intellij/AndroidStudio's
-// Gradle integration.  As a temporary solution, please launch from the command line.
-// ./gradlew modulename:appengineRun
 // If you would like more information on the gradle-appengine-plugin please refer to the github page
 // https://github.com/GoogleCloudPlatform/gradle-appengine-plugin
 
diff --git a/resources/templates/GcmEndpoints/root/src/main/MessagingEndpoint.java.ftl b/resources/templates/GcmEndpoints/root/src/main/MessagingEndpoint.java.ftl
index 05a161a..09e8cc1 100644
--- a/resources/templates/GcmEndpoints/root/src/main/MessagingEndpoint.java.ftl
+++ b/resources/templates/GcmEndpoints/root/src/main/MessagingEndpoint.java.ftl
@@ -1,3 +1,9 @@
+/*
+   For step-by-step instructions on connecting your Android application to this backend module,
+   see "App Engine Backend with Google Cloud Messaging" template documentation at
+   https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/GcmEndpoints
+*/
+
 package ${packageName};
 
 import com.google.android.gcm.server.Constants;
diff --git a/resources/templates/GcmEndpoints/root/src/main/RegistrationEndpoint.java.ftl b/resources/templates/GcmEndpoints/root/src/main/RegistrationEndpoint.java.ftl
index 95fa77d..0f4d085 100644
--- a/resources/templates/GcmEndpoints/root/src/main/RegistrationEndpoint.java.ftl
+++ b/resources/templates/GcmEndpoints/root/src/main/RegistrationEndpoint.java.ftl
@@ -1,3 +1,9 @@
+/*
+   For step-by-step instructions on connecting your Android application to this backend module,
+   see "App Engine Backend with Google Cloud Messaging" template documentation at
+   https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/GcmEndpoints
+*/
+
 package ${packageName};
 
 import com.google.api.server.spi.config.Api;
@@ -73,4 +79,4 @@
         return ofy().load().type(RegistrationRecord.class).filter("regId", regId).first().now();
     }
 
-}
\ No newline at end of file
+}
diff --git a/resources/templates/GcmEndpoints/root/src/webapp/index.html.ftl b/resources/templates/GcmEndpoints/root/src/webapp/index.html.ftl
index 7070937..21e9927 100644
--- a/resources/templates/GcmEndpoints/root/src/webapp/index.html.ftl
+++ b/resources/templates/GcmEndpoints/root/src/webapp/index.html.ftl
@@ -101,8 +101,13 @@
     function init() {
       var apiName = 'messaging'
       var apiVersion = 'v1'
-      // set the apiRoot to work on a deployed app and locally
-      var apiRoot = '//' + window.location.host + '/_ah/api';
+      var apiRoot = 'https://' + window.location.host + '/_ah/api';
+      if (window.location.hostname == 'localhost'
+          || window.location.hostname == '127.0.0.1'
+          || ((window.location.port != "") && (window.location.port > 1023))) {
+            // We're probably running against the DevAppServer
+            apiRoot = 'http://' + window.location.host + '/_ah/api';
+      }
       var callback = function() {
         enableClick();
       }
diff --git a/resources/templates/GcmEndpoints/template.xml b/resources/templates/GcmEndpoints/template.xml
index d853856..deeab15 100644
--- a/resources/templates/GcmEndpoints/template.xml
+++ b/resources/templates/GcmEndpoints/template.xml
@@ -27,7 +27,7 @@
   <parameter id="endpointOwnerDomain" name="Endpoint Owner Domain" type="string" constraints="package" default="mycompany.com"/>
   <parameter id="endpointPackagePath" name="Endpoint Package Path" type="string" constraints="package" default="myapp"/>
   <parameter id="apiKey" name="Gcm Api Key" type="string" default="REPLACE WITH YOUR API KEY"/>
-  <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.6" />
+  <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.8" />
   <parameter id="docUrl" name="Doc URL" type="string" visibility="false"
              default="https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/GcmEndpoints"/>
   <parameter id="sideIconPath" name="Side icon path" type="string" visibility="false"
diff --git a/resources/templates/HelloEndpoints/root/build.gradle.ftl b/resources/templates/HelloEndpoints/root/build.gradle.ftl
index cfa6468..f77265d 100644
--- a/resources/templates/HelloEndpoints/root/build.gradle.ftl
+++ b/resources/templates/HelloEndpoints/root/build.gradle.ftl
@@ -1,6 +1,3 @@
-// Currently, the appengine gradle plugin's appengine devappserver launch doesn't interact well with Intellij/AndroidStudio's
-// Gradle integration.  As a temporary solution, please launch from the command line.
-// ./gradlew modulename:appengineRun
 // If you would like more information on the gradle-appengine-plugin please refer to the github page
 // https://github.com/GoogleCloudPlatform/gradle-appengine-plugin
 
diff --git a/resources/templates/HelloEndpoints/root/src/main/MyEndpoint.java.ftl b/resources/templates/HelloEndpoints/root/src/main/MyEndpoint.java.ftl
index 9a4c188..b420697 100644
--- a/resources/templates/HelloEndpoints/root/src/main/MyEndpoint.java.ftl
+++ b/resources/templates/HelloEndpoints/root/src/main/MyEndpoint.java.ftl
@@ -1,3 +1,9 @@
+/*
+   For step-by-step instructions on connecting your Android application to this backend module,
+   see "App Engine Java Endpoints Module" template documentation at
+   https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/HelloEndpoints
+*/
+
 package ${packageName};
 
 import com.google.api.server.spi.config.Api;
@@ -19,4 +25,4 @@
         return response;
     }
 
-}
\ No newline at end of file
+}
diff --git a/resources/templates/HelloEndpoints/root/src/webapp/index.html.ftl b/resources/templates/HelloEndpoints/root/src/webapp/index.html.ftl
index a75f804..6116784 100644
--- a/resources/templates/HelloEndpoints/root/src/webapp/index.html.ftl
+++ b/resources/templates/HelloEndpoints/root/src/webapp/index.html.ftl
@@ -92,7 +92,13 @@
     function init() {
       var apiName = 'myApi';
       var apiVersion = 'v1';
-      var apiRoot = '//' + window.location.host + '/_ah/api';
+      var apiRoot = 'https://' + window.location.host + '/_ah/api';
+      if (window.location.hostname == 'localhost'
+          || window.location.hostname == '127.0.0.1'
+          || ((window.location.port != "") && (window.location.port > 1023))) {
+            // We're probably running against the DevAppServer
+            apiRoot = 'http://' + window.location.host + '/_ah/api';
+      }
       var callback = function() {
         enableClick();
       }
diff --git a/resources/templates/HelloEndpoints/template.xml b/resources/templates/HelloEndpoints/template.xml
index 78ed749..e0b0ab1 100644
--- a/resources/templates/HelloEndpoints/template.xml
+++ b/resources/templates/HelloEndpoints/template.xml
@@ -26,7 +26,7 @@
   <parameter id="appId" name="Application Id" type="string" constraints="nonempty" default="myApplicationId"/>
   <parameter id="endpointOwnerDomain" name="Endpoint Owner Domain" type="string" constraints="package" default="mycompany.com"/>
   <parameter id="endpointPackagePath" name="Endpoint Package Path" type="string" constraints="package" default="myapp"/>
-  <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.6" />
+  <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.8" />
   <parameter id="docUrl" name="Doc URL" type="string" visibility="false"
              default="https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/HelloEndpoints"/>
 
diff --git a/resources/templates/HelloWorld/root/build.gradle.ftl b/resources/templates/HelloWorld/root/build.gradle.ftl
index 8cb5faa..6d73c1b 100644
--- a/resources/templates/HelloWorld/root/build.gradle.ftl
+++ b/resources/templates/HelloWorld/root/build.gradle.ftl
@@ -1,6 +1,3 @@
-// Currently, the appengine gradle plugin's appengine devappserver launch doesn't interact well with Intellij/AndroidStudio's
-// Gradle integration.  As a temporary solution, please launch from the command line.
-// ./gradlew modulename:appengineRun
 // If you would like more information on the gradle-appengine-plugin please refer to the github page
 // https://github.com/GoogleCloudPlatform/gradle-appengine-plugin
 
diff --git a/resources/templates/HelloWorld/root/src/main/MyServlet.java.ftl b/resources/templates/HelloWorld/root/src/main/MyServlet.java.ftl
index bf30f05..9f89f42 100644
--- a/resources/templates/HelloWorld/root/src/main/MyServlet.java.ftl
+++ b/resources/templates/HelloWorld/root/src/main/MyServlet.java.ftl
@@ -1,3 +1,9 @@
+/*
+   For step-by-step instructions on connecting your Android application to this backend module,
+   see "App Engine Java Servlet Module" template documentation at
+   https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/HelloWorld
+*/
+
 package ${packageName};
 
 import java.io.IOException;
@@ -21,4 +27,4 @@
         }
         resp.getWriter().println("Hello " + name);
     }
-}
\ No newline at end of file
+}
diff --git a/resources/templates/HelloWorld/template.xml b/resources/templates/HelloWorld/template.xml
index 8be72c9..6cada3e 100644
--- a/resources/templates/HelloWorld/template.xml
+++ b/resources/templates/HelloWorld/template.xml
@@ -24,7 +24,7 @@
 
   <parameter id="packageName" name="Package name" type="string" constraints="package|nonempty" default="com.mycompany.myapp"/>
   <parameter id="appId" name="Application Id" type="string" constraints="nonempty" default="myApplicationId"/>
-  <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.6" />
+  <parameter id="appEngineVersion" name="AppEngine Version" type="string" default="1.9.8" />
   <parameter id="docUrl" name="Doc URL" type="string" visibility="false"
              default="https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/HelloWorld"/>
   <parameter id="sideIconPath" name="Side icon path" type="string" visibility="false"
diff --git a/src/META-INF/plugin.xml b/src/META-INF/plugin.xml
index c428928..ecba450 100644
--- a/src/META-INF/plugin.xml
+++ b/src/META-INF/plugin.xml
@@ -18,6 +18,9 @@
       <implementation-class>com.google.gct.idea.appengine.synchronization.SampleSyncRegistration</implementation-class>
     </component>
     -->
+    <component>
+      <implementation-class>com.google.gct.idea.appengine.initialization.CloudPluginRegistration</implementation-class>
+    </component>
   </application-components>
 
   <project-components>
@@ -44,6 +47,9 @@
     <runConfigurationProducer implementation="com.google.gct.idea.appengine.run.AppEngineRunConfigurationProducer"/>
     -->
 
+    <!-- Dom for the App Engine config file -->
+    <dom.fileDescription implementation="com.google.gct.idea.appengine.dom.AppEngineWebFileDescription"/>
+
     <implicitUsageProvider implementation="com.google.gct.idea.appengine.validation.EndpointImplicitUsageProvider"/>
 
     <localInspection language="JAVA" shortName="ApiName" bundle="messages.EndpointBundle"  hasStaticDescription="true"
@@ -117,23 +123,6 @@
       <reference id="GoogleCloudTools.GenerateEndpoint"/>
       <add-to-group group-id="ToolsMenu" anchor="last"/>
     </group>
-
-    <!-- Google Login -->
-    <!--
-    <action id="GoogleLogin.UserProfile"
-            class="com.google.gct.login.ui.GoogleLoginAction"
-            text="Google Login">
-      <add-to-group group-id="MainToolBar" anchor="first"  />
-    </action>
-    -->
-
   </actions>
 
-  <!-- Extension point for Google Login Listener -->
-  <!--
-  <extensionPoints>
-    <extensionPoint name="googleLoginListener" interface="com.google.gct.login.GoogleLoginListener"/>
-  </extensionPoints>
-  -->
-
 </idea-plugin>
diff --git a/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateAction.java b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateAction.java
new file mode 100644
index 0000000..b50389d
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateAction.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2014 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.idea.appengine.deploy;
+
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.LangDataKeys;
+import com.intellij.openapi.module.Module;
+
+/**
+ * Handles the menu action to deploy to AppEngine.
+ */
+public class AppEngineUpdateAction extends AnAction {
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    final Module selectedModule = LangDataKeys.MODULE.getData(e.getDataContext());
+
+    AppEngineUpdateDialog.show(e.getProject(), selectedModule);
+  }
+}
diff --git a/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.form b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.form
new file mode 100644
index 0000000..249d6a6
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.form
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.google.gct.idea.appengine.deploy.AppEngineUpdateDialog">
+  <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="2" vgap="2">
+    <margin top="0" left="0" bottom="0" right="0"/>
+    <constraints>
+      <xy x="20" y="20" width="306" height="113"/>
+    </constraints>
+    <properties>
+      <preferredSize width="275" height="135"/>
+    </properties>
+    <border type="none"/>
+    <children>
+      <component id="c6667" class="com.intellij.ui.components.JBLabel">
+        <constraints>
+          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Module:"/>
+        </properties>
+      </component>
+      <vspacer id="8a93e">
+        <constraints>
+          <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+        </constraints>
+      </vspacer>
+      <component id="9f4da" class="com.intellij.openapi.roots.ui.configuration.ModulesCombobox" binding="myModuleComboBox">
+        <constraints>
+          <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties/>
+      </component>
+      <component id="b23a6" class="com.intellij.ui.components.JBLabel">
+        <constraints>
+          <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Project ID:"/>
+        </properties>
+      </component>
+      <component id="c6f41" class="javax.swing.JTextField" binding="myProjectId">
+        <constraints>
+          <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <editable value="true"/>
+          <enabled value="true"/>
+          <horizontalAlignment value="2"/>
+          <text value=""/>
+        </properties>
+      </component>
+      <component id="1011a" class="com.intellij.ui.components.JBLabel">
+        <constraints>
+          <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Version:"/>
+        </properties>
+      </component>
+      <component id="331e4" class="javax.swing.JTextField" binding="myVersion">
+        <constraints>
+          <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <editable value="true"/>
+          <enabled value="true"/>
+          <horizontalAlignment value="2"/>
+        </properties>
+      </component>
+    </children>
+  </grid>
+</form>
diff --git a/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.java b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.java
new file mode 100644
index 0000000..bd8c2f1
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdateDialog.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2014 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.idea.appengine.deploy;
+
+import com.google.common.base.Strings;
+import com.google.gct.idea.appengine.dom.AppEngineWebApp;
+import com.google.gct.idea.appengine.dom.AppEngineWebFileDescription;
+import com.google.gct.idea.appengine.gradle.facet.AppEngineConfigurationProperties;
+import com.google.gct.idea.appengine.gradle.facet.AppEngineGradleFacet;
+import com.google.gct.login.GoogleLogin;
+import com.google.gct.login.IGoogleLoginCompletedCallback;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ui.configuration.ModulesCombobox;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.packaging.artifacts.Artifact;
+import com.intellij.packaging.artifacts.ArtifactManager;
+import com.intellij.packaging.elements.PackagingElementResolvingContext;
+import com.intellij.packaging.impl.artifacts.ArtifactUtil;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.ui.SortedComboBoxModel;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomFileElement;
+import com.intellij.util.xml.DomManager;
+import com.intellij.xml.util.XmlStringUtil;
+import org.eclipse.jgit.util.StringUtils;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * AppEngineUpdateDialog shows a dialog allowing the user to select a module and deploy.
+ */
+public class AppEngineUpdateDialog extends DialogWrapper {
+  private static final Logger LOG = Logger.getInstance(AppEngineUpdateDialog.class);
+
+  private ModulesCombobox myModuleComboBox;
+  private JTextField myProjectId;
+  private JTextField myVersion;
+  private JPanel myPanel;
+  private List<Module> myDeployableModules;
+  private Project myProject;
+  private Module myInitiallySelectedModule;
+
+  private AppEngineUpdateDialog(Project project, List<Module> deployableModules, Module selectedModule) {
+    super(project, true);
+    myDeployableModules = deployableModules;
+    myProject = project;
+    myInitiallySelectedModule = selectedModule;
+
+    init();
+    initValidation();
+    setTitle("Deploy to App Engine");
+    setOKButtonText("Deploy");
+
+    Window myWindow = getWindow();
+    if (myWindow != null) {
+      myWindow.setPreferredSize(new Dimension(285, 135));
+    }
+  }
+
+  /**
+   * Shows a dialog to deploy a module to AppEngine.  Will force a login if required
+   * If either the login fails or there are no valid modules to upload, it will return  after
+   * displaying an error.
+   *
+   * @param project The project whose modules will be uploaded.
+   * @param selectedModule The module selected by default in the deploy dialog.  Can be null.  If null or not a valid app engine module,
+   *                       no module will be selected by default.
+   */
+  static void show(final Project project, Module selectedModule) {
+
+    final java.util.List<Module> modules = new ArrayList<Module>();
+
+    // Filter the module list by whether we can actually deploy them to appengine.
+    for (Module module : ModuleManager.getInstance(project).getModules()) {
+      AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(module);
+      if (facet != null) {
+        modules.add(module);
+      }
+    }
+
+    // Tell the user what he has to do if he has none.
+    if (modules.size() == 0) {
+      //there are no modules to upload -- or we hit a bug due to gradle sync.
+      //TODO do we need to use the mainwindow as owner?
+      Messages.showErrorDialog(
+        XmlStringUtil.wrapInHtml(
+          "This project does not contain any App Engine modules. To add an App Engine module for your project, <br> open “File > New Module…” menu and choose one of App Engine modules.")
+        , "Error");
+      return;
+    }
+
+    if (selectedModule != null && !modules.contains(selectedModule)) {
+      selectedModule = null;
+    }
+
+    if (selectedModule == null && modules.size() == 1) {
+      selectedModule = modules.get(0);
+    }
+
+    // To invoke later, we need a final local.
+    final Module passedSelectedModule = selectedModule;
+
+    // Login on demand and queue up the dialog to show after a successful login.
+    //if login fails, it already shows an error.
+    if (!GoogleLogin.getInstance().isLoggedIn()) {
+      // log in on demand...
+      GoogleLogin.getInstance().logIn(null, new IGoogleLoginCompletedCallback() {
+        @Override
+        public void onLoginCompleted() {
+          if (GoogleLogin.getInstance().isLoggedIn()) {
+            EventQueue.invokeLater(new Runnable() {
+              @Override
+              public void run() {
+                // Success!, lets run the deploy now.
+                AppEngineUpdateDialog dialog = new AppEngineUpdateDialog(project, modules, passedSelectedModule);
+                dialog.show();
+              }
+            });
+          }
+        }
+      });
+    }
+    else {
+      AppEngineUpdateDialog dialog = new AppEngineUpdateDialog(project, modules, passedSelectedModule);
+      dialog.show();
+    }
+  }
+
+  @Nullable
+  @Override
+  protected JComponent createCenterPanel() {
+    @SuppressWarnings("unchecked")
+    final SortedComboBoxModel<Module> model = (SortedComboBoxModel<Module>)myModuleComboBox.getModel();
+    model.clear();
+    model.addAll(myDeployableModules);
+
+    if (myInitiallySelectedModule != null) {
+      // Auto select if there is only one item
+      model.setSelectedItem(myInitiallySelectedModule);
+      populateFields();
+    }
+
+    myModuleComboBox.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        populateFields();
+      }
+    });
+    return myPanel;
+  }
+
+  private void populateFields() {
+    myProjectId.setText("");
+    myVersion.setText("");
+
+    Module appEngineModule = myModuleComboBox.getSelectedModule();
+    if (appEngineModule != null) {
+      AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(appEngineModule);
+      if (facet == null) {
+        Messages.showErrorDialog(this.getPeer().getOwner(), "Could not acquire App Engine module information.", "Deploy");
+        return;
+      }
+
+      final AppEngineWebApp appEngineWebApp = facet.getAppEngineWebXml();
+      if (appEngineWebApp == null) {
+        Messages.showErrorDialog(this.getPeer().getOwner(), "Could not locate or parse the appengine-web.xml fle.", "Deploy");
+        return;
+      }
+
+      myProjectId.setText(appEngineWebApp.getApplication().getRawText());
+      myVersion.setText(appEngineWebApp.getVersion().getRawText());
+    }
+  }
+
+  @Override
+  protected void doOKAction() {
+    if (getOKAction().isEnabled()) {
+      GoogleLogin login = GoogleLogin.getInstance();
+      Module selectedModule = myModuleComboBox.getSelectedModule();
+      String sdk = "";
+      String war = "";
+      AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(selectedModule);
+      if (facet != null) {
+        AppEngineConfigurationProperties model = facet.getConfiguration().getState();
+        sdk = model.APPENGINE_SDKROOT;
+        war = model.WAR_DIR;
+      }
+
+      String client_secret = login.fetchOAuth2ClientSecret();
+      String client_id = login.fetchOAuth2ClientId();
+      String refresh_token = login.fetchOAuth2RefreshToken();
+
+      if (StringUtils.isEmptyOrNull(client_secret) ||
+          StringUtils.isEmptyOrNull(client_id) ||
+          StringUtils.isEmptyOrNull(refresh_token)) {
+        // The login is somehow invalid, bail -- this shouldn't happen.
+        LOG.error("StartUploading while logged in, but it doesn't have full credentials.");
+        Messages.showErrorDialog(this.getPeer().getOwner(), "Login credentials are not valid.", "Login");
+        return;
+      }
+
+      // These should not fail as they are a part of the dialog validation.
+      if (Strings.isNullOrEmpty(sdk) ||
+          Strings.isNullOrEmpty(war) ||
+          Strings.isNullOrEmpty(myProjectId.getText()) ||
+          selectedModule == null) {
+        Messages.showErrorDialog(this.getPeer().getOwner(), "Could not deploy due to missing information (sdk/war/projectid).", "Deploy");
+        LOG.error("StartUploading was called with bad module/sdk/war");
+        return;
+      }
+
+      close(OK_EXIT_CODE);  // We close before kicking off the update so it doesn't interfere with the output window coming to focus.
+
+      // Kick off the upload.  detailed status will be shown in an output window.
+      new AppEngineUpdater(myProject, selectedModule, sdk, war, myProjectId.getText(), myVersion.getText(),
+                           client_secret, client_id, refresh_token).startUploading();
+    }
+  }
+
+  @Override
+  protected ValidationInfo doValidate() {
+    // These should not normally occur..
+    if (!GoogleLogin.getInstance().isLoggedIn()) {
+      return new ValidationInfo("You must be logged in to perform this action.");
+    }
+
+    Module module = myModuleComboBox.getSelectedModule();
+    if (module == null) {
+      return new ValidationInfo("Select a module");
+    }
+
+    AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(module);
+    if (facet == null) {
+      return new ValidationInfo("Could not find App Engine gradle configuration on Module");
+    }
+
+    // We'll let AppCfg error if the project is wrong.  The user can see this in the console window.
+    // Note that version can be blank to indicate current version.
+    if (Strings.isNullOrEmpty(myProjectId.getText())) {
+      return new ValidationInfo("Please enter a Project ID.");
+    }
+
+    return null;
+  }
+
+}
diff --git a/src/com/google/gct/idea/appengine/deploy/AppEngineUpdater.java b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdater.java
new file mode 100644
index 0000000..aa48dd4
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/deploy/AppEngineUpdater.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2014 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.idea.appengine.deploy;
+
+import com.android.tools.idea.gradle.invoker.GradleInvoker;
+import com.google.common.base.Strings;
+import com.google.gct.idea.appengine.sdk.AppEngineSdk;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.ExecutionManager;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.CommandLineBuilder;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.configurations.JavaParameters;
+import com.intellij.execution.configurations.ParametersList;
+import com.intellij.execution.executors.DefaultRunExecutor;
+import com.intellij.execution.filters.TextConsoleBuilderFactory;
+import com.intellij.execution.process.*;
+import com.intellij.execution.ui.ConsoleView;
+import com.intellij.execution.ui.RunContentDescriptor;
+import com.intellij.execution.ui.RunnerLayoutUi;
+import com.intellij.execution.ui.actions.CloseAction;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.actionSystem.ActionPlaces;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.actionSystem.IdeActions;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.KeyValue;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.net.HttpConfigurable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.*;
+import java.util.List;
+
+/**
+ * Compiles and deploys a module to AppEngine using AppCfg.
+ *
+ * @author benwu
+ */
+class AppEngineUpdater {
+  private static final Logger LOG = Logger.getInstance("#com.google.gct.idea.appengine.deploy.AppEngineUpdater");
+  private final Project myProject;
+  private final Module myModule;
+  private final String myExplodedWarPath;
+  private final String mySdkPath;
+  private final String myClientSecret;
+  private final String myClientId;
+  private final String myRefreshToken;
+  private final String myVersion;
+  private final String myAppEngineProject;
+
+  AppEngineUpdater(Project project,
+                   Module module,
+                   String sdkPath,
+                   String explodedWarPath,
+                   String appEngineProject,
+                   String version,
+                   String clientSecret,
+                   String clientId,
+                   String refreshToken) {
+    myProject = project;
+    myModule = module;
+    mySdkPath = sdkPath;
+    myExplodedWarPath = explodedWarPath;
+    myClientSecret = clientSecret;
+    myClientId = clientId;
+    myRefreshToken = refreshToken;
+    myVersion = version;
+    myAppEngineProject = appEngineProject;
+  }
+
+  /**
+   * Starts the compile and upload async process.
+   */
+  void startUploading() {
+    FileDocumentManager.getInstance().saveAllDocuments();
+    ProgressManager.getInstance().run(new Task.Backgroundable(myModule.getProject(), "Deploying application", true, null) {
+      @Override
+      public void run(@NotNull ProgressIndicator indicator) {
+        compileAndUpload();
+      }
+    });
+  }
+
+  private void compileAndUpload() {
+    final Runnable startUploading = new Runnable() {
+      @Override
+      public void run() {
+        ApplicationManager.getApplication().invokeLater(new Runnable() {
+          @Override
+          public void run() {
+            startUploadingProcess();
+          }
+        });
+      }
+    };
+
+    GradleInvoker.getInstance(myProject).compileJava(new Module[]{myModule});
+    startUploading.run();
+  }
+
+  private void startUploadingProcess() {
+    final Process process;
+    final GeneralCommandLine commandLine;
+
+    try {
+      JavaParameters parameters = new JavaParameters();
+      parameters.configureByModule(myModule, JavaParameters.JDK_ONLY);
+      parameters.setMainClass("com.google.appengine.tools.admin.AppCfg");
+      AppEngineSdk mySdk = new AppEngineSdk(mySdkPath);
+      parameters.getClassPath().add(mySdk.getToolsApiJarFile().getAbsolutePath());
+
+      final List<KeyValue<String, String>> list = HttpConfigurable.getJvmPropertiesList(false, null);
+      if (!list.isEmpty()) {
+        final ParametersList parametersList = parameters.getVMParametersList();
+        for (KeyValue<String, String> value : list) {
+          parametersList.defineProperty(value.getKey(), value.getValue());
+        }
+      }
+
+      final ParametersList programParameters = parameters.getProgramParametersList();
+      programParameters.add("--application=" + myAppEngineProject);
+      if (!Strings.isNullOrEmpty(myVersion)) {
+        programParameters.add("--version=" + myVersion);
+      }
+      programParameters.add("--oauth2");
+      programParameters.add("--oauth2_client_secret=" + myClientSecret);
+      programParameters.add("--oauth2_client_id=" + myClientId);
+      programParameters.add("--oauth2_refresh_token=" + myRefreshToken);
+      programParameters.add("update");
+      programParameters.add(FileUtil.toSystemDependentName(myExplodedWarPath));
+
+      commandLine = CommandLineBuilder.createFromJavaParameters(parameters);
+
+      process = commandLine.createProcess();
+    }
+    catch (ExecutionException e) {
+      final String message = e.getMessage();
+      LOG.error("Cannot start uploading: " + message);
+
+      if (!EventQueue.isDispatchThread()) {
+        EventQueue.invokeLater(new Runnable() {
+          @Override
+          public void run() {
+            Messages.showErrorDialog("Cannot start uploading: " + message, "Error");
+          }
+        });
+      }
+      else {
+        Messages.showErrorDialog("Cannot start uploading: " + message, "Error");
+      }
+
+      return;
+    }
+
+    final ProcessHandler processHandler = new FilteredOSProcessHandler(process, commandLine.getCommandLineString(),
+                                                                       new String[]{myRefreshToken, myClientSecret, myClientId});
+    final Executor executor = DefaultRunExecutor.getRunExecutorInstance();
+    final ConsoleView console = TextConsoleBuilderFactory.getInstance().createBuilder(myModule.getProject()).getConsole();
+    final RunnerLayoutUi ui = RunnerLayoutUi.Factory.getInstance(myModule.getProject())
+      .create("Deploy", "Deploy to AppEngine", "Deploy Application", myModule.getProject());
+    final DefaultActionGroup group = new DefaultActionGroup();
+    ui.getOptions().setLeftToolbar(group, ActionPlaces.UNKNOWN);
+    ui.addContent(ui.createContent("upload", console.getComponent(), "Deploy Application", null, console.getPreferredFocusableComponent()));
+
+    console.attachToProcess(processHandler);
+    final RunContentDescriptor contentDescriptor =
+      new RunContentDescriptor(console, processHandler, ui.getComponent(), "Deploy to AppEngine");
+    group.add(ActionManager.getInstance().getAction(IdeActions.ACTION_STOP_PROGRAM));
+    group.add(new CloseAction(executor, contentDescriptor, myModule.getProject()));
+
+    ExecutionManager.getInstance(myModule.getProject()).getContentManager().showRunContent(executor, contentDescriptor);
+    processHandler.startNotify();
+  }
+
+  private class FilteredOSProcessHandler extends OSProcessHandler {
+    String[] tokensToFilter;
+
+    FilteredOSProcessHandler(@NotNull final Process process, @Nullable final String commandLine, String[] filteredTokens) {
+      super(process, commandLine);
+      tokensToFilter = filteredTokens;
+    }
+
+    @Override
+    public void notifyTextAvailable(final String text, final Key outputType) {
+      String newText = text;
+      if (tokensToFilter != null) {
+        for (String token : tokensToFilter) {
+          newText = newText.replace(token, "*****");
+        }
+      }
+      super.notifyTextAvailable(newText, outputType);
+    }
+  }
+}
diff --git a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java b/src/com/google/gct/idea/appengine/dom/AppEngineWebApp.java
similarity index 64%
copy from login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
copy to src/com/google/gct/idea/appengine/dom/AppEngineWebApp.java
index 771081b..ce3ba7d 100644
--- a/login/src/com/google/gct/login/IGoogleLoginUpdateUser.java
+++ b/src/com/google/gct/idea/appengine/dom/AppEngineWebApp.java
@@ -13,8 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.gct.login;
+package com.google.gct.idea.appengine.dom;
 
-public interface IGoogleLoginUpdateUser {
-  public void updateUser();
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.GenericDomValue;
+
+/**
+ * This is the Dom for the App Engine config file.
+ */
+public interface AppEngineWebApp extends DomElement {
+  GenericDomValue<String> getApplication();
+  GenericDomValue<String> getVersion();
 }
diff --git a/src/com/google/gct/idea/appengine/dom/AppEngineWebFileDescription.java b/src/com/google/gct/idea/appengine/dom/AppEngineWebFileDescription.java
new file mode 100644
index 0000000..9efe86f
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/dom/AppEngineWebFileDescription.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014 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.idea.appengine.dom;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.util.xml.DomFileDescription;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This is the file description for the App Engine xml config file
+ */
+public class AppEngineWebFileDescription extends DomFileDescription<AppEngineWebApp> {
+  @NonNls public static final String APP_ENGINE_WEB_XML_NAME = "appengine-web.xml";
+
+  public AppEngineWebFileDescription() {
+    super(AppEngineWebApp.class, "appengine-web-app");
+  }
+
+  @Override
+  public boolean isMyFile(@NotNull XmlFile file, @Nullable Module module) {
+    return file.getName().equals(APP_ENGINE_WEB_XML_NAME);
+  }
+}
diff --git a/src/com/google/gct/idea/appengine/gradle/action/GenerateEndpointAction.java b/src/com/google/gct/idea/appengine/gradle/action/GenerateEndpointAction.java
index f442f43..7e236b2 100644
--- a/src/com/google/gct/idea/appengine/gradle/action/GenerateEndpointAction.java
+++ b/src/com/google/gct/idea/appengine/gradle/action/GenerateEndpointAction.java
@@ -83,6 +83,7 @@
     try {
       if (!AppEngineUtils.isAppEngineModule(project, module)) {
         Messages.showErrorDialog(project, "Endpoints can only be generated for App Engine projects.", ERROR_MESSAGE_TITLE);
+        return;
       }
     }
     catch (FileNotFoundException error) {
diff --git a/src/com/google/gct/idea/appengine/gradle/facet/AppEngineConfigurationProperties.java b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineConfigurationProperties.java
new file mode 100644
index 0000000..9baee10
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineConfigurationProperties.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 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.idea.appengine.gradle.facet;
+
+import com.intellij.util.xmlb.annotations.AbstractCollection;
+import org.jetbrains.android.util.AndroidCommonUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Holds configuration property values for the AppEngine facet.
+ */
+public class AppEngineConfigurationProperties {
+  public String HTTP_ADDRESS;
+  public Integer HTTP_PORT;
+  @AbstractCollection(surroundWithTag = false, elementTag = "jvmflags", elementValueAttribute = "")
+  public List<String> JVM_FLAGS = new ArrayList<String>();
+  public String WAR_DIR;
+  public String WEB_APP_DIR;
+  public String APPENGINE_SDKROOT;
+}
diff --git a/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacet.java b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacet.java
index 08ef0cd..198f472 100644
--- a/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacet.java
+++ b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacet.java
@@ -15,7 +15,8 @@
  */
 package com.google.gct.idea.appengine.gradle.facet;
 
-import com.google.gct.idea.appengine.gradle.project.IdeaAppEngineProject;
+import com.google.common.base.Strings;
+import com.google.gct.idea.appengine.dom.AppEngineWebApp;
 import com.intellij.facet.Facet;
 import com.intellij.facet.FacetManager;
 import com.intellij.facet.FacetType;
@@ -23,10 +24,18 @@
 import com.intellij.facet.FacetTypeRegistry;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.util.xml.DomManager;
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.io.File;
+
 /**
  * App Engine Gradle facet for App Engine Modules with a Gradle build file
  */
@@ -38,8 +47,6 @@
 
   public static final FacetTypeId<AppEngineGradleFacet> TYPE_ID = new FacetTypeId<AppEngineGradleFacet>(ID);
 
-  private IdeaAppEngineProject myIdeaAppEngineProject;
-
   @Nullable
   public static AppEngineGradleFacet getInstance(@NotNull Module module) {
     return FacetManager.getInstance(module).getFacetByType(TYPE_ID);
@@ -53,6 +60,30 @@
     super(facetType, module, name, configuration, null);
   }
 
+  /**
+   * Returns an object holding information from the appengine-web.xml file.
+   */
+  public AppEngineWebApp getAppEngineWebXml() {
+    AppEngineConfigurationProperties model = getConfiguration().getState();
+    if (model == null || Strings.isNullOrEmpty(model.WEB_APP_DIR)) {
+      return null;
+    }
+
+    String path = model.WEB_APP_DIR + "/WEB-INF/appengine-web.xml";
+    VirtualFile appEngineFile = LocalFileSystem.getInstance().findFileByPath(path.replace(File.separatorChar, '/'));
+    if (appEngineFile == null) {
+      return null;
+    }
+
+    PsiFile psiFile = PsiManager.getInstance(getModule().getProject()).findFile(appEngineFile);
+    if (psiFile == null || !(psiFile instanceof XmlFile)) {
+      return null;
+    }
+
+    final DomManager domManager = DomManager.getDomManager(getModule().getProject());
+    return domManager.getFileElement((XmlFile)psiFile, AppEngineWebApp.class).getRootElement();
+  }
+
   public static FacetType<AppEngineGradleFacet, AppEngineGradleFacetConfiguration> getFacetType() {
     return FacetTypeRegistry.getInstance().findFacetType(ID);
   }
@@ -62,13 +93,4 @@
     if (module == null) return null;
     return FacetManager.getInstance(module).getFacetByType(TYPE_ID);
   }
-
-  public IdeaAppEngineProject getIdeaAppEngineProject() {
-    return myIdeaAppEngineProject;
-  }
-
-  public void setIdeaAppEngineProject(IdeaAppEngineProject ideaAppEngineProject) {
-    myIdeaAppEngineProject = ideaAppEngineProject;
-  }
-
 }
diff --git a/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacetConfiguration.java b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacetConfiguration.java
index c5466e5..817393d 100644
--- a/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacetConfiguration.java
+++ b/src/com/google/gct/idea/appengine/gradle/facet/AppEngineGradleFacetConfiguration.java
@@ -19,16 +19,19 @@
 import com.intellij.facet.ui.FacetEditorContext;
 import com.intellij.facet.ui.FacetEditorTab;
 import com.intellij.facet.ui.FacetValidatorsManager;
+import com.intellij.openapi.components.PersistentStateComponent;
 import com.intellij.openapi.util.InvalidDataException;
 import com.intellij.openapi.util.WriteExternalException;
-import com.intellij.util.xmlb.XmlSerializer;
 
 import org.jdom.Element;
+import org.jetbrains.annotations.Nullable;
 
 /**
- * Currently an empty configuration for App Engine Gradle configurations
+ * A configuration for App Engine Gradle Facets that is populated during gradle project import
  */
-public class AppEngineGradleFacetConfiguration implements FacetConfiguration {
+public class AppEngineGradleFacetConfiguration implements FacetConfiguration, PersistentStateComponent<AppEngineConfigurationProperties> {
+  AppEngineConfigurationProperties myProperties = new AppEngineConfigurationProperties();
+
   @Override
   public FacetEditorTab[] createEditorTabs(FacetEditorContext editorContext, FacetValidatorsManager validatorsManager) {
     return new FacetEditorTab[] {
@@ -38,12 +41,22 @@
 
   @Override
   public void readExternal(Element element) throws InvalidDataException {
-    XmlSerializer.deserializeInto(this, element);
+    //Deprecated abstract method, using persistent state component now
   }
 
   @Override
   public void writeExternal(Element element) throws WriteExternalException {
-    XmlSerializer.serializeInto(this, element);
+    //Deprecated abstract method, using persistent state component now
   }
 
+  @Nullable
+  @Override
+  public AppEngineConfigurationProperties getState() {
+    return myProperties;
+  }
+
+  @Override
+  public void loadState(AppEngineConfigurationProperties state) {
+    myProperties = state;
+  }
 }
diff --git a/src/com/google/gct/idea/appengine/gradle/project/IdeaAppEngineProject.java b/src/com/google/gct/idea/appengine/gradle/project/IdeaAppEngineProject.java
index 97a0270..b54e7e7 100644
--- a/src/com/google/gct/idea/appengine/gradle/project/IdeaAppEngineProject.java
+++ b/src/com/google/gct/idea/appengine/gradle/project/IdeaAppEngineProject.java
@@ -24,7 +24,7 @@
 import java.io.File;
 
 /**
- * Project wrapper for App Engine Gradle Projects
+ * Transient project wrapper for App Engine Gradle Projects during gradle imports
  */
 public class IdeaAppEngineProject {
   @NotNull private final String myModuleName;
diff --git a/src/com/google/gct/idea/appengine/gradle/service/AppEngineGradleProjectDataService.java b/src/com/google/gct/idea/appengine/gradle/service/AppEngineGradleProjectDataService.java
index 36b25de..94e2ce9 100644
--- a/src/com/google/gct/idea/appengine/gradle/service/AppEngineGradleProjectDataService.java
+++ b/src/com/google/gct/idea/appengine/gradle/service/AppEngineGradleProjectDataService.java
@@ -99,7 +99,18 @@
         model.commit();
       }
     }
-    facet.setIdeaAppEngineProject(ideaAppEngineProject);
+
+    //deserialize state from ideaAppEngineProject into facet config.
+    if (facet != null) {
+      facet.getConfiguration().getState().APPENGINE_SDKROOT = ideaAppEngineProject.getDelegate().getAppEngineSdkRoot();
+      facet.getConfiguration().getState().HTTP_ADDRESS = ideaAppEngineProject.getDelegate().getHttpAddress();
+      facet.getConfiguration().getState().HTTP_PORT = ideaAppEngineProject.getDelegate().getHttpPort();
+      for (String flag : ideaAppEngineProject.getDelegate().getJvmFlags()) {
+        facet.getConfiguration().getState().JVM_FLAGS.add(flag);
+      }
+      facet.getConfiguration().getState().WAR_DIR = ideaAppEngineProject.getDelegate().getWarDir().getAbsolutePath();
+      facet.getConfiguration().getState().WEB_APP_DIR = ideaAppEngineProject.getDelegate().getWebAppDir().getAbsolutePath();
+    }
   }
 
   @NotNull
diff --git a/src/com/google/gct/idea/appengine/initialization/CloudPluginRegistration.java b/src/com/google/gct/idea/appengine/initialization/CloudPluginRegistration.java
new file mode 100644
index 0000000..1f103af
--- /dev/null
+++ b/src/com/google/gct/idea/appengine/initialization/CloudPluginRegistration.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014 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.idea.appengine.initialization;
+
+import com.google.gct.idea.appengine.deploy.AppEngineUpdateAction;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.actionSystem.Anchor;
+import com.intellij.openapi.actionSystem.Constraints;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.components.ApplicationComponent;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Initializes the menus for deploy.
+ */
+public class CloudPluginRegistration implements ApplicationComponent {
+
+  // We are reusing the flag for login.
+  private final static String SHOW_DEPLOY = "show.google.login.button";
+
+  public CloudPluginRegistration() {
+  }
+
+  @Override
+  public void initComponent() {
+    if (Boolean.getBoolean(SHOW_DEPLOY)) {
+      ActionManager am = ActionManager.getInstance();
+
+      AppEngineUpdateAction action = new AppEngineUpdateAction();
+      action.getTemplatePresentation().setText("Deploy Module to App Engine...");
+
+      am.registerAction("GoogleCloudTools.AppEngineUpdate", action);
+      DefaultActionGroup buildMenu = (DefaultActionGroup)am.getAction("BuildMenu");
+
+      DefaultActionGroup appEngineUpdateGroup = new DefaultActionGroup();
+      appEngineUpdateGroup.addSeparator();
+      appEngineUpdateGroup.add(action);
+      buildMenu.add(appEngineUpdateGroup, new Constraints(Anchor.AFTER, "Compile"));
+    }
+  }
+
+  @Override
+  public void disposeComponent() {
+  }
+
+  @NotNull
+  @Override
+  public String getComponentName() {
+    return "CloudPluginRegistration";
+  }
+}
diff --git a/src/com/google/gct/idea/appengine/run/AppEngineRunConfiguration.java b/src/com/google/gct/idea/appengine/run/AppEngineRunConfiguration.java
index 763eb1c..62ecf53 100644
--- a/src/com/google/gct/idea/appengine/run/AppEngineRunConfiguration.java
+++ b/src/com/google/gct/idea/appengine/run/AppEngineRunConfiguration.java
@@ -15,6 +15,8 @@
  */
 package com.google.gct.idea.appengine.run;
 
+import com.google.gct.idea.appengine.gradle.facet.AppEngineConfigurationProperties;
+import com.google.gct.idea.appengine.gradle.facet.AppEngineGradleFacet;
 import com.google.gct.idea.appengine.sdk.AppEngineSdk;
 
 import com.intellij.execution.ExecutionException;
@@ -59,11 +61,13 @@
   private String myServerAddress = "";
   private String mySdkPath = "";
   private String myServerPort = "";
+  private Boolean mySyncWithGradle = false;
   private String myVmArgs = "";
   private String myWarPath = "";
 
   private static final String KEY_SERVER_ADDRESS = "serverAddress";
   private static final String KEY_SERVER_PORT = "serverPort";
+  private static final String KEY_SYNC = "sync";
   private static final String KEY_SDK_PATH = "sdkPath";
   private static final String KEY_VM_ARGS = "vmArgs";
   private static final String KEY_WAR_PATH = "warPath";
@@ -100,6 +104,14 @@
     this.myServerPort = serverPort;
   }
 
+  public Boolean getSyncWithGradle() {
+    return mySyncWithGradle;
+  }
+
+  public void setSyncWithGradle(Boolean syncWithGradle) {
+    mySyncWithGradle = syncWithGradle;
+  }
+
   public String getVmArgs() {
     return myVmArgs;
   }
@@ -108,6 +120,7 @@
     this.myVmArgs = vmArgs;
   }
 
+
   public AppEngineRunConfiguration(String name, Project project, ConfigurationFactory factory) {
     super(name, new JavaRunConfigurationModule(project, false), factory);
   }
@@ -120,7 +133,7 @@
     ArrayList<Module> res = new ArrayList<Module>();
     for (Module module : modules) {
       Facet[] facetList = FacetManager.getInstance(module).getAllFacets();
-      for(Facet f : facetList) {
+      for (Facet f : facetList) {
         if (f.getTypeId() == AppEngineGradleFacet.TYPE_ID) {
           res.add(module);
           break;
@@ -149,6 +162,23 @@
     return state;
   }
 
+  // Syncs a run configuration with information from build.gradle via the App Engine Gradle facet
+  protected void syncWithBuildFileViaFacet() {
+    Module module = getConfigurationModule().getModule();
+    if (module != null) {
+      AppEngineGradleFacet facet = AppEngineGradleFacet.getInstance(module);
+      if (facet != null) {
+        AppEngineConfigurationProperties model = facet.getConfiguration().getState();
+        if (model != null) {
+          myServerPort = model.HTTP_PORT.toString();
+          myServerAddress = model.HTTP_ADDRESS;
+          mySdkPath = model.APPENGINE_SDKROOT;
+          myWarPath = model.WAR_DIR;
+        }
+      }
+    }
+  }
+
   @Override
   public final void checkConfiguration() throws RuntimeConfigurationException {
     JavaRunConfigurationModule configurationModule = getConfigurationModule();
@@ -158,11 +188,21 @@
     if (module == null) {
       return;
     }
-    /* this will be useful in future when we enable the appengine gradle facet
     AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(module);
     if (facet == null) {
-      throw new RuntimeConfigurationError("No App-Engine Facet");
-    }*/
+      throw new RuntimeConfigurationError(
+        "App Engine Gradle configuration not detected on module, maybe you need to Sync Project with Gradle");
+    }
+
+    if (mySyncWithGradle &&
+        (facet.getConfiguration().getState() == null || StringUtil.isEmpty(facet.getConfiguration().getState().WEB_APP_DIR))) {
+      throw new RuntimeConfigurationError(
+        "App Engine Gradle configuration does not appear to be loaded, please Sync Project with Gradle files before running");
+    }
+
+    if (mySyncWithGradle) {
+      syncWithBuildFileViaFacet();
+    }
 
     if (mySdkPath == null || mySdkPath.trim().isEmpty() || !new AppEngineSdk(mySdkPath).canRunDevAppServer()) {
       throw new RuntimeConfigurationError("Invalid App-Engine SDK Path");
@@ -255,6 +295,7 @@
     myServerPort = StringUtil.notNullize(JDOMExternalizer.readString(element, KEY_SERVER_PORT));
     myVmArgs = StringUtil.notNullize(JDOMExternalizer.readString(element, KEY_VM_ARGS));
     myWarPath = StringUtil.notNullize(JDOMExternalizer.readString(element, KEY_WAR_PATH));
+    mySyncWithGradle = JDOMExternalizer.readBoolean(element, KEY_SYNC);
   }
 
   @Override
@@ -266,6 +307,7 @@
     JDOMExternalizer.write(element, KEY_SERVER_PORT, myServerPort);
     JDOMExternalizer.write(element, KEY_VM_ARGS, myVmArgs);
     JDOMExternalizer.write(element, KEY_WAR_PATH, myWarPath);
+    JDOMExternalizer.write(element, KEY_SYNC, mySyncWithGradle);
     PathMacroManager.getInstance(getProject()).collapsePathsRecursively(element);
   }
 }
diff --git a/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.form b/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.form
index aef286c..02fe9d7 100644
--- a/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.form
+++ b/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.form
@@ -1,16 +1,16 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.google.gct.idea.appengine.run.AppEngineRunConfigurationSettingsEditor">
-  <grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="6" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+  <grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="8" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
     <margin top="0" left="0" bottom="0" right="0"/>
     <constraints>
-      <xy x="17" y="25" width="500" height="169"/>
+      <xy x="17" y="25" width="500" height="285"/>
     </constraints>
     <properties/>
     <border type="none"/>
     <children>
       <component id="78f2" class="javax.swing.JLabel">
         <constraints>
-          <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+          <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
         </constraints>
         <properties>
           <text value="War Path"/>
@@ -18,7 +18,7 @@
       </component>
       <component id="80c76" class="javax.swing.JLabel">
         <constraints>
-          <grid row="5" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+          <grid row="6" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
         </constraints>
         <properties>
           <text value="Server Port"/>
@@ -26,7 +26,7 @@
       </component>
       <component id="84c27" class="javax.swing.JTextField" binding="myServerPortField">
         <constraints>
-          <grid row="5" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+          <grid row="6" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
             <preferred-size width="150" height="-1"/>
           </grid>
         </constraints>
@@ -36,29 +36,15 @@
       </component>
       <component id="9770e" class="javax.swing.JLabel">
         <constraints>
-          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+          <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
         </constraints>
         <properties>
           <text value="App Engine SDK"/>
         </properties>
       </component>
-      <component id="2305e" class="javax.swing.JLabel">
-        <constraints>
-          <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
-        </constraints>
-        <properties>
-          <text value="Module"/>
-        </properties>
-      </component>
-      <component id="f369c" class="javax.swing.JComboBox" binding="myModuleComboBox">
-        <constraints>
-          <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
-        </constraints>
-        <properties/>
-      </component>
       <component id="4f254" class="javax.swing.JLabel">
         <constraints>
-          <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+          <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
         </constraints>
         <properties>
           <text value="VM Args"/>
@@ -66,7 +52,7 @@
       </component>
       <component id="2344d" class="javax.swing.JTextField" binding="myVmArgsField">
         <constraints>
-          <grid row="3" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+          <grid row="4" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
             <preferred-size width="150" height="-1"/>
           </grid>
         </constraints>
@@ -74,19 +60,19 @@
       </component>
       <component id="786dc" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myAppEngineSdkField">
         <constraints>
-          <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+          <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
         </constraints>
         <properties/>
       </component>
       <component id="b37fb" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myWarPathField">
         <constraints>
-          <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+          <grid row="3" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
         </constraints>
         <properties/>
       </component>
       <component id="fa285" class="javax.swing.JTextField" binding="myServerAddressField">
         <constraints>
-          <grid row="4" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+          <grid row="5" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
             <preferred-size width="150" height="-1"/>
           </grid>
         </constraints>
@@ -96,12 +82,39 @@
       </component>
       <component id="d19b0" class="javax.swing.JLabel">
         <constraints>
-          <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+          <grid row="5" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
         </constraints>
         <properties>
           <text value="Server Address"/>
         </properties>
       </component>
+      <component id="2305e" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Module"/>
+        </properties>
+      </component>
+      <vspacer id="5f8a6">
+        <constraints>
+          <grid row="7" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+        </constraints>
+      </vspacer>
+      <component id="f369c" class="javax.swing.JComboBox" binding="myModuleComboBox">
+        <constraints>
+          <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties/>
+      </component>
+      <component id="3e43f" class="javax.swing.JCheckBox" binding="mySynchronizeWithBuildGradleCheckBox" default-binding="true">
+        <constraints>
+          <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Synchronize with build.gradle configuration"/>
+        </properties>
+      </component>
     </children>
   </grid>
 </form>
diff --git a/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.java b/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.java
index b77872b..160c604 100644
--- a/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.java
+++ b/src/com/google/gct/idea/appengine/run/AppEngineRunConfigurationSettingsEditor.java
@@ -15,7 +15,7 @@
  */
 package com.google.gct.idea.appengine.run;
 
-import com.google.appengine.gradle.model.AppEngineModel;
+import com.google.gct.idea.appengine.gradle.facet.AppEngineConfigurationProperties;
 import com.google.gct.idea.appengine.gradle.facet.AppEngineGradleFacet;
 import com.intellij.execution.ui.ConfigurationModuleSelector;
 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
@@ -25,11 +25,13 @@
 import com.intellij.openapi.ui.TextFieldWithBrowseButton;
 import org.jetbrains.annotations.NotNull;
 
-import javax.swing.JButton;
+import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JPanel;
 import javax.swing.JTextField;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 
 /** GUI for configuring App Engine Run Configurations */
 public class AppEngineRunConfigurationSettingsEditor extends SettingsEditor<AppEngineRunConfiguration> {
@@ -40,28 +42,57 @@
   private TextFieldWithBrowseButton myWarPathField;
   private TextFieldWithBrowseButton myAppEngineSdkField;
   private JTextField myServerAddressField;
+  private JCheckBox mySynchronizeWithBuildGradleCheckBox;
   private final Project myProject;
   private final ConfigurationModuleSelector moduleSelector;
 
   public AppEngineRunConfigurationSettingsEditor(Project project) {
-    this.myProject = project;
+    myProject = project;
     moduleSelector = new ConfigurationModuleSelector(project, myModuleComboBox);
+    myModuleComboBox.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        updateSync();
+      }
+    });
+    mySynchronizeWithBuildGradleCheckBox.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        updateSync();
+      }
+    });
     myAppEngineSdkField.addBrowseFolderListener("Select App Engine Sdk Root", null, null,
                                                 FileChooserDescriptorFactory.createSingleFolderDescriptor());
     myWarPathField.addBrowseFolderListener("Select Exploded War Root", null, myProject,
                                            FileChooserDescriptorFactory.createSingleFolderDescriptor());
   }
 
-  // TODO: unused currently, but useful once gradle facet for App Engine is completely set up.
+  protected void updateSync() {
+    boolean isSync = mySynchronizeWithBuildGradleCheckBox.isSelected();
+    if (isSync) {
+      syncWithBuildFile();
+    }
+    myWarPathField.setEditable(!isSync);
+    myServerAddressField.setEditable(!isSync);
+    myServerPortField.setEditable(!isSync);
+    myAppEngineSdkField.setEditable(!isSync);
+  }
+
+  // Syncs a run configuration with information from build.gradle via the App Engine Gradle facet
+  // This is also a near-duplicate of the sync in AppEngineRunConfiguration, but this is required
+  // here to update the UI correctly when "sync" is checked and turned on, if we didn't reflect the
+  // configuration in the UI, we wouldn't need this.
   protected void syncWithBuildFile() {
+
     AppEngineGradleFacet facet = AppEngineGradleFacet.getInstance(moduleSelector.getModule());
-    if(facet != null) {
-      // proof of concept of usefulness of Gradle model
-      AppEngineModel model = facet.getIdeaAppEngineProject().getDelegate();
-      myServerPortField.setText(model.getHttpPort().toString());
-      myServerAddressField.setText(model.getHttpAddress());
-      myAppEngineSdkField.setText(model.getAppEngineSdkRoot());
-      myWarPathField.setText(model.getWarDir().getAbsolutePath());
+    if (facet != null) {
+      AppEngineConfigurationProperties model = facet.getConfiguration().getState();
+      if (model != null) {
+        myServerPortField.setText(model.HTTP_PORT.toString());
+        myServerAddressField.setText(model.HTTP_ADDRESS);
+        myAppEngineSdkField.setText(model.APPENGINE_SDKROOT);
+        myWarPathField.setText(model.WAR_DIR);
+      }
     }
   }
 
@@ -77,6 +108,8 @@
     myServerAddressField.setText(configuration.getServerAddress());
     myVmArgsField.setText(configuration.getVmArgs());
     moduleSelector.reset(configuration);
+    mySynchronizeWithBuildGradleCheckBox.setSelected(configuration.getSyncWithGradle());
+    updateSync();
   }
 
   @Override
@@ -87,6 +120,7 @@
     configuration.setServerPort(myServerPortField.getText());
     configuration.setVmArgs(myVmArgsField.getText());
     configuration.setWarPath(myWarPathField.getText());
+    configuration.setSyncWithGradle(mySynchronizeWithBuildGradleCheckBox.isSelected());
   }
 
   @NotNull
diff --git a/src/com/google/gct/idea/appengine/wizard/NewAppEngineModuleAction.java b/src/com/google/gct/idea/appengine/wizard/NewAppEngineModuleAction.java
index 0d2874d..d93e6ea 100644
--- a/src/com/google/gct/idea/appengine/wizard/NewAppEngineModuleAction.java
+++ b/src/com/google/gct/idea/appengine/wizard/NewAppEngineModuleAction.java
@@ -116,7 +116,6 @@
     }
 
     ApplicationManager.getApplication().runWriteAction(new Runnable() {
-
       @Override
       public void run() {
         List<File> allFilesToOpen = new ArrayList<File>();
@@ -131,23 +130,15 @@
         TemplateUtils.openEditors(project, allFilesToOpen, true);
 
         GradleProjectImporter projectImporter = GradleProjectImporter.getInstance();
-        projectImporter.requestProjectSync(project, new GradleSyncListener() {
-          @Override
-          public void syncStarted(@NotNull Project project) {
-          }
+        projectImporter.requestProjectSync(project, new GradleSyncListener.Adapter() {
 
           @Override
-          public void syncEnded(@NotNull final Project project) {
+          public void syncSucceeded(@NotNull final Project project) {
             ApplicationManager.getApplication().runWriteAction(new Runnable() {
               @Override
               public void run() {
                 Module module = ModuleManager.getInstance(project).findModuleByName(moduleName);
-
-                Parameter appEngineVersionParam = template.getMetadata().getParameter("appEngineVersion");
-                String appEngineVersion = (appEngineVersionParam == null) ? "unknown" : appEngineVersionParam.initial;
-
-                createRunConfiguration(project, module, moduleRoot, appEngineVersion);
-                addAppEngineGradleFacet();
+                createRunConfiguration(project, module);
               }
             });
           }
@@ -194,7 +185,7 @@
       String backendModulePath = (String) replacementMap.get(TemplateMetadata.ATTR_PROJECT_OUT);
       replacementMap.put(ATTR_SERVER_MODULE_PATH, FileUtil.getRelativePath(targetFolder.getPath(), backendModulePath, '/'));
       replacementMap.put(TemplateMetadata.ATTR_PROJECT_OUT, targetFolder.getPath());
-      List<SourceProvider> sourceProviders = IdeaSourceProvider.getSourceProvidersForFile(facet, targetFolder, facet.getMainSourceSet());
+      List<SourceProvider> sourceProviders = IdeaSourceProvider.getSourceProvidersForFile(facet, targetFolder, facet.getMainSourceProvider());
       SourceProvider sourceProvider = sourceProviders.get(0);
       File manifestDirectory = NewTemplateObjectWizard.findManifestDirectory(sourceProvider);
       replacementMap.put(TemplateMetadata.ATTR_MANIFEST_DIR, manifestDirectory.getPath());
@@ -217,7 +208,7 @@
     return null;
   }
 
-  private static void createRunConfiguration(Project project, Module module, File moduleRoot, String appEngineVersion) {
+  private static void createRunConfiguration(Project project, Module module) {
     // Create a run configuration for this module
     final RunManagerEx runManager = RunManagerEx.getInstanceEx(project);
     final RunnerAndConfigurationSettings settings = runManager.
@@ -225,30 +216,9 @@
     settings.setSingleton(true);
     final AppEngineRunConfiguration configuration = (AppEngineRunConfiguration)settings.getConfiguration();
     configuration.setModule(module);
-    configuration.setWarPath(new File(moduleRoot, "build/exploded-app").getAbsolutePath());
-    String gradleHomePath = GradleSettings.getInstance(project).getServiceDirectoryPath();
-    if (StringUtil.isEmpty(gradleHomePath)) {
-      gradleHomePath = new File(System.getProperty("user.home"), ".gradle").getAbsolutePath();
-    }
-    // This is a little strange because the sdk is "downloaded", but in our templates that's where the sdk is
-    // TODO, perhaps extract this from the build.gradle
-    // TODO, add support for the appengine environment/system properties (probably in the runconfig not here)
-    configuration.setSdkPath(new File(gradleHomePath, "/appengine-sdk/appengine-java-sdk-" + appEngineVersion).getAbsolutePath());
-    configuration.setServerPort("8080");
+    // pull configuration out of gradle
+    configuration.setSyncWithGradle(true);
     runManager.addConfiguration(settings, false);
   }
-
-  private static void addAppEngineGradleFacet() {
-    // Module does not have AppEngine-Gradle facet. Create one and add it.
-    // Commented out for now, ENABLE when AppEngine Gradle facet is ready.
-    // FacetManager facetManager = FacetManager.getInstance(module);
-    // ModifiableFacetModel model = facetManager.createModifiableModel();
-    //try {
-    //  Facet facet = facetManager.createFacet(AppEngineGradleFacet.getFacetType(), AppEngineGradleFacet.NAME, null);
-    //  model.addFacet(facet);
-    //} finally {
-    //  model.commit();
-    //}
-  }
 }
 
diff --git a/src/com/google/gct/idea/settings/ExportSettings.java b/src/com/google/gct/idea/settings/ExportSettings.java
new file mode 100644
index 0000000..6228f25
--- /dev/null
+++ b/src/com/google/gct/idea/settings/ExportSettings.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2014 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.idea.settings;
+
+import com.intellij.ide.IdeBundle;
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.ide.plugins.PluginManagerCore;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.components.ExportableApplicationComponent;
+import com.intellij.openapi.components.ExportableComponent;
+import com.intellij.openapi.components.ServiceBean;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.io.ZipUtil;
+
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.*;
+import java.util.jar.JarOutputStream;
+
+/**
+ * Provides functionality to export the IDEA settings into a specified jar
+ */
+public class ExportSettings {
+  // TODO: use ImportSettings.SETTINGS_JAR_MARKER after merge
+  public static final String SETTINGS_JAR_MARKER = "IntelliJ IDEA Global Settings";
+
+  public static void doExport(String path) {
+    final Set<ExportableComponent> exportableComponents =
+      new HashSet<ExportableComponent>(Arrays.asList(ApplicationManager.getApplication().getComponents(ExportableApplicationComponent.class)));
+    exportableComponents.addAll(ServiceBean.loadServicesFromBeans(ExportableComponent.EXTENSION_POINT, ExportableComponent.class));
+
+    if (exportableComponents.isEmpty()) {
+      return;
+    }
+
+    Set<File> exportFiles = new HashSet<File>();
+    for (final ExportableComponent markedComponent : exportableComponents) {
+      ContainerUtil.addAll(exportFiles, markedComponent.getExportFiles());
+    }
+
+    ApplicationManager.getApplication().saveSettings();
+
+    final File saveFile = new File(path);
+    try {
+      if (saveFile.exists()) {
+        final int ret = Messages
+          .showOkCancelDialog(IdeBundle.message("prompt.overwrite.settings.file", FileUtil.toSystemDependentName(saveFile.getPath())),
+                              IdeBundle.message("title.file.already.exists"), Messages.getWarningIcon());
+        if (ret != Messages.OK) return;
+      }
+
+      final JarOutputStream output = new JarOutputStream(new FileOutputStream(saveFile));
+      try {
+        final File configPath = new File(PathManager.getConfigPath());
+        final HashSet<String> writtenItemRelativePaths = new HashSet<String>();
+        for (File file : exportFiles) {
+          final String rPath = FileUtil.getRelativePath(configPath, file);
+          assert rPath != null;
+          final String relativePath = FileUtil.toSystemIndependentName(rPath);
+          if (file.exists()) {
+            ZipUtil.addFileOrDirRecursively(output, saveFile, file, relativePath, null, writtenItemRelativePaths);
+          }
+        }
+
+        exportInstalledPlugins(saveFile, output, writtenItemRelativePaths);
+
+        final File magicFile = new File(FileUtil.getTempDirectory(), SETTINGS_JAR_MARKER);
+        FileUtil.createIfDoesntExist(magicFile);
+        magicFile.deleteOnExit();
+        ZipUtil.addFileToZip(output, magicFile, SETTINGS_JAR_MARKER, writtenItemRelativePaths, null);
+      }
+      finally {
+        output.close();
+      }
+    }
+    catch (IOException e1) {
+      Messages.showErrorDialog(IdeBundle.message("error.writing.settings", e1.toString()),IdeBundle.message("title.error.writing.file"));
+    }
+
+  }
+
+  private static void exportInstalledPlugins(File saveFile, JarOutputStream output, HashSet<String> writtenItemRelativePaths) throws IOException {
+    final List<String> oldPlugins = new ArrayList<String>();
+    for (IdeaPluginDescriptor descriptor : PluginManagerCore.getPlugins()) {
+      if (!descriptor.isBundled() && descriptor.isEnabled()) {
+        oldPlugins.add(descriptor.getPluginId().getIdString());
+      }
+    }
+    if (!oldPlugins.isEmpty()) {
+      final File tempFile = File.createTempFile("installed", "plugins");
+      tempFile.deleteOnExit();
+      PluginManagerCore.savePluginsList(oldPlugins, false, tempFile);
+      ZipUtil.addDirToZipRecursively(output, saveFile, tempFile, "/" + PluginManager.INSTALLED_TXT, null, writtenItemRelativePaths);
+    }
+  }
+}
diff --git a/src/com/google/gct/idea/settings/ImportSettings.java b/src/com/google/gct/idea/settings/ImportSettings.java
new file mode 100644
index 0000000..a509fc6
--- /dev/null
+++ b/src/com/google/gct/idea/settings/ImportSettings.java
@@ -0,0 +1,137 @@
+package com.google.gct.idea.settings;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.intellij.ide.IdeBundle;
+import com.intellij.ide.actions.ImportSettingsFilenameFilter;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.ide.startup.StartupActionScriptManager;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ApplicationNamesInfo;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.application.ex.ApplicationEx;
+import com.intellij.openapi.components.ExportableApplicationComponent;
+import com.intellij.openapi.components.ExportableComponent;
+import com.intellij.openapi.components.ServiceBean;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.updateSettings.impl.UpdateSettings;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.io.ZipUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+/**
+ * Provides functionality to update IDEA setting from a jar containing new settings
+ */
+public class ImportSettings {
+  private static final String DIALOG_TITLE = "Setting Synchronization";
+  public static final String SETTINGS_JAR_MARKER = "IntelliJ IDEA Global Settings";
+
+  /**
+   * Parse and update the IDEA settings in the jar at <code>path</code>.
+   * Note: This function might require a restart of the application.
+   * @param path The location of the jar with the new IDEA settings.
+   */
+  public static void doImport(String path) {
+    final File saveFile = new File(path);
+    try {
+      if (!saveFile.exists()) {
+        Messages.showErrorDialog(IdeBundle.message("error.cannot.find.file", presentableFileName(saveFile)), DIALOG_TITLE);
+        return;
+      }
+
+      // What is this file used for?
+      final ZipEntry magicEntry = new ZipFile(saveFile).getEntry(SETTINGS_JAR_MARKER);
+      if (magicEntry == null) {
+        Messages.showErrorDialog("The file " + presentableFileName(saveFile) + " contains no settings to import",
+          DIALOG_TITLE);
+        return;
+      }
+
+      final ArrayList<ExportableComponent> registeredComponents = new ArrayList<ExportableComponent>(
+        Arrays.asList(ApplicationManager.getApplication().getComponents(ExportableApplicationComponent.class)));
+      registeredComponents.addAll(ServiceBean.loadServicesFromBeans(ExportableComponent.EXTENSION_POINT, ExportableComponent.class));
+
+      List<ExportableComponent> storedComponents = getComponentsStored(saveFile, registeredComponents);
+
+      Set<String> relativeNamesToExtract = new HashSet<String>();
+      for (final ExportableComponent aComponent : storedComponents) {
+        final File[] exportFiles = aComponent.getExportFiles();
+        for (File exportFile : exportFiles) {
+          final File configPath = new File(PathManager.getConfigPath());
+          final String rPath = FileUtil.getRelativePath(configPath, exportFile);
+          assert rPath != null;
+          final String relativePath = FileUtil.toSystemIndependentName(rPath);
+          relativeNamesToExtract.add(relativePath);
+        }
+      }
+
+      relativeNamesToExtract.add(PluginManager.INSTALLED_TXT);
+
+      final File tempFile = new File(PathManager.getPluginTempPath() + "/" + saveFile.getName());
+      FileUtil.copy(saveFile, tempFile);
+      File outDir = new File(PathManager.getConfigPath());
+      final ImportSettingsFilenameFilter filenameFilter = new ImportSettingsFilenameFilter(relativeNamesToExtract);
+      StartupActionScriptManager.ActionCommand unzip = new StartupActionScriptManager.UnzipCommand(tempFile, outDir, filenameFilter);
+      StartupActionScriptManager.addActionCommand(unzip);
+
+      // remove temp file
+      StartupActionScriptManager.ActionCommand deleteTemp = new StartupActionScriptManager.DeleteCommand(tempFile);
+      StartupActionScriptManager.addActionCommand(deleteTemp);
+
+      UpdateSettings.getInstance().forceCheckForUpdateAfterRestart();
+
+      String key = ApplicationManager.getApplication().isRestartCapable()
+                   ? "message.settings.imported.successfully.restart"
+                   : "message.settings.imported.successfully";
+      final int ret = Messages.showOkCancelDialog(IdeBundle.message(key,
+                                                                    ApplicationNamesInfo.getInstance().getProductName(),
+                                                                    ApplicationNamesInfo.getInstance().getFullProductName()),
+                                                  IdeBundle.message("title.restart.needed"), Messages.getQuestionIcon());
+      if (ret == Messages.OK) {
+        ((ApplicationEx)ApplicationManager.getApplication()).restart(true);
+      }
+    }
+    catch (ZipException e1) {
+      Messages.showErrorDialog(
+        "Error reading file " + presentableFileName(saveFile) + ".\\nThere was " + e1.getMessage(),
+        DIALOG_TITLE);
+    }
+    catch (IOException e1) {
+      Messages.showErrorDialog(IdeBundle.message("error.reading.settings.file.2", presentableFileName(saveFile), e1.getMessage()),
+                               DIALOG_TITLE);
+    }
+  }
+
+  private static String presentableFileName(final File file) {
+    return "'" + FileUtil.toSystemDependentName(file.getPath()) + "'";
+  }
+
+  private static List<ExportableComponent> getComponentsStored(File zipFile,
+    ArrayList<ExportableComponent> registeredComponents)
+    throws IOException {
+    final File configPath = new File(PathManager.getConfigPath());
+
+    final ArrayList<ExportableComponent> components = new ArrayList<ExportableComponent>();
+    for (ExportableComponent component : registeredComponents) {
+      final File[] exportFiles = component.getExportFiles();
+      for (File exportFile : exportFiles) {
+        final String rPath = FileUtil.getRelativePath(configPath, exportFile);
+        assert rPath != null;
+        String relativePath = FileUtil.toSystemIndependentName(rPath);
+        if (exportFile.isDirectory()) relativePath += "/";
+        if (ZipUtil.isZipContainsEntry(zipFile, relativePath)) {
+          components.add(component);
+          break;
+        }
+      }
+    }
+    return components;
+  }
+}