MobileWebApp sample. Showcases GWT providing a single app providing specialized views for Desktop, Mobile and Tablet devices. Uses App Engine as a backend.
Requires Google App Engine SDK 1.4.2 or later.

Review at http://gwt-code-reviews.appspot.com/1427803


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10041 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/build.xml b/samples/build.xml
index b5061e1..f735d99 100644
--- a/samples/build.xml
+++ b/samples/build.xml
@@ -34,6 +34,10 @@
     <gwt.ant dir="mail" />
   </target>
 
+  <target name="mobilewebapp" description="Build mobile web app">
+    <gwt.ant dir="mobilewebapp" target="source" />
+  </target>
+
   <target name="showcase" description="Build showcase">
     <gwt.ant dir="showcase" />
   </target>
@@ -54,6 +58,7 @@
     <antcall target="json" />
     <antcall target="logexample" />
     <antcall target="mail" />
+    <antcall target="mobilewebapp" />
     <antcall target="showcase" />
     <antcall target="validation" />
     <!-- don't include validationtck, it is not really a sample -->
diff --git a/samples/common.ant.xml b/samples/common.ant.xml
index 4d994e3..d68aac9 100755
--- a/samples/common.ant.xml
+++ b/samples/common.ant.xml
@@ -55,6 +55,12 @@
       </then>
     </if>
     <if>
+      <available file="README.txt"/>
+      <then>
+        <copy tofile="${sample.build}/README.txt" file="README.txt"/>
+      </then>
+    </if>
+    <if>
       <available file="user-build.xml"/>
       <then>
         <copy tofile="${sample.build}/build.xml" file="user-build.xml"/>
diff --git a/samples/mobilewebapp/README.txt b/samples/mobilewebapp/README.txt
new file mode 100644
index 0000000..23ca6b4
--- /dev/null
+++ b/samples/mobilewebapp/README.txt
@@ -0,0 +1,28 @@
+-- Preparation --
+
+Use Ant to build this project (http://ant.apache.org/). Ant uses the
+'build.xml' file in this folder, which describes exactly how to build
+your project.  This file has been tested to work against Ant 1.7.1.
+The following assumes 'ant' is on your command line path.
+
+This project uses Google App Engine
+(https://appengine.google.com/). You can download the Google App
+Engine SDK from http://code.google.com/appengine/. The Ant build.xml
+script needs to know where your Google App Engine SDK is
+installed. Create a file named local.properties in the same folder as
+the build.xml file. It needs to contain a definition for the
+appengine.sdk property. For example, if the Google App Engine SDK is
+installed at /opt/appengine-sdk, a line in the local.properties file
+should contain:
+
+appengine.sdk=/opt/appengine-sdk
+
+-- Build from the command line with Ant --
+
+To run development mode, just type 'ant devmode'.
+
+To compile your project for deployment, just type 'ant'.
+
+To compile and also bundle into a .war file, type 'ant war'.
+
+For a full listing of other targets, type 'ant -p'.
diff --git a/samples/mobilewebapp/build.xml b/samples/mobilewebapp/build.xml
new file mode 100755
index 0000000..7cf1f95
--- /dev/null
+++ b/samples/mobilewebapp/build.xml
@@ -0,0 +1,14 @@
+<project name="mobilewebapp" default="build" basedir=".">
+  <property name="sample.root" value="mobilewebapp" />
+  <property name="sample.module" value="MobileWebApp" />
+  <property name="sample.path" value="src/main/java" />
+  <import file="../common.ant.xml" />
+  <!-- these are after the common.ant.xml so they have gwt.tools... -->
+  <path id="sample.extraclasspath">
+    <pathelement location="${gwt.tools.lib}/javax/validation/validation-api-1.0.0.GA.jar" />
+    <pathelement location="${gwt.tools.lib}/javax/validation/validation-api-1.0.0.GA-sources.jar" />
+  </path>
+  <fileset id="sample.server.libs" dir="${gwt.tools.lib}">
+    <include name="javax/validation/validation-api-1.0.0.GA-sources.jar" />
+  </fileset>
+</project>
diff --git a/samples/mobilewebapp/src/META-INF/jdoconfig.xml b/samples/mobilewebapp/src/META-INF/jdoconfig.xml
new file mode 100644
index 0000000..5f56aa1
--- /dev/null
+++ b/samples/mobilewebapp/src/META-INF/jdoconfig.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
+   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+   xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">
+
+   <persistence-manager-factory name="transactions-optional">
+       <property name="javax.jdo.PersistenceManagerFactoryClass"
+           value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
+       <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
+       <property name="javax.jdo.option.NontransactionalRead" value="true"/>
+       <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
+       <property name="javax.jdo.option.RetainValues" value="true"/>
+       <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
+   </persistence-manager-factory>
+</jdoconfig>
diff --git a/samples/mobilewebapp/src/META-INF/persistence.xml b/samples/mobilewebapp/src/META-INF/persistence.xml
new file mode 100644
index 0000000..f6e4224
--- /dev/null
+++ b/samples/mobilewebapp/src/META-INF/persistence.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0"
+	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
+	<persistence-unit name="transactions-optional">
+		<provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
+		<properties>
+			<property name="datanucleus.NontransactionalRead" value="true" />
+			<property name="datanucleus.NontransactionalWrite" value="true" />
+			<property name="datanucleus.ConnectionURL" value="appengine" />
+		</properties>
+	</persistence-unit>
+</persistence>           
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/FormFactor.gwt.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/FormFactor.gwt.xml
new file mode 100644
index 0000000..19d3ef4
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/FormFactor.gwt.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- Defines the formfactor property and its provider function. -->
+<module>
+
+  <!-- Determine if we are in a mobile browser. -->
+  <define-property name="formfactor" values="desktop,tablet,mobile"/>
+
+  <property-provider name="formfactor">
+  <![CDATA[
+      // Look for the formfactor as a url argument.
+      var args = location.search;
+      var start = args.indexOf("formfactor");
+      if (start >= 0) {
+        var value = args.substring(start);
+        var begin = value.indexOf("=") + 1;
+        var end = value.indexOf("&");
+        if (end == -1) {
+          end = value.length;
+        }
+        return value.substring(begin, end);
+      }
+
+      // Detect form factor from user agent.
+      var ua = navigator.userAgent.toLowerCase();
+      if (ua.indexOf("iphone") != -1 || ua.indexOf("ipod") != -1) {
+        // iphone and ipod.
+        return "mobile";
+      } else if (ua.indexOf("ipad") != -1) {
+        // ipad.
+        return "tablet";
+      } else if (ua.indexOf("android") != -1 || ua.indexOf("mobile") != -1) {
+        /*
+         * Android - determine the form factor of android devices based on the diagonal screen
+         * size. Anything under six inches is a phone, anything over six inches is a tablet.
+         */
+        var dpi = 160;
+        var width = $wnd.screen.width / dpi;
+        var height = $wnd.screen.height / dpi;
+        var size = Math.sqrt(width*width + height*height);
+        return (size < 6) ? "mobile" : "tablet";
+      }
+      
+      // Everything else is a desktop.
+      return "desktop";
+  ]]>
+  </property-provider>
+
+</module>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
new file mode 100644
index 0000000..033d73b
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module rename-to='mobilewebapp'>
+  <!-- Inherit the core Web Toolkit stuff.                        -->
+  <inherits name='com.google.gwt.user.User'/>
+  <inherits name='com.google.gwt.sample.mobilewebapp.FormFactor'/>
+
+  <!-- Inherit RequestFactory.                                    -->
+  <inherits name='com.google.web.bindery.requestfactory.RequestFactory'/>
+
+  <!-- Inherit Activities and Places.                             -->
+  <inherits name='com.google.gwt.activity.Activity'/>
+  <inherits name='com.google.gwt.place.Place'/>
+  
+  <!-- Inherit the default GWT style sheet.  You can change       -->
+  <!-- the theme of your GWT application by uncommenting          -->
+  <!-- any one of the following lines.                            -->
+  <inherits name='com.google.gwt.user.theme.clean.Clean'/>
+  <!-- <inherits name='com.google.gwt.user.theme.standard.Standard'/> -->
+  <!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> -->
+  <!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/>     -->
+
+  <!-- Other module inherits                                      -->
+
+  <!-- Specify the app entry point class.                         -->
+  <entry-point class='com.google.gwt.sample.mobilewebapp.client.MobileWebApp'/>
+
+  <!-- Use ClientFactoryImpl by default -->
+  <replace-with class="com.google.gwt.sample.mobilewebapp.client.ClientFactoryImpl">
+    <when-type-is class="com.google.gwt.sample.mobilewebapp.client.ClientFactory"/>
+  </replace-with>
+
+  <!-- Use ClientFactoryImplMobile for mobile form factor. -->
+  <replace-with class="com.google.gwt.sample.mobilewebapp.client.ClientFactoryImplMobile">
+    <when-type-is class="com.google.gwt.sample.mobilewebapp.client.ClientFactory"/>
+    <when-property-is name="formfactor" value="mobile"/>
+  </replace-with>
+
+  <!-- Use ClientFactoryImplTablet for tablet form factor. -->
+  <replace-with class="com.google.gwt.sample.mobilewebapp.client.ClientFactoryImplTablet">
+    <when-type-is class="com.google.gwt.sample.mobilewebapp.client.ClientFactory"/>
+    <when-property-is name="formfactor" value="tablet"/>
+  </replace-with>
+
+  <!-- Specify the paths for translatable code                    -->
+  <source path='client'/>
+  <source path='shared'/>
+</module>
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactory.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactory.java
new file mode 100644
index 0000000..d2dc4df
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.place.shared.PlaceController;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+
+/**
+ * A factory for retrieving objects used throughout the application.
+ */
+public interface ClientFactory {
+
+  /**
+   * Get the event bus.
+   * 
+   * @return the {@link EventBus} used throughout the app
+   */
+  EventBus getEventBus();
+
+  /**
+   * Get the local {@link Storage} object if supported.
+   * 
+   * @return the local {@link Storage} object
+   */
+  Storage getLocalStorageIfSupported();
+
+  /**
+   * Get the {@link PlaceController}.
+   * 
+   * @return the place controller
+   */
+  PlaceController getPlaceController();
+
+  /**
+   * Get the RequestFactory used to query the server.
+   * 
+   * @return the request factory
+   */
+  MobileWebAppRequestFactory getRequestFactory();
+
+  /**
+   * Get the UI shell.
+   * 
+   * @return the shell
+   */
+  MobileWebAppShell getShell();
+
+  /**
+   * Get an implementation of {@link TaskEditView}.
+   */
+  TaskEditView getTaskEditView();
+
+  /**
+   * Get an implementation of {@link TaskListView}.
+   */
+  TaskListView getTaskListView();
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImpl.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImpl.java
new file mode 100644
index 0000000..dd68aad
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImpl.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2011 Google Inc.
+ * 
+ * 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.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.place.shared.PlaceController;
+import com.google.web.bindery.requestfactory.gwt.client.DefaultRequestTransport;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.client.desktop.DesktopTaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.desktop.DesktopTaskListView;
+import com.google.gwt.sample.mobilewebapp.client.desktop.MobileWebAppShellDesktop;
+
+/**
+ * Default implementation of {@link ClientFactory}. Used by desktop version.
+ */
+class ClientFactoryImpl implements ClientFactory {
+
+  /**
+   * The URL argument used to enable or disable local storage.
+   */
+  private static final String STORAGE_URL_ARG = "storage";
+
+  private final EventBus eventBus = new SimpleEventBus();
+  private final PlaceController placeController = new PlaceController(eventBus);
+  private final MobileWebAppRequestFactory requestFactory;
+  private MobileWebAppShell shell;
+  private final Storage localStorage;
+  private TaskEditView taskEditView;
+  private TaskListView taskListView;
+
+  public ClientFactoryImpl() {
+    DefaultRequestTransport requestTransport = new DefaultRequestTransport();
+    requestFactory = GWT.create(MobileWebAppRequestFactory.class);
+    requestFactory.initialize(eventBus, requestTransport);
+
+    // Initialize local storage.
+    String storageUrlValue = Window.Location.getParameter(STORAGE_URL_ARG);
+    if (storageUrlValue == null || storageUrlValue.startsWith("t")) {
+      localStorage = Storage.getLocalStorageIfSupported();
+    } else {
+      localStorage = null;
+    }
+  }
+
+  public EventBus getEventBus() {
+    return eventBus;
+  }
+
+  public Storage getLocalStorageIfSupported() {
+    return localStorage;
+  }
+
+  public PlaceController getPlaceController() {
+    return placeController;
+  }
+
+  public MobileWebAppRequestFactory getRequestFactory() {
+    return requestFactory;
+  }
+
+  public MobileWebAppShell getShell() {
+    if (shell == null) {
+      shell = createShell();
+    }
+    return shell;
+  }
+
+  public TaskEditView getTaskEditView() {
+    if (taskEditView == null) {
+      taskEditView = createTaskEditView();
+    }
+    return taskEditView;
+  }
+
+  public TaskListView getTaskListView() {
+    if (taskListView == null) {
+      taskListView = createTaskListView();
+    }
+    return taskListView;
+  }
+
+  /**
+   * Create the application UI shell.
+   * 
+   * @return the UI shell
+   */
+  protected MobileWebAppShell createShell() {
+    return new MobileWebAppShellDesktop(this);
+  }
+
+  /**
+   * Create a {@link TaskEditView}.
+   * 
+   * @return a new {@link TaskEditView}
+   */
+  protected TaskEditView createTaskEditView() {
+    return new DesktopTaskEditView();
+  }
+
+  /**
+   * Create a {@link TaskListView}.
+   * 
+   * @return a new {@link TaskListView}
+   */
+  protected TaskListView createTaskListView() {
+    return new DesktopTaskListView();
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplMobile.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplMobile.java
new file mode 100644
index 0000000..ba4f31c
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplMobile.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.client.mobile.MobileTaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.mobile.MobileTaskListView;
+import com.google.gwt.sample.mobilewebapp.client.mobile.MobileWebAppShellMobile;
+
+/**
+ * Mobile version of {@link ClientFactory}.
+ */
+public class ClientFactoryImplMobile extends ClientFactoryImpl {
+
+  @Override
+  protected MobileWebAppShell createShell() {
+    return new MobileWebAppShellMobile(this);
+  }
+
+  @Override
+  protected TaskEditView createTaskEditView() {
+    return new MobileTaskEditView();
+  }
+
+  @Override
+  protected TaskListView createTaskListView() {
+    return new MobileTaskListView();
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplTablet.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplTablet.java
new file mode 100644
index 0000000..12b3699
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplTablet.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.client.mobile.MobileTaskListView;
+import com.google.gwt.sample.mobilewebapp.client.tablet.MobileWebAppShellTablet;
+import com.google.gwt.sample.mobilewebapp.client.tablet.TabletTaskEditView;
+
+/**
+ * Tablet version of {@link ClientFactory}.
+ */
+public class ClientFactoryImplTablet extends ClientFactoryImpl {
+
+  @Override
+  protected MobileWebAppShell createShell() {
+    return new MobileWebAppShellTablet(this);
+  }
+
+  @Override
+  protected TaskEditView createTaskEditView() {
+    return new TabletTaskEditView();
+  }
+
+  @Override
+  protected TaskListView createTaskListView() {
+    // Use the mobile list view on tablets.
+    return new MobileTaskListView();
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebApp.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebApp.java
new file mode 100644
index 0000000..d2cc221
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebApp.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.activity.shared.ActivityManager;
+import com.google.gwt.activity.shared.ActivityMapper;
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.place.shared.PlaceChangeEvent;
+import com.google.gwt.place.shared.PlaceController;
+import com.google.gwt.place.shared.PlaceHistoryHandler;
+import com.google.gwt.place.shared.PlaceHistoryHandler.DefaultHistorian;
+import com.google.gwt.place.shared.PlaceHistoryHandler.Historian;
+import com.google.gwt.sample.mobilewebapp.client.activity.AppActivityMapper;
+import com.google.gwt.sample.mobilewebapp.client.place.AppPlaceHistoryMapper;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.ui.RootLayoutPanel;
+
+/**
+ * Entry point classes define <code>onModuleLoad()</code>.
+ */
+public class MobileWebApp implements EntryPoint {
+
+  static final String HISTORY_SAVE_KEY = "SAVEPLACE";
+  final Storage storage = Storage.getLocalStorageIfSupported();
+
+  /**
+   * This is the entry point method.
+   */
+  public void onModuleLoad() {
+    /*
+     * Initialize the ClientFactory, which includes common resources used
+     * throughout the app. We bundle them all into a ClientFactory so we only
+     * have to pass one argument around.
+     */
+    ClientFactory clientFactory = GWT.create(ClientFactory.class);
+    EventBus eventBus = clientFactory.getEventBus();
+    PlaceController placeController = clientFactory.getPlaceController();
+
+    /*
+     * Add the main application shell to the RootLayoutPanel. The shell includes
+     * the header bar at the top and a content area.
+     */
+    MobileWebAppShell shell = clientFactory.getShell();
+    RootLayoutPanel.get().add(shell);
+
+    /*
+     * Start ActivityManager for the main widget with our ActivityMapper. The
+     * ActivityManager starts an activity based on the current place.
+     */
+    ActivityMapper activityMapper = new AppActivityMapper(clientFactory);
+    ActivityManager activityManager = new ActivityManager(activityMapper, eventBus);
+    activityManager.setDisplay(shell);
+
+    /*
+     * Start PlaceHistoryHandler with our PlaceHistoryMapper. The
+     * PlaceHistoryHandler chooses the correct place based on the history token
+     * in the URL.
+     */
+    final AppPlaceHistoryMapper historyMapper = GWT.create(AppPlaceHistoryMapper.class);
+    Historian historian = (Historian) GWT.create(DefaultHistorian.class);
+
+    PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(historyMapper, historian);
+    Place savedPlace = null;
+    if (storage != null) {
+      try {
+        // wrap in try-catch in case stored value is invalid
+        savedPlace = historyMapper.getPlace(storage.getItem(HISTORY_SAVE_KEY));
+      } catch (Throwable t) {
+        // ignore error
+      }
+    }
+    if (savedPlace == null) {
+      savedPlace = new TaskListPlace(true);
+    }
+    historyHandler.register(placeController, eventBus, savedPlace);
+
+    // Go to the place represented in the URL else default place.
+    historyHandler.handleCurrentHistory();
+
+    /* Hook up storage to listen to event bus */
+    eventBus.addHandler(PlaceChangeEvent.TYPE, new PlaceChangeEvent.Handler() {
+      public void onPlaceChange(PlaceChangeEvent event) {
+        Place newPlace = event.getNewPlace();
+        if (storage != null) {
+          storage.setItem(HISTORY_SAVE_KEY, historyMapper.getToken(newPlace));
+        }
+      }
+    });
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppRequestFactory.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppRequestFactory.java
new file mode 100644
index 0000000..1900a4c
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppRequestFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client;
+
+import com.google.web.bindery.requestfactory.shared.RequestFactory;
+
+/**
+ * Request factory for this app.
+ */
+public interface MobileWebAppRequestFactory extends RequestFactory {
+
+  /**
+   * Create a new {@link TaskRequest}.
+   */
+  TaskRequest taskRequest();
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShell.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShell.java
new file mode 100644
index 0000000..fe94209
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShell.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.AcceptsOneWidget;
+import com.google.gwt.user.client.ui.IsWidget;
+
+/**
+ * An interface that describes the UI Shell.
+ */
+public interface MobileWebAppShell extends AcceptsOneWidget, IsWidget {
+
+  /**
+   * Check if the task list is included in the shell. If true, a
+   * {@link com.google.gwt.sample.mobilewebapp.client.activity.TaskListActivity}
+   * will not be started.
+   * 
+   * @return true if included, false if not
+   */
+  boolean isTaskListIncluded();
+
+  /**
+   * Set the handler to invoke when the add button is pressed. If no handler is
+   * specified, the button is hidden.
+   * 
+   * @param handler the handler to add to the button, or null to hide
+   */
+  void setAddButtonHandler(ClickHandler handler);
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShellBase.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShellBase.java
new file mode 100644
index 0000000..91e8686
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShellBase.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.ResizeComposite;
+
+/**
+ * Base class for UI shell.
+ */
+public abstract class MobileWebAppShellBase extends ResizeComposite implements
+    MobileWebAppShell {
+
+  /**
+   * Calculate the orientation based on the screen dimensions.
+   * 
+   * @return true if portrait, false if lansdcape
+   */
+  private static boolean calculateOrientationPortrait() {
+    return Window.getClientHeight() > Window.getClientWidth();
+  }
+
+  /**
+   * The registration for the window resize handler.
+   */
+  private HandlerRegistration windowResizeHandler;
+
+  /**
+   * The current orientation of the app.
+   */
+  private boolean isOrientationPortrait;
+
+  /**
+   * Adjust the orientation based on the current window height and width. This
+   * is a no-op by default, but subclasses can override it.
+   * 
+   * @param isPortrait true if in portrait orientation, false if landscape
+   */
+  protected void adjustOrientation(boolean isPortrait) {
+    // No-op by default.
+  }
+
+  /**
+   * Check if we are in portrait or landscape orientation.
+   * 
+   * @return true if portrait, false if lansdcape
+   */
+  protected boolean isOrientationPortrait() {
+    return isOrientationPortrait;
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+
+    /*
+     * Listen for changes in the window size so we can adjust the orientation of
+     * the app. This will also catch orientation changes on mobile devices.
+     */
+    windowResizeHandler = Window.addResizeHandler(new ResizeHandler() {
+      public void onResize(ResizeEvent event) {
+        if (isOrientationPortrait != calculateOrientationPortrait()) {
+          isOrientationPortrait = !isOrientationPortrait;
+          adjustOrientation(isOrientationPortrait);
+        }
+      }
+    });
+
+    // Initialize the orientation. Do not animate when we first load the shell.
+    isOrientationPortrait = calculateOrientationPortrait();
+    adjustOrientation(isOrientationPortrait);
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    windowResizeHandler.removeHandler();
+    windowResizeHandler = null;
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/TaskRequest.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/TaskRequest.java
new file mode 100644
index 0000000..65fa8ec
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/TaskRequest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client;
+
+import com.google.gwt.sample.mobilewebapp.server.domain.Task;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.web.bindery.requestfactory.shared.InstanceRequest;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.Service;
+
+import java.util.List;
+
+/**
+ * Remote request for {@link Task}.
+ */
+@Service(Task.class)
+public interface TaskRequest extends RequestContext {
+
+  /**
+   * Create a {@link Request} for all tasks.
+   * 
+   * @return a {@link Request}
+   */
+  Request<List<TaskProxy>> findAllTasks();
+
+  /**
+   * Create a {@link Request} to find a Task by id.
+   * 
+   * @param id the task id
+   * @return a {@link Request}
+   */
+  Request<TaskProxy> findTask(Long id);
+
+  /**
+   * Persist a Task instance in the datastore.
+   * 
+   * @return an {@link InstanceRequest}
+   */
+  InstanceRequest<TaskProxy, Void> persist();
+
+  /**
+   * Remove a Task instance from the datastore.
+   * 
+   * @return an {@link InstanceRequest}
+   */
+  InstanceRequest<TaskProxy, Void> remove();
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapper.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapper.java
new file mode 100644
index 0000000..ac387d1
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.activity;
+
+import com.google.gwt.activity.shared.Activity;
+import com.google.gwt.activity.shared.ActivityMapper;
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+
+/**
+ * A mapping of places to activities used by this application.
+ */
+public class AppActivityMapper implements ActivityMapper {
+
+  private final ClientFactory clientFactory;
+
+  public AppActivityMapper(ClientFactory clientFactory) {
+    this.clientFactory = clientFactory;
+  }
+
+  public Activity getActivity(Place place) {
+    if (place instanceof TaskListPlace) {
+      // The list of tasks.
+      if (!clientFactory.getShell().isTaskListIncluded()) {
+        // Do not start a task list activity if the task list is always visible.
+        return new TaskListActivity(clientFactory, ((TaskListPlace) place).isTaskListStale());
+      }
+    } else if (place instanceof TaskEditPlace) {
+      // Editable view of a task.
+      return new TaskEditActivity((TaskEditPlace) place, clientFactory);
+    }
+    return null;
+  }
+
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditActivity.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditActivity.java
new file mode 100644
index 0000000..29f5b95
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditActivity.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.activity;
+
+import com.google.gwt.activity.shared.AbstractActivity;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.TaskRequest;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.user.client.ui.AcceptsOneWidget;
+import com.google.web.bindery.requestfactory.shared.Receiver;
+
+/**
+ * Activity that presents a task to be edited.
+ */
+public class TaskEditActivity extends AbstractActivity implements TaskEditView.Presenter {
+
+  private static final int ONE_HOUR = 3600000;
+
+  private final ClientFactory clientFactory;
+
+  /**
+   * A boolean indicating whether or not this activity is still active. The user
+   * might move to another activity while this one is loading, in which case we
+   * do not want to do any more work.
+   */
+  private boolean isDead = false;
+
+  /**
+   * The current task being edited.
+   */
+  private TaskProxy task;
+
+  /**
+   * The ID of the current task being edited.
+   */
+  private final Long taskId;
+
+  /**
+   * Construct a new {@link TaskEditActivity}.
+   * 
+   * @param place the task being edited
+   * @param clientFactory the {@link ClientFactory} of shared resources
+   */
+  public TaskEditActivity(TaskEditPlace place, ClientFactory clientFactory) {
+    this.taskId = place.getTaskId();
+    this.clientFactory = clientFactory;
+  }
+
+  public void deleteTask() {
+    if (task == null) {
+      doCancelTask();
+    } else {
+      doDeleteTask();
+    }
+  }
+
+  @Override
+  public void onCancel() {
+    // Ignore all incoming responses to the requests from this activity.
+    isDead = true;
+    clientFactory.getTaskEditView().setLocked(false);
+  }
+
+  @Override
+  public void onStop() {
+    // Ignore all incoming responses to the requests from this activity.
+    isDead = true;
+    clientFactory.getTaskEditView().setLocked(false);
+  }
+
+  public void saveTask(boolean addToCalendar) {
+    if (task == null) {
+      doCreateTask(addToCalendar);
+    } else {
+      doUpdateTask();
+    }
+  }
+
+  public void start(AcceptsOneWidget container, EventBus eventBus) {
+    // Hide the 'add' button in the shell.
+    clientFactory.getShell().setAddButtonHandler(null);
+
+    // Set the presenter on the view.
+    final TaskEditView view = clientFactory.getTaskEditView();
+    view.setPresenter(this);
+
+    // Clear the display until the task is loaded.
+    showTask(null);
+
+    if (taskId != null) {
+      // Lock the display until the task is loaded.
+      view.setLocked(true);
+
+      // Load the existing task.
+      clientFactory.getRequestFactory().taskRequest().findTask(this.taskId).fire(
+          new Receiver<TaskProxy>() {
+            @Override
+            public void onSuccess(TaskProxy response) {
+              // Early exit if this activity has already been cancelled.
+              if (isDead) {
+                return;
+              }
+
+              // Show the task.
+              showTask(response);
+              view.setLocked(false);
+            }
+          });
+    }
+
+    // Display the view.
+    container.setWidget(view.asWidget());
+  }
+
+  /**
+   * Cancel the current task.
+   */
+  private void doCancelTask() {
+    clientFactory.getPlaceController().goTo(new TaskListPlace(false));
+  }
+
+  /**
+   * Create a new task.
+   * 
+   * @param addToCalendar true to add the task to the calendar
+   */
+  private void doCreateTask(final boolean addToCalendar) {
+    TaskRequest request = clientFactory.getRequestFactory().taskRequest();
+    final TaskProxy toCreate = request.create(TaskProxy.class);
+    populateTaskFromView(toCreate);
+    request.persist().using(toCreate).fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void response) {
+        onTaskCreated(toCreate, addToCalendar);
+      }
+    });
+  }
+
+  /**
+   * Delete the current task.
+   */
+  private void doDeleteTask() {
+    if (task == null) {
+      return;
+    }
+
+    // Delete the task in the data store.
+    final TaskProxy toDelete = this.task;
+    clientFactory.getRequestFactory().taskRequest().remove().using(toDelete).fire(
+        new Receiver<Void>() {
+          @Override
+          public void onSuccess(Void response) {
+            onTaskDeleted(toDelete);
+          }
+        });
+  }
+
+  /**
+   * Update the current task.
+   */
+  private void doUpdateTask() {
+    if (task == null) {
+      return;
+    }
+
+    // Create a mutable version of the current task.
+    TaskRequest request = clientFactory.getRequestFactory().taskRequest();
+    final TaskProxy toEdit = request.edit(task);
+    populateTaskFromView(toEdit);
+
+    // Persist the changes.
+    request.persist().using(toEdit).fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void response) {
+        onTaskUpdated(toEdit);
+      }
+    });
+  }
+
+  /**
+   * Notify the user of a message.
+   * 
+   * @param message the message to display
+   */
+  private void notify(String message) {
+    // TODO Add notification pop-up
+  }
+
+  /**
+   * Called when a task has been successfully created.
+   * 
+   * @param task the task that was created
+   * @param addToCalendar true to add the task to the calendar
+   */
+  private void onTaskCreated(TaskProxy task, boolean addToCalendar) {
+    // Notify the user that the task was created.
+    notify("Created task '" + task.getName() + "'");
+
+    // Return to the task list.
+    clientFactory.getPlaceController().goTo(new TaskListPlace(true));
+  }
+
+  /**
+   * Called when a task has been successfully deleted.
+   * 
+   * @param task the task that was deleted
+   */
+  private void onTaskDeleted(TaskProxy task) {
+    // Notify the user that the task was deleted.
+    notify("Task Deleted");
+
+    // Return to the task list.
+    clientFactory.getPlaceController().goTo(new TaskListPlace(true));
+  }
+
+  /**
+   * Called when a task has been successfully updated.
+   * 
+   * @param task the task that was updated
+   */
+  private void onTaskUpdated(TaskProxy task) {
+    // Notify the user that the task was updated.
+    notify("Task Updated");
+
+    // Return to the task list.
+    clientFactory.getPlaceController().goTo(new TaskListPlace(true));
+  }
+
+  /**
+   * Populate the specified task using the values the user specified in the
+   * view.
+   * 
+   * @param task the task to populate
+   */
+  private void populateTaskFromView(TaskProxy task) {
+    TaskEditView view = clientFactory.getTaskEditView();
+    task.setName(view.getName());
+    task.setNotes(view.getNotes());
+    task.setDueDate(view.getDueDate());
+  }
+
+  /**
+   * Show the specified task in the view.
+   * 
+   * @param task the task to show
+   */
+  private void showTask(TaskProxy task) {
+    this.task = task;
+    TaskEditView view = clientFactory.getTaskEditView();
+    if (task == null) {
+      // Create a new task.
+      view.setEditing(false);
+      view.setDueDate(null);
+      view.setName("");
+      view.setNotes("");
+    } else {
+      // Edit an existing task.
+      view.setEditing(true);
+      view.setDueDate(task.getDueDate());
+      view.setName(task.getName());
+      view.setNotes(task.getNotes());
+    }
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditView.java
new file mode 100644
index 0000000..026a6ce
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditView.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.activity;
+
+import com.google.gwt.user.client.ui.IsWidget;
+
+import java.util.Date;
+
+/**
+ * A view of {@link TaskEditActivity}.
+ */
+public interface TaskEditView extends IsWidget {
+
+  /**
+   * The presenter for this view.
+   */
+  public static interface Presenter {
+    /**
+     * Delete the current task or cancel the creation of a task.
+     */
+    void deleteTask();
+
+    /**
+     * Create a new task or save the current task based on the values in the
+     * inputs.
+     * 
+     * @param addToCalendar true to add the task to the calendar
+     */
+    void saveTask(boolean addToCalendar);
+  }
+
+  Date getDueDate();
+
+  String getName();
+
+  String getNotes();
+
+  /**
+   * Set the due date of the task.
+   */
+  void setDueDate(Date date);
+
+  /**
+   * Specify whether the view is editing an existing task or creating a new
+   * task.
+   * 
+   * @param isEditing true if editing, false if creating
+   */
+  void setEditing(boolean isEditing);
+
+  /**
+   * Lock or unlock the UI so the user cannot enter data. The UI is locked until
+   * the task is loaded.
+   * 
+   * @param locked true to lock, false to unlock
+   */
+  void setLocked(boolean locked);
+
+  /**
+   * Set the task name.
+   */
+  void setName(String name);
+
+  /**
+   * Set the notes associated with the task.
+   */
+  void setNotes(String notes);
+
+  /**
+   * Set the {@link Presenter} for this view.
+   * 
+   * @param presenter the presenter
+   */
+  void setPresenter(Presenter presenter);
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListActivity.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListActivity.java
new file mode 100644
index 0000000..832931a
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListActivity.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.activity;
+
+import com.google.gwt.activity.shared.AbstractActivity;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.AcceptsOneWidget;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxyImpl;
+import com.google.web.bindery.requestfactory.shared.Receiver;
+import com.google.web.bindery.requestfactory.shared.ServerFailure;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Activity that presents a list of tasks.
+ */
+public class TaskListActivity extends AbstractActivity implements TaskListView.Presenter {
+
+  /**
+   * Event fired when the task list is updated.
+   */
+  public static class TaskListUpdateEvent extends GwtEvent<TaskListActivity.TaskListUpdateHandler> {
+
+    /**
+     * Handler type.
+     */
+    private static Type<TaskListUpdateHandler> TYPE;
+
+    /**
+     * Gets the type associated with this event.
+     * 
+     * @return returns the handler type
+     */
+    public static Type<TaskListUpdateHandler> getType() {
+      if (TYPE == null) {
+        TYPE = new Type<TaskListUpdateHandler>();
+      }
+      return TYPE;
+    }
+
+    private final List<TaskProxy> tasks;
+
+    public TaskListUpdateEvent(List<TaskProxy> tasks) {
+      this.tasks = tasks;
+    }
+
+    @Override
+    public Type<TaskListUpdateHandler> getAssociatedType() {
+      return TYPE;
+    }
+
+    public List<TaskProxy> getTasks() {
+      return tasks;
+    }
+
+    @Override
+    protected void dispatch(TaskListUpdateHandler handler) {
+      handler.onTaskListUpdated(this);
+    }
+  }
+
+  /**
+   * Handler for {@link TaskListUpdateEvent}.
+   */
+  public static interface TaskListUpdateHandler extends EventHandler {
+
+    /**
+     * Called when the task list is updated.
+     */
+    void onTaskListUpdated(TaskListUpdateEvent event);
+  }
+
+  private static final String TASKLIST_SAVE_KEY = "TASKLIST";
+  private static final String TASKSEP = "&&";
+  private static final String FIELDSEP = "@@";
+  private static final String FIELDEMPTY = "***";
+
+  /**
+   * The delay in milliseconds between calls to refresh the task list.
+   */
+  private static final int REFRESH_DELAY = 5000;
+
+  /**
+   * Convert a task proxy list into a string.
+   */
+  private static String getStringFromTaskProxy(List<TaskProxy> list) {
+    StringBuilder sb = new StringBuilder();
+    for (TaskProxy proxy : list) {
+      sb.append(proxy.getDueDate() != null ? proxy.getDueDate().getTime() : FIELDEMPTY);
+      sb.append(FIELDSEP);
+      sb.append(proxy.getId() != null ? proxy.getId() : "");
+      sb.append(FIELDSEP);
+      String name = proxy.getName();
+      sb.append(name != null && name.length() > 0 ? proxy.getName() : FIELDEMPTY);
+      sb.append(FIELDSEP);
+      String notes = proxy.getNotes();
+      sb.append(notes != null && notes.length() > 0 ? proxy.getNotes() : FIELDEMPTY);
+      sb.append(TASKSEP);
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Parse a task proxy list from a string.
+   */
+  private static List<TaskProxy> getTaskProxyFromString(String taskProxyList) {
+    ArrayList<TaskProxy> list = new ArrayList<TaskProxy>(0);
+    if (taskProxyList == null) {
+      return list;
+    }
+    // taskproxy1&&taskproxy2&&taskproxy3&&...
+    String taskProxyStrings[] = taskProxyList.split(TASKSEP);
+    for (String taskProxyString : taskProxyStrings) {
+      if (taskProxyString == null) {
+        continue;
+      }
+      // date@@id@@name@@notes
+      String taskProxyStringData[] = taskProxyString.split(FIELDSEP);
+      if (taskProxyStringData.length >= 4) {
+        // collect the fields
+        String dateString = taskProxyStringData[0];
+        String idString = taskProxyStringData[1];
+        String nameString = taskProxyStringData[2];
+        if (FIELDEMPTY.equals(nameString)) {
+          nameString = null;
+        }
+        String notesString = taskProxyStringData[3];
+        if (FIELDEMPTY.equals(notesString)) {
+          notesString = null;
+        }
+        // parse the numerical fields
+        Date dueDate = null;
+        try {
+          dueDate = new Date(Long.parseLong(dateString));
+        } catch (NumberFormatException nfe) {
+        }
+        Long idLong = 0L;
+        try {
+          idLong = Long.parseLong(idString);
+        } catch (NumberFormatException nfe) {
+        }
+        // create and populate the TaskProxy
+        TaskProxyImpl taskProxy = new TaskProxyImpl();
+        taskProxy.setDueDate(dueDate);
+        taskProxy.setId(idLong);
+        taskProxy.setName(nameString);
+        taskProxy.setNotes(notesString);
+        list.add(taskProxy);
+      }
+    }
+    return list;
+  }
+
+  private final Storage storage;
+
+  /**
+   * A boolean indicating that we should clear the task list when started.
+   */
+  private final boolean clearTaskList;
+
+  private final ClientFactory clientFactory;
+
+  /**
+   * A boolean indicating whether or not this activity is still active. The user
+   * might move to another activity while this one is loading, in which case we
+   * do not want to do any more work.
+   */
+  private boolean isDead = false;
+
+  /**
+   * The refresh timer used to periodically refresh the task list.
+   */
+  private Timer refreshTimer;
+
+  /**
+   * Construct a new {@link TaskListActivity}.
+   * 
+   * @param clientFactory the {@link ClientFactory} of shared resources
+   * @param clearTaskList true to clear the task list, false not to
+   */
+  public TaskListActivity(ClientFactory clientFactory, boolean clearTaskList) {
+    this.clientFactory = clientFactory;
+    this.clearTaskList = clearTaskList;
+    this.storage = clientFactory.getLocalStorageIfSupported();
+  }
+
+  @Override
+  public void onCancel() {
+    killActivity();
+  }
+
+  @Override
+  public void onStop() {
+    killActivity();
+  }
+
+  public void selectTask(TaskProxy selected) {
+    // Go into edit mode when a task is selected.
+    clientFactory.getPlaceController().goTo(TaskEditPlace.createTaskEditPlace(selected.getId()));
+  }
+
+  public void start(AcceptsOneWidget container, EventBus eventBus) {
+    // Add a handler to the 'add' button in the shell.
+    clientFactory.getShell().setAddButtonHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        clientFactory.getPlaceController().goTo(TaskEditPlace.getTaskCreatePlace());
+      }
+    });
+
+    // Set the presenter on the view.
+    final TaskListView view = clientFactory.getTaskListView();
+    view.setPresenter(this);
+
+    // Clear the task list and display it.
+    if (clearTaskList) {
+      view.clearList();
+    }
+    container.setWidget(view);
+
+    // Create a timer to periodically refresh the task list.
+    refreshTimer = new Timer() {
+      @Override
+      public void run() {
+        refreshTaskList();
+      }
+    };
+
+    // Load the saved task list from storage
+    if (storage != null) { // if storage is supported
+      String taskString = storage.getItem(TASKLIST_SAVE_KEY);
+      if (taskString != null) {
+        List<TaskProxy> list = getTaskProxyFromString(taskString);
+        setTasks(list);
+      }
+    }
+
+    // Request the task list now.
+    refreshTaskList();
+  }
+
+  /**
+   * Kill this activity.
+   */
+  private void killActivity() {
+    // Ignore all incoming responses to the requests from this activity.
+    isDead = true;
+
+    // Kill the refresh timer.
+    if (refreshTimer != null) {
+      refreshTimer.cancel();
+    }
+  }
+
+  /**
+   * Refresh the task list.
+   */
+  private void refreshTaskList() {
+    clientFactory.getRequestFactory().taskRequest().findAllTasks().fire(
+        new Receiver<List<TaskProxy>>() {
+          @Override
+          public void onFailure(ServerFailure error) {
+            // ignore
+          }
+
+          @Override
+          public void onSuccess(List<TaskProxy> response) {
+            // Early exit if this activity has already been canceled.
+            if (isDead) {
+              return;
+            }
+
+            // Display the tasks in the view.
+            if (response == null) {
+              setTasks(Collections.<TaskProxy> emptyList());
+            } else {
+              setTasks(response);
+
+              // save the response to storage
+              if (storage != null) { // if storage is supported
+                String responseString = getStringFromTaskProxy(response);
+                storage.setItem(TASKLIST_SAVE_KEY, responseString);
+              }
+            }
+
+            // Restart the timer.
+            refreshTimer.schedule(REFRESH_DELAY);
+          }
+        });
+  }
+
+  /**
+   * Set the list of tasks.
+   */
+  private void setTasks(List<TaskProxy> tasks) {
+    clientFactory.getTaskListView().setTasks(tasks);
+    clientFactory.getEventBus().fireEventFromSource(new TaskListUpdateEvent(tasks), this);
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListView.java
new file mode 100644
index 0000000..2bc0aa9
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/activity/TaskListView.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.activity;
+
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.List;
+
+/**
+ * A view of a {@link TaskListActivity}.
+ */
+public interface TaskListView extends IsWidget {
+
+  /**
+   * The presenter for this view.
+   */
+  public static interface Presenter {
+
+    /**
+     * Select a task.
+     * 
+     * @param selected the select task
+     */
+    void selectTask(TaskProxy selected);
+  }
+
+  /**
+   * Clear the list of tasks.
+   */
+  void clearList();
+
+  /**
+   * Set the {@link Presenter} for this view.
+   * 
+   * @param presenter the presenter
+   */
+  void setPresenter(Presenter presenter);
+
+  /**
+   * Set the list of tasks to display.
+   * 
+   * @param tasks the list of tasks
+   */
+  void setTasks(List<TaskProxy> tasks);
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.java
new file mode 100644
index 0000000..349793f
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.desktop;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DecoratedPopupPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.TextBoxBase;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.datepicker.client.DatePicker;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+
+import java.util.Date;
+
+/**
+ * View used to edit a task.
+ */
+public class DesktopTaskEditView extends Composite implements TaskEditView {
+
+  /**
+   * The UiBinder interface.
+   */
+  interface DesktopTaskEditViewUiBinder extends
+      UiBinder<Widget, DesktopTaskEditView> {
+  }
+
+  /**
+   * The UiBinder used to generate the view.
+   */
+  private static DesktopTaskEditViewUiBinder uiBinder = GWT.create(DesktopTaskEditViewUiBinder.class);
+
+  /**
+   * The formatter used to format the date.
+   */
+  private static DateTimeFormat dateFormat = DateTimeFormat.getFormat("EE, MMM d, yyyy");
+
+  /**
+   * The glass panel used to lock the UI.
+   */
+  private static PopupPanel glassPanel;
+
+  /**
+   * Show or hide the glass panel used to lock the UI will the task loads.
+   * 
+   * @param visible true to show, false to hide
+   */
+  private static void setGlassPanelVisible(boolean visible) {
+    // Initialize the panel.
+    if (glassPanel == null) {
+      glassPanel = new DecoratedPopupPanel(false, true);
+      glassPanel.setWidget(new Label("Loading..."));
+    }
+
+    if (visible) {
+      // Show the loading panel.
+      glassPanel.center();
+    } else {
+      // Hide the loading panel.
+      glassPanel.hide();
+    }
+  }
+
+  /**
+   * The checkbox used to specify that the task should be added to the calendar.
+   */
+  @UiField
+  CheckBox calendarCheckbox;
+
+  /**
+   * The button used to select the date.
+   */
+  @UiField
+  Button dateButton;
+
+  /**
+   * The button used to delete a task or cancel changes.
+   */
+  @UiField
+  Button deleteButton;
+
+  /**
+   * The text box used to enter the task name.
+   */
+  @UiField
+  TextBoxBase nameBox;
+
+  /**
+   * The text box used to enter task notes.
+   */
+  @UiField
+  TextBoxBase notesBox;
+
+  /**
+   * The text box used to save changes or create a new task.
+   */
+  @UiField
+  Button saveButton;
+
+  /**
+   * The current date value.
+   */
+  private Date currentDate;
+
+  /**
+   * The popup panel that contains a date picker for selecting the date.
+   */
+  private final PopupPanel datePickerPopup;
+
+  /**
+   * The {@link Presenter} for this view.
+   */
+  private Presenter presenter;
+
+  /**
+   * Construct a new {@link DesktopTaskEditView}.
+   * 
+   * @param presenter the {@link Presenter} that handles interactions
+   */
+  public DesktopTaskEditView() {
+    initWidget(uiBinder.createAndBindUi(this));
+
+    // Create the datePickerPopup.
+    final DatePicker datePicker = new DatePicker();
+    datePickerPopup = new PopupPanel(true, true);
+    datePickerPopup.setWidget(datePicker);
+    datePickerPopup.setGlassEnabled(true);
+
+    /*
+     * When the user clicks on the date button, open a date picker so they can
+     * select a date.
+     */
+    dateButton.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        datePicker.setValue(currentDate, false);
+        datePickerPopup.center();
+      }
+    });
+    datePicker.addValueChangeHandler(new ValueChangeHandler<Date>() {
+      public void onValueChange(ValueChangeEvent<Date> event) {
+        setDueDate(event.getValue());
+        datePickerPopup.hide();
+      }
+    });
+
+    // Create a new task or modify the current task when done is pressed.
+    saveButton.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        if (presenter != null) {
+          presenter.saveTask(calendarCheckbox.getValue());
+        }
+      }
+    });
+
+    // Delete the current task or cancel when delete is pressed.
+    deleteButton.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        if (presenter != null) {
+          presenter.deleteTask();
+        }
+      }
+    });
+  }
+
+  public Date getDueDate() {
+    return currentDate;
+  }
+
+  public String getName() {
+    return nameBox.getValue();
+  }
+
+  public String getNotes() {
+    return notesBox.getValue();
+  }
+
+  public void setDueDate(Date date) {
+    boolean wasNull = (currentDate == null);
+    currentDate = date;
+    if (date == null) {
+      dateButton.setText("Set due date");
+      calendarCheckbox.setValue(false);
+      calendarCheckbox.setEnabled(false);
+    } else {
+      dateButton.setText(dateFormat.format(date));
+      calendarCheckbox.setEnabled(true);
+      if (wasNull) {
+        calendarCheckbox.setValue(true);
+      }
+    }
+  }
+
+  public void setEditing(boolean isEditing) {
+    if (isEditing) {
+      deleteButton.setText("Delete item");
+    } else {
+      deleteButton.setText("Cancel");
+    }
+  }
+
+  public void setLocked(boolean locked) {
+    setGlassPanelVisible(locked);
+  }
+
+  public void setName(String name) {
+    nameBox.setText(name);
+  }
+
+  public void setNotes(String notes) {
+    notesBox.setText(notes);
+  }
+
+  public void setPresenter(Presenter presenter) {
+    this.presenter = presenter;
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.ui.xml
new file mode 100644
index 0000000..2e167fe
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.ui.xml
@@ -0,0 +1,141 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+  xmlns:ui="urn:ui:com.google.gwt.uibinder"
+  xmlns:g="urn:import:com.google.gwt.user.client.ui">
+
+  <ui:style>
+    .title {
+      padding: 4px 10px;
+      font-size: 14pt;
+      font-weight: bold;
+      color: #666;
+    }
+    
+    .editForm {
+      padding: 10px;
+      background: white;
+    }
+    
+    .label {
+      color: #333;
+      font-size: 10pt;
+      padding-bottom: 3px;
+    }
+    
+    .field {
+      width: 300px;
+    }
+    
+    .textBoxWrapper {
+      margin-right: 10px;
+    }
+    
+    .checkbox {
+      color: #3f3f3f;
+    }
+    
+    .notesBox {
+      height: 6em;
+    }
+    
+    .button {
+      padding-top: 8px;
+      padding-bottom: 8px;
+      color: #3f3f3f;
+    }
+    
+    .dateButton {
+      text-align: center;
+    }
+    
+    .buttonPanel {
+      padding: 10px;
+      margin-top: 15px;
+    }
+    
+    .saveButton {
+      width: 130px;
+      margin-right: 5px;
+    }
+    
+    .deleteButton {
+      width: 130px;
+      margin-left: 5px;
+      color: white;
+      background: #940000;
+    }
+  </ui:style>
+
+  <!-- Edit Form. -->
+  <g:HTMLPanel
+    addStyleNames="{style.editForm}">
+    <table
+      cellspacing="10"
+      align="center">
+
+      <!-- Form title. -->
+      <tr>
+        <td
+          align='center'
+          colspan='2'
+          class="{style.title}">Task Details</td>
+      </tr>
+
+      <!-- Task name. -->
+      <tr>
+        <td
+          class="{style.label}">Task Name:</td>
+        <td
+          class="{style.textBoxWrapper}">
+          <g:TextBox
+            addStyleNames="{style.field}"
+            ui:field="nameBox" />
+        </td>
+      </tr>
+
+      <!-- Task notes. -->
+      <tr>
+        <td
+          class="{style.label}">Notes:</td>
+        <td
+          class="{style.textBoxWrapper}">
+          <g:TextArea
+            addStyleNames="{style.field} {style.notesBox}"
+            ui:field="notesBox" />
+        </td>
+      </tr>
+
+      <!-- Task due date. -->
+      <tr>
+        <td
+          class="{style.label}">Due date:</td>
+        <td>
+          <g:Button
+            addStyleNames="{style.field} {style.button} {style.dateButton}"
+            ui:field="dateButton">Set due date</g:Button>
+
+          <!-- Add to calendar. -->
+          <br />
+          <g:CheckBox
+            addStyleNames="{style.checkbox}"
+            ui:field="calendarCheckbox">Add to my calender</g:CheckBox>
+        </td>
+      </tr>
+
+      <!-- Button panel. -->
+      <tr
+        class="{style.buttonPanel}">
+        <td></td>
+        <td
+          align='center'>
+          <g:Button
+            ui:field="saveButton"
+            addStyleNames="{style.button} {style.saveButton}">Done</g:Button>
+          <g:Button
+            ui:field="deleteButton"
+            addStyleNames="{style.button} {style.deleteButton}">Delete Item</g:Button>
+        </td>
+      </tr>
+    </table>
+  </g:HTMLPanel>
+</ui:UiBinder> 
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.java
new file mode 100644
index 0000000..18b84f4
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.desktop;
+
+import com.google.gwt.cell.client.DateCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.cellview.client.CellTable;
+import com.google.gwt.user.cellview.client.Column;
+import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
+import com.google.gwt.user.cellview.client.TextColumn;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.NoSelectionModel;
+import com.google.gwt.view.client.SelectionChangeEvent;
+import com.google.gwt.view.client.SelectionModel;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * View used to display the list of Tasks.
+ */
+public class DesktopTaskListView extends Composite implements TaskListView {
+
+  /**
+   * The UiBinder interface.
+   */
+  interface DesktopTaskListViewUiBinder extends
+      UiBinder<Widget, DesktopTaskListView> {
+  }
+
+  /**
+   * The UiBinder used to generate the view.
+   */
+  private static DesktopTaskListViewUiBinder uiBinder = GWT.create(DesktopTaskListViewUiBinder.class);
+
+  /**
+   * Displays the list of tasks.
+   */
+  @UiField(provided = true)
+  CellTable<TaskProxy> taskList;
+
+  /**
+   * The presenter for this view.
+   */
+  private Presenter presenter;
+
+  /**
+   * Construct a new {@link DesktopTaskListView}.
+   * 
+   * @param presenter the {@link Presenter} that handles this view
+   */
+  public DesktopTaskListView() {
+    // Create the CellTable.
+    taskList = new CellTable<TaskProxy>();
+    taskList.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
+    taskList.setWidth("100%");
+
+    // Add the task name column.
+    Column<TaskProxy, String> nameColumn = new TextColumn<TaskProxy>() {
+      @Override
+      public String getValue(TaskProxy object) {
+        return (object == null) ? null : object.getName();
+      }
+    };
+    taskList.addColumn(nameColumn, "Task");
+
+    // Add the task notes column.
+    Column<TaskProxy, String> notesColumn = new TextColumn<TaskProxy>() {
+      @Override
+      public String getValue(TaskProxy object) {
+        return (object == null) ? "" : object.getNotes();
+      }
+    };
+    taskList.addColumn(notesColumn, "Description");
+
+    // Add the task due date column.
+    Column<TaskProxy, Date> dateColumn = new Column<TaskProxy, Date>(
+        new DateCell()) {
+      @Override
+      public Date getValue(TaskProxy object) {
+        return (object == null) ? null : object.getDueDate();
+      }
+    };
+    taskList.addColumn(dateColumn, "Due Date");
+
+    /*
+     * Inform the presenter when the user selects a task from the task list.
+     */
+    final NoSelectionModel<TaskProxy> selectionModel = new NoSelectionModel<TaskProxy>();
+    taskList.setSelectionModel(selectionModel);
+    selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+      public void onSelectionChange(SelectionChangeEvent event) {
+        // Edit the task.
+        if (presenter != null) {
+          presenter.selectTask(selectionModel.getLastSelectedObject());
+        }
+      }
+    });
+
+    // Initialize the widget.
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  public void clearList() {
+    taskList.setVisibleRangeAndClearData(taskList.getVisibleRange(), true);
+  }
+
+  public void setPresenter(Presenter presenter) {
+    this.presenter = presenter;
+  }
+
+  public void setSelectionModel(SelectionModel<TaskProxy> selectionModel) {
+    taskList.setSelectionModel(selectionModel);
+  }
+
+  public void setTasks(List<TaskProxy> tasks) {
+    taskList.setRowData(tasks);
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.ui.xml
new file mode 100644
index 0000000..266cf6d
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.ui.xml
@@ -0,0 +1,16 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+  xmlns:ui="urn:ui:com.google.gwt.uibinder"
+  xmlns:c="urn:import:com.google.gwt.user.cellview.client"
+  xmlns:g="urn:import:com.google.gwt.user.client.ui">
+
+  <ui:style>
+    
+  </ui:style>
+
+  <g:ScrollPanel>
+    <c:CellTable
+      ui:field="taskList" />
+  </g:ScrollPanel>
+
+</ui:UiBinder> 
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MainMenuCellList.css b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MainMenuCellList.css
new file mode 100644
index 0000000..b0c7222
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MainMenuCellList.css
@@ -0,0 +1,9 @@
+.cellListEvenItem,.cellListOddItem {
+  margin: 8px;
+  border: 1px solid #666;
+  -moz-border-radius: 20px 8px;
+  border-radius: 20px 8px;
+  padding: 18px 10px;
+  font-size: 12pt;
+  background-color: white;
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.java
new file mode 100644
index 0000000..42a713d
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.desktop;
+
+import com.google.gwt.canvas.dom.client.CssColor;
+import com.google.gwt.cell.client.AbstractCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.place.shared.PlaceChangeEvent;
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.cellview.client.CellList;
+import com.google.gwt.user.cellview.client.CellList.Style;
+import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.DeckLayoutPanel;
+import com.google.gwt.user.client.ui.DockLayoutPanel;
+import com.google.gwt.user.client.ui.HasOneWidget;
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.SelectionChangeEvent;
+import com.google.gwt.view.client.SingleSelectionModel;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.MobileWebAppShellBase;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListActivity;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListActivity.TaskListUpdateEvent;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Desktop version of the UI shell.
+ */
+public class MobileWebAppShellDesktop extends MobileWebAppShellBase {
+
+  /**
+   * CSS override used for the main menu.
+   */
+  interface MainMenuStyle extends CellList.Style {
+  }
+
+  interface MobileWebAppShellDesktopUiBinder extends
+      UiBinder<Widget, MobileWebAppShellDesktop> {
+  }
+
+  /**
+   * A ClientBundle that provides images for this widget.
+   */
+  interface Resources extends CellList.Resources {
+    /**
+     * The styles used in the main menu. We extend
+     * {@link CellList.Style#DEFAULT_CSS} with the styles defined in
+     * MainMenuCellList.css.
+     */
+    @Source({"MainMenuCellList.css", CellList.Style.DEFAULT_CSS})
+    Style cellListStyle();
+  }
+
+  /**
+   * An item in the main menu that maps to a specific place.
+   */
+  private static class MainMenuItem {
+    private final String name;
+    private final Place place;
+
+    /**
+     * Construct a new {@link MainMenuItem}.
+     * 
+     * @param name the display name
+     * @param place the place to open when selected
+     */
+    public MainMenuItem(String name, Place place) {
+      this.name = name;
+      this.place = place;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public Place getPlace() {
+      return place;
+    }
+  }
+
+  /**
+   * The cell used to render a {@link MainMenuItem}.
+   */
+  private static class MainMenuItemCell extends AbstractCell<MainMenuItem> {
+
+    @Override
+    public void render(com.google.gwt.cell.client.Cell.Context context, MainMenuItem value,
+        SafeHtmlBuilder sb) {
+      if (value == null) {
+        return;
+      }
+      sb.appendEscaped(value.getName());
+    }
+  }
+
+  /**
+   * The URL attribute that determines whether or not to include the pie chart.
+   */
+  private static final String CHART_URL_ATTRIBUTE = "chart";
+
+  private static MobileWebAppShellDesktopUiBinder uiBinder = GWT
+      .create(MobileWebAppShellDesktopUiBinder.class);
+
+  /**
+   * The main menu list.
+   */
+  @UiField(provided = true)
+  CellList<MainMenuItem> mainMenu;
+
+  /**
+   * The container that holds content.
+   */
+  @UiField
+  DeckLayoutPanel contentContainer;
+
+  @UiField
+  DockLayoutPanel leftNav;
+
+  /**
+   * The container that holds the pie chart.
+   */
+  @UiField
+  HasOneWidget pieChartContainer;
+
+  /**
+   * A boolean indicating that we have not yet seen the first content widget.
+   */
+  private boolean firstContentWidget = true;
+
+  /**
+   * A pie chart showing a snapshot of the tasks.
+   */
+  private PieChart pieChart;
+
+  /**
+   * Construct a new {@link MobileWebAppShellDesktop}.
+   * 
+   * @param clientFactory the {@link ClientFactory} of shared resources
+   */
+  public MobileWebAppShellDesktop(final ClientFactory clientFactory) {
+    // Initialize the main menu.
+    Resources resources = GWT.create(Resources.class);
+    mainMenu = new CellList<MainMenuItem>(new MainMenuItemCell(), resources);
+    mainMenu.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
+
+    // We don't expect to have more than 30 menu items.
+    mainMenu.setVisibleRange(0, 30);
+
+    // Add items to the main menu.
+    final List<MainMenuItem> menuItems = new ArrayList<MainMenuItem>();
+    menuItems.add(new MainMenuItem("Task List", new TaskListPlace(false)));
+    menuItems.add(new MainMenuItem("Add Task", TaskEditPlace.getTaskCreatePlace()));
+    mainMenu.setRowData(menuItems);
+
+    // Choose a place when a menu item is selected.
+    final SingleSelectionModel<MainMenuItem> selectionModel =
+        new SingleSelectionModel<MainMenuItem>();
+    selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+      public void onSelectionChange(SelectionChangeEvent event) {
+        MainMenuItem selected = selectionModel.getSelectedObject();
+        if (selected != null) {
+          clientFactory.getPlaceController().goTo(selected.getPlace());
+        }
+      }
+    });
+    mainMenu.setSelectionModel(selectionModel);
+
+    // Update selection based on the current place.
+    clientFactory.getEventBus().addHandler(PlaceChangeEvent.TYPE, new PlaceChangeEvent.Handler() {
+      public void onPlaceChange(PlaceChangeEvent event) {
+        Place place = event.getNewPlace();
+        for (MainMenuItem menuItem : menuItems) {
+          if (place == menuItem.getPlace()) {
+            // We found a match in the main menu.
+            selectionModel.setSelected(menuItem, true);
+            return;
+          }
+        }
+
+        // We didn't find a match in the main menu.
+        selectionModel.setSelected(null, true);
+      }
+    });
+
+    // Initialize this widget.
+    initWidget(uiBinder.createAndBindUi(this));
+
+    // Initialize the pie chart.
+    pieChart = PieChart.createIfSupported();
+    String chartUrlValue = Window.Location.getParameter(CHART_URL_ATTRIBUTE);
+    if (chartUrlValue != null && chartUrlValue.startsWith("f")) {
+      // Chart manually disabled.
+      leftNav.remove(1); // Pie Chart.
+      leftNav.remove(0); // Pie chart legend.
+    } else if (pieChart == null) {
+      // Chart not supported.
+      pieChartContainer.setWidget(new Label("Try upgrading to a modern browser to enable charts."));
+    } else {
+      // Chart supported.
+      pieChart.setWidth("90%");
+      pieChart.setHeight("90%");
+      pieChart.getElement().getStyle().setMarginLeft(5.0, Unit.PCT);
+      pieChart.getElement().getStyle().setMarginTop(5.0, Unit.PCT);
+      pieChartContainer.setWidget(pieChart);
+    }
+
+    // Initialize the add button.
+    setAddButtonHandler(null);
+
+    /*
+     * Add both views to the DeckLayoutPanel so we can animate between them.
+     * Using a DeckLayoutPanel here works because we only have two views, and we
+     * always know that the edit view should animate in from the right side of
+     * the screen. A more complex app will require more complex logic to figure
+     * out which direction to animate.
+     */
+    contentContainer.add(clientFactory.getTaskListView());
+    contentContainer.add(clientFactory.getTaskEditView());
+    contentContainer.setAnimationDuration(800);
+
+    // Listen for events from the task list activity.
+    clientFactory.getEventBus().addHandler(TaskListUpdateEvent.getType(),
+        new TaskListActivity.TaskListUpdateHandler() {
+          public void onTaskListUpdated(TaskListUpdateEvent event) {
+            updatePieChart(event.getTasks());
+          }
+        });
+  }
+
+  public boolean isTaskListIncluded() {
+    return false;
+  }
+
+  /**
+   * Set the handler to invoke when the add button is pressed. If no handler is
+   * specified, the button is hidden.
+   * 
+   * @param handler the handler to add to the button, or null to hide
+   */
+  public void setAddButtonHandler(ClickHandler handler) {
+    // No-op: Adding a task is handled in the main menu.
+  }
+
+  /**
+   * Set the widget to display in content area.
+   * 
+   * @param content the {@link Widget} to display
+   */
+  public void setWidget(IsWidget content) {
+    contentContainer.setWidget(content);
+
+    // Do not animate the first time we show a widget.
+    if (firstContentWidget) {
+      firstContentWidget = false;
+      contentContainer.animate(0);
+    }
+  }
+
+  /**
+   * Update the pie chart with the list of tasks.
+   * 
+   * @param tasks the list of tasks
+   */
+  @SuppressWarnings("deprecation")
+  private void updatePieChart(List<TaskProxy> tasks) {
+    if (pieChart == null) {
+      return;
+    }
+
+    // Calculate the slices based on the due date.
+    double pastDue = 0;
+    double dueSoon = 0;
+    double onTime = 0;
+    double noDate = 0;
+    final Date now = new Date();
+    final Date tomorrow = new Date(now.getYear(), now.getMonth(), now.getDate() + 1, 23, 59, 59);
+    for (TaskProxy task : tasks) {
+      Date dueDate = task.getDueDate();
+      if (dueDate == null) {
+        noDate++;
+      } else if (dueDate.before(now)) {
+        pastDue++;
+      } else if (dueDate.before(tomorrow)) {
+        dueSoon++;
+      } else {
+        onTime++;
+      }
+    }
+
+    // Update the pie chart.
+    pieChart.clearSlices();
+    pieChart.addSlice(pastDue, CssColor.make(255, 100, 100));
+    pieChart.addSlice(dueSoon, CssColor.make(255, 200, 100));
+    pieChart.addSlice(onTime, CssColor.make(100, 255, 100));
+    pieChart.addSlice(noDate, CssColor.make(200, 200, 200));
+    pieChart.redraw();
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.ui.xml
new file mode 100644
index 0000000..0d06261
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.ui.xml
@@ -0,0 +1,155 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+  xmlns:ui="urn:ui:com.google.gwt.uibinder"
+  xmlns:a="urn:import:com.google.gwt.sample.mobilewebapp.client.activity"
+  xmlns:g="urn:import:com.google.gwt.user.client.ui"
+  xmlns:c="urn:import:com.google.gwt.user.cellview.client"
+  xmlns:d="urn:import:com.google.gwt.sample.mobilewebapp.client.desktop">
+
+  <ui:style>
+    .header {
+      border-bottom: 1px solid #666;
+      padding-top: 6px;
+      padding-left: 20px;
+    }
+    
+    .headerWord {
+      font-size: 20pt;
+      font-weight: bold;
+      color: #889;
+      text-shadow: 5px 5px 6px #aaa;
+      -moz-transform: matrix(1, -0.04, 0, 1, 0pt, 0pt);
+      -webkit-transform: matrix(1, -0.04, 0, 1, 0pt, 0pt);
+      padding-right: 14px;
+    }
+    
+    .leftNav {
+      border-right: 1px solid #666;
+    }
+    
+    .mainMenuContainer {
+      background: #eee;
+      border-bottom: 1px solid #666;
+    }
+    
+    .legendColor {
+      height: 1em;
+      width: 1em;
+      margin-left: 5px;
+    }
+    
+    .legendText {
+      font-size: 80%
+    }
+  </ui:style>
+
+  <g:DockLayoutPanel
+    unit="PT">
+
+    <!-- Header -->
+    <g:north
+      size="40">
+      <g:HTMLPanel
+        addStyleNames="{style.header}">
+        <table>
+          <tr>
+            <td>
+              <div
+                class="{style.headerWord}">Task</div>
+            </td>
+            <td>
+              <div
+                class="{style.headerWord}">Manager</div>
+            </td>
+          </tr>
+        </table>
+      </g:HTMLPanel>
+    </g:north>
+
+
+    <g:center>
+      <g:DockLayoutPanel
+        unit="EM">
+
+        <!-- Left Nav. -->
+        <g:west
+          size="20">
+          <g:DockLayoutPanel
+            unit="EM"
+            ui:field="leftNav"
+            addStyleNames="{style.leftNav}">
+            <!-- Main Menu. -->
+            <g:center>
+              <g:ScrollPanel
+                addStyleNames="{style.mainMenuContainer}">
+                <c:CellList
+                  ui:field="mainMenu" />
+              </g:ScrollPanel>
+            </g:center>
+
+            <!-- Pie Chart Legand. -->
+            <g:south
+              size="6">
+              <g:HTMLPanel>
+                <table
+                  cellspacing="0"
+                  cellpadding="2">
+                  <tr>
+                    <td>
+                      <div
+                        style="background: #faa"
+                        class="{style.legendColor}"></div>
+                    </td>
+                    <td
+                      class="{style.legendText}">Past Due</td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <div
+                        style="background: #fca"
+                        class="{style.legendColor}"></div>
+                    </td>
+                    <td
+                      class="{style.legendText}">Due Tomorrow</td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <div
+                        style="background: #afa"
+                        class="{style.legendColor}"></div>
+                    </td>
+                    <td
+                      class="{style.legendText}">On Time</td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <div
+                        style="background: #aaa"
+                        class="{style.legendColor}"></div>
+                    </td>
+                    <td
+                      class="{style.legendText}">No Due Date</td>
+                  </tr>
+                </table>
+              </g:HTMLPanel>
+            </g:south>
+
+            <!-- Pie Chart. -->
+            <g:south
+              size="20">
+              <g:SimpleLayoutPanel
+                ui:field="pieChartContainer" />
+            </g:south>
+          </g:DockLayoutPanel>
+        </g:west>
+
+        <!-- Content. -->
+        <g:center>
+          <g:DeckLayoutPanel
+            ui:field="contentContainer" />
+        </g:center>
+      </g:DockLayoutPanel>
+    </g:center>
+
+  </g:DockLayoutPanel>
+</ui:UiBinder> 
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/PieChart.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/PieChart.java
new file mode 100644
index 0000000..679cd75
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/desktop/PieChart.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.desktop;
+
+import com.google.gwt.canvas.client.Canvas;
+import com.google.gwt.canvas.dom.client.Context2d;
+import com.google.gwt.canvas.dom.client.FillStrokeStyle;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.PartialSupport;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.RequiresResize;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A pie chart representation of data.
+ */
+@PartialSupport
+public class PieChart extends Composite implements RequiresResize {
+
+  /**
+   * Information about a slice of pie.
+   */
+  private static class Slice {
+    private final double weight;
+    private final FillStrokeStyle fill;
+
+    public Slice(double weight, FillStrokeStyle fill) {
+      this.weight = weight;
+      this.fill = fill;
+    }
+  }
+
+  /**
+   * The number of radians in a circle.
+   */
+  private static final double RADIANS_IN_CIRCLE = 2 * Math.PI;
+
+  /**
+   * Return a new {@link Canvas} if supported, and null otherwise.
+   * 
+   * @return a new {@link Canvas} if supported, and null otherwise
+   */
+  public static PieChart createIfSupported() {
+    return isSupported() ? new PieChart() : null;
+  }
+
+  /**
+   * Runtime check for whether the canvas element is supported in this browser.
+   * 
+   * @return whether the canvas element is supported
+   */
+  public static boolean isSupported() {
+    return Canvas.isSupported();
+  }
+
+  private final Canvas canvas;
+  private final List<Slice> slices = new ArrayList<Slice>();
+
+  /**
+   * Create using factory methods.
+   */
+  private PieChart() {
+    canvas = Canvas.createIfSupported();
+    canvas.setCoordinateSpaceHeight(300);
+    canvas.setCoordinateSpaceWidth(300);
+    initWidget(canvas);
+  }
+
+  /**
+   * Add a slice to the chart.
+   * 
+   * @param weight the weight of the slice
+   * @param fill the fill color
+   */
+  public void addSlice(double weight, FillStrokeStyle fill) {
+    slices.add(new Slice(weight, fill));
+  }
+
+  /**
+   * Clear all slices.
+   */
+  public void clearSlices() {
+    slices.clear();
+  }
+
+  public void onResize() {
+    redraw();
+  }
+
+  /**
+   * Redraw the pie chart.
+   */
+  public void redraw() {
+    if (!isAttached()) {
+      return;
+    }
+
+    // Get the dimensions of the chart.
+    int width = canvas.getCoordinateSpaceWidth();
+    int height = canvas.getCoordinateSpaceHeight();
+    double radius = Math.min(width, height) / 2.0;
+    double cx = width / 2.0;
+    double cy = height / 2.0;
+
+    // Clear the context.
+    Context2d context = canvas.getContext2d();
+    context.clearRect(0, 0, width, height);
+
+    // Get the total weight of all slices.
+    double totalWeight = 0;
+    for (Slice slice : slices) {
+      totalWeight += slice.weight;
+    }
+
+    // Draw the slices.
+    double startAngle = -0.5 * Math.PI;
+    for (Slice slice : slices) {
+      double weight = slice.weight / totalWeight;
+      double endAngle = startAngle + (weight * RADIANS_IN_CIRCLE);
+      context.setFillStyle(slice.fill);
+      context.beginPath();
+      context.moveTo(cx, cy);
+      context.arc(cx, cy, radius, startAngle, endAngle);
+      context.fill();
+      startAngle = endAngle;
+    }
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      public void execute() {
+        redraw();
+      }
+    });
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileCellList.css b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileCellList.css
new file mode 100644
index 0000000..122da68
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileCellList.css
@@ -0,0 +1,6 @@
+.cellListEvenItem,.cellListOddItem {
+  border-bottom: 1px solid #bbb;
+  padding: 22px 10px;
+  font-size: 14pt;
+  color: #333;
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.java
new file mode 100644
index 0000000..a2341bd
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.mobile;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DecoratedPopupPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.TextBoxBase;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.datepicker.client.DatePicker;
+import com.google.gwt.widget.client.TextButton;
+
+import java.util.Date;
+
+/**
+ * View used to edit a task.
+ */
+public class MobileTaskEditView extends Composite implements TaskEditView {
+
+  /**
+   * The UiBinder interface.
+   */
+  interface MobileTaskEditViewUiBinder extends
+      UiBinder<Widget, MobileTaskEditView> {
+  }
+
+  /**
+   * The UiBinder used to generate the view.
+   */
+  private static MobileTaskEditViewUiBinder uiBinder = GWT.create(MobileTaskEditViewUiBinder.class);
+
+  /**
+   * The formatter used to format the date.
+   */
+  private static DateTimeFormat dateFormat = DateTimeFormat.getFormat("EE, MMM d, yyyy");
+
+  /**
+   * The glass panel used to lock the UI.
+   */
+  private static PopupPanel glassPanel;
+
+  /**
+   * Show or hide the glass panel used to lock the UI will the task loads.
+   * 
+   * @param visible true to show, false to hide
+   */
+  private static void setGlassPanelVisible(boolean visible) {
+    // Initialize the panel.
+    if (glassPanel == null) {
+      glassPanel = new DecoratedPopupPanel(false, true);
+      glassPanel.setWidget(new Label("Loading..."));
+    }
+
+    if (visible) {
+      // Show the loading panel.
+      glassPanel.center();
+    } else {
+      // Hide the loading panel.
+      glassPanel.hide();
+    }
+  }
+
+  /**
+   * The checkbox used to specify that the task should be added to the calendar.
+   */
+  @UiField
+  CheckBox calendarCheckbox;
+
+  /**
+   * The button used to select the date.
+   */
+  @UiField
+  TextButton dateButton;
+
+  /**
+   * The button used to delete a task or cancel changes.
+   */
+  @UiField
+  TextButton deleteButton;
+
+  /**
+   * The text box used to enter the task name.
+   */
+  @UiField
+  TextBoxBase nameBox;
+
+  /**
+   * The text box used to enter task notes.
+   */
+  @UiField
+  TextBoxBase notesBox;
+
+  /**
+   * The text box used to save changes or create a new task.
+   */
+  @UiField
+  TextButton saveButton;
+
+  /**
+   * The current date value.
+   */
+  private Date currentDate;
+
+  /**
+   * The popup panel that contains a date picker for selecting the date.
+   */
+  private final PopupPanel datePickerPopup;
+
+  /**
+   * The {@link Presenter} for this view.
+   */
+  private Presenter presenter;
+
+  /**
+   * Construct a new {@link MobileTaskEditView}.
+   * 
+   * @param presenter the {@link Presenter} that handles interactions
+   */
+  public MobileTaskEditView() {
+    initWidget(uiBinder.createAndBindUi(this));
+
+    // if this is a web app
+    // Create the datePickerPopup.
+    final DatePicker datePicker = new DatePicker();
+    datePickerPopup = new PopupPanel(true, true);
+    datePickerPopup.setWidget(datePicker);
+    datePickerPopup.setGlassEnabled(true);
+
+    /*
+     * When the user clicks on the date button, open a date picker so they can
+     * select a date.
+     */
+    dateButton.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        datePicker.setValue(currentDate, false);
+        datePickerPopup.center();
+      }
+    });
+    datePicker.addValueChangeHandler(new ValueChangeHandler<Date>() {
+      public void onValueChange(ValueChangeEvent<Date> event) {
+        setDueDate(event.getValue());
+        datePickerPopup.hide();
+      }
+    });
+
+    // Create a new task or modify the current task when done is pressed.
+    saveButton.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        if (presenter != null) {
+          presenter.saveTask(calendarCheckbox.getValue());
+        }
+      }
+    });
+
+    // Delete the current task or cancel when delete is pressed.
+    deleteButton.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        if (presenter != null) {
+          presenter.deleteTask();
+        }
+      }
+    });
+  }
+
+  public Date getDueDate() {
+    return currentDate;
+  }
+
+  public String getName() {
+    return nameBox.getValue();
+  }
+
+  public String getNotes() {
+    return notesBox.getValue();
+  }
+
+  public void setDueDate(Date date) {
+    boolean wasNull = (currentDate == null);
+    currentDate = date;
+    if (date == null) {
+      dateButton.setText("Set due date");
+      calendarCheckbox.setValue(false);
+      calendarCheckbox.setEnabled(false);
+    } else {
+      dateButton.setText(dateFormat.format(date));
+      calendarCheckbox.setEnabled(true);
+      if (wasNull) {
+        calendarCheckbox.setValue(true);
+      }
+    }
+  }
+
+  public void setEditing(boolean isEditing) {
+    if (isEditing) {
+      deleteButton.setText("Delete item");
+    } else {
+      deleteButton.setText("Cancel");
+    }
+  }
+
+  public void setLocked(boolean locked) {
+    setGlassPanelVisible(locked);
+  }
+
+  public void setName(String name) {
+    nameBox.setText(name);
+  }
+
+  public void setNotes(String notes) {
+    notesBox.setText(notes);
+  }
+
+  public void setPresenter(Presenter presenter) {
+    this.presenter = presenter;
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.ui.xml
new file mode 100644
index 0000000..d831f4e
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.ui.xml
@@ -0,0 +1,141 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+  xmlns:ui="urn:ui:com.google.gwt.uibinder"
+  xmlns:g="urn:import:com.google.gwt.user.client.ui"
+  xmlns:w="urn:import:com.google.gwt.widget.client">
+
+  <ui:style>
+    .outer {
+      background: #eee;
+    }
+    
+    .title {
+      background: #393939;
+      color: white;
+      padding: 4px 10px;
+      font-size: 10pt;
+    }
+    
+    .editForm {
+      padding: 10px;
+      background: white;
+    }
+    
+    .label {
+      color: #666;
+      font-size: 10pt;
+      padding-bottom: 3px;
+    }
+    
+    .field {
+      width: 100%;
+      margin-bottom: 12px;
+    }
+    
+    .textBoxWrapper {
+      margin-right: 10px;
+    }
+    
+    .checkbox {
+      color: #3f3f3f;
+    }
+    
+    .nameBox {
+      height: 2em;
+    }
+    
+    .notesBox {
+      height: 4em;
+    }
+    
+    .buttonPanel {
+      width: 100%;
+      padding: 10px;
+      margin-top: 15px;
+    }
+    
+    .button {
+      width: 100%;
+    }
+  </ui:style>
+
+  <g:DockLayoutPanel
+    unit="PT">
+    <!-- Title. -->
+    <g:north
+      size="18">
+      <g:Label
+        addStyleNames="{style.title}">DETAILS</g:Label>
+    </g:north>
+
+    <g:center>
+      <g:ScrollPanel
+        addStyleNames="{style.outer}">
+        <g:HTMLPanel>
+
+          <!-- Edit Form. -->
+          <div
+            class="{style.editForm}">
+            <!-- Task name. -->
+            <div
+              class="{style.label}">What</div>
+            <div
+              class="{style.textBoxWrapper}">
+              <g:TextArea
+                addStyleNames="{style.field} {style.nameBox}"
+                ui:field="nameBox" />
+            </div>
+
+            <!-- Task notes. -->
+            <div
+              class="{style.label}">Notes</div>
+            <div
+              class="{style.textBoxWrapper}">
+              <g:TextArea
+                addStyleNames="{style.field} {style.notesBox}"
+                ui:field="notesBox" />
+            </div>
+
+            <!-- Task due date. -->
+            <div
+              class="{style.label}">Due date</div>
+            <w:TextButton
+              addStyleNames="{style.field}"
+              ui:field="dateButton">Set due date</w:TextButton>
+
+            <!-- Add to calendar. -->
+            <g:CheckBox
+              addStyleNames="{style.checkbox}"
+              ui:field="calendarCheckbox">Add to my calender</g:CheckBox>
+          </div>
+
+          <!-- Button panel. -->
+          <table
+            class="{style.buttonPanel}"
+            cellspacing="0"
+            cellpadding="0">
+            <tr>
+              <td
+                align="center"
+                style="width:50%;padding-right:5px;">
+                <w:TextButton
+                  ui:field="saveButton"
+                  addStyleNames="{style.button}">Done</w:TextButton>
+              </td>
+              <td
+                align="center"
+                style="width:50%;padding-left:5px;">
+                <w:TextButton
+                  ui:field="deleteButton"
+                  decoration="NEGATIVE"
+                  addStyleNames="{style.button}">Delete Item</w:TextButton>
+              </td>
+            </tr>
+          </table>
+
+        </g:HTMLPanel>
+      </g:ScrollPanel>
+    </g:center>
+  </g:DockLayoutPanel>
+
+</ui:UiBinder> 
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.java
new file mode 100644
index 0000000..49bd93b
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.mobile;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.cellview.client.CellList;
+import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.NoSelectionModel;
+import com.google.gwt.view.client.SelectionChangeEvent;
+import com.google.gwt.view.client.SelectionModel;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.List;
+
+/**
+ * View used to display the list of Tasks.
+ */
+public class MobileTaskListView extends Composite implements TaskListView {
+
+  /**
+   * Resources used by the mobile CellList.
+   */
+  static interface CellListResources extends CellList.Resources {
+    @Source({CellList.Style.DEFAULT_CSS, "MobileCellList.css"})
+    CellListStyle cellListStyle();
+  }
+
+  /**
+   * Styles used by the mobile CellList.
+   */
+  static interface CellListStyle extends CellList.Style {
+  }
+
+  /**
+   * The UiBinder interface.
+   */
+  interface MobileTaskListViewUiBinder extends
+      UiBinder<Widget, MobileTaskListView> {
+  }
+
+  /**
+   * The UiBinder used to generate the view.
+   */
+  private static MobileTaskListViewUiBinder uiBinder = GWT
+      .create(MobileTaskListViewUiBinder.class);
+
+  /**
+   * Displays the list of tasks.
+   */
+  @UiField(provided = true)
+  CellList<TaskProxy> taskList;
+
+  /**
+   * The presenter for this view.
+   */
+  private Presenter presenter;
+
+  /**
+   * Construct a new {@link MobileTaskListView}.
+   * 
+   * @param presenter the {@link Presenter} that handles this view
+   */
+  public MobileTaskListView() {
+    // Create the CellList.
+    CellListResources cellListRes = GWT.create(CellListResources.class);
+    taskList = new CellList<TaskProxy>(new TaskProxyCell(), cellListRes);
+    taskList.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
+
+    /*
+     * Inform the presenter when the user selects a task from the task list. We
+     * use a NoSelectionModel because we don't want the task to remain selected,
+     * we just want to be notified of the selection event.
+     */
+    final NoSelectionModel<TaskProxy> selectionModel = new NoSelectionModel<TaskProxy>();
+    taskList.setSelectionModel(selectionModel);
+    selectionModel
+        .addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+          public void onSelectionChange(SelectionChangeEvent event) {
+            // Edit the task.
+            if (presenter != null) {
+              presenter.selectTask(selectionModel.getLastSelectedObject());
+            }
+          }
+        });
+
+    // Initialize the widget.
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  public void clearList() {
+    taskList.setVisibleRangeAndClearData(taskList.getVisibleRange(), true);
+  }
+
+  public void setPresenter(Presenter presenter) {
+    this.presenter = presenter;
+  }
+
+  public void setSelectionModel(SelectionModel<TaskProxy> selectionModel) {
+    taskList.setSelectionModel(selectionModel);
+  }
+
+  public void setTasks(List<TaskProxy> tasks) {
+    taskList.setRowData(tasks);
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.ui.xml
new file mode 100644
index 0000000..2dc3497
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.ui.xml
@@ -0,0 +1,16 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+  xmlns:ui="urn:ui:com.google.gwt.uibinder"
+  xmlns:c="urn:import:com.google.gwt.user.cellview.client"
+  xmlns:g="urn:import:com.google.gwt.user.client.ui">
+
+  <ui:style>
+    
+  </ui:style>
+
+  <g:ScrollPanel>
+    <c:CellList
+      ui:field="taskList" />
+  </g:ScrollPanel>
+
+</ui:UiBinder> 
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.java
new file mode 100644
index 0000000..04d0087
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.mobile;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.DeckLayoutPanel;
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.user.client.ui.LayoutPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.MobileWebAppShellBase;
+
+/**
+ * Mobile version of the UI shell.
+ */
+public class MobileWebAppShellMobile extends MobileWebAppShellBase {
+
+  interface MobileWebAppShellMobileUiBinder extends UiBinder<Widget, MobileWebAppShellMobile> {
+  }
+
+  private static MobileWebAppShellMobileUiBinder uiBinder = GWT
+      .create(MobileWebAppShellMobileUiBinder.class);
+
+  /**
+   * The width of the menu bar in landscape mode in EX.
+   */
+  private static final double LANDSCAPE_MENU_WIDTH_EX = 8.0;
+
+  /**
+   * The height of the menu bar in portrait mode in PT.
+   */
+  private static final double PORTRAIT_MENU_HEIGHT_PT = 28.0;
+
+  /**
+   * The button used to add items.
+   */
+  @UiField
+  Button addButton;
+
+  /**
+   * The widget that wraps the add button.
+   */
+  @UiField
+  Widget addButtonContainer;
+
+  /**
+   * The widget that wraps the back button.
+   */
+  @UiField
+  Widget backButtonContainer;
+
+  /**
+   * The panel that holds the current content.
+   */
+  @UiField
+  DeckLayoutPanel contentContainer;
+
+  /**
+   * The panel used for layout.
+   */
+  @UiField
+  LayoutPanel layoutPanel;
+
+  @UiField
+  Widget titleBar;
+
+  @UiField
+  Element titleElem;
+
+  /**
+   * A reference to the handler for the add button.
+   */
+  private HandlerRegistration addButtonHandler;
+
+  /**
+   * A boolean indicating that we have not yet seen the first content widget.
+   */
+  private boolean firstContentWidget = true;
+
+  /**
+   * Construct a new {@link MobileWebAppShellMobile}.
+   * 
+   * @param clientFactory the {@link ClientFactory} of shared resources
+   */
+  public MobileWebAppShellMobile(ClientFactory clientFactory) {
+    initWidget(uiBinder.createAndBindUi(this));
+
+    // Initialize the add button.
+    setAddButtonHandler(null);
+
+    /*
+     * Add both views to the DeckLayoutPanel so we can animate between them.
+     * Using a DeckLayoutPanel here works because we only have two views, and we
+     * always know that the edit view should animate in from the right side of
+     * the screen. A more complex app will require more complex logic to figure
+     * out which direction to animate.
+     */
+    contentContainer.add(clientFactory.getTaskListView());
+    contentContainer.add(clientFactory.getTaskEditView());
+    contentContainer.setAnimationDuration(500);
+  }
+
+  public boolean isTaskListIncluded() {
+    return false;
+  }
+
+  /**
+   * Set the handler to invoke when the add button is pressed. If no handler is
+   * specified, the button is hidden.
+   * 
+   * @param handler the handler to add to the button, or null to hide
+   */
+  public void setAddButtonHandler(ClickHandler handler) {
+    // Clear the old handler.
+    if (addButtonHandler != null) {
+      addButtonHandler.removeHandler();
+      addButtonHandler = null;
+    }
+
+    if (handler == null) {
+      // Hide the button.
+      addButton.setVisible(false);
+    } else {
+      // Show the button and add the handler.
+      addButton.setVisible(true);
+      addButtonHandler = addButton.addClickHandler(handler);
+    }
+  }
+
+  /**
+   * Set the widget to display in content area.
+   * 
+   * @param content the {@link Widget} to display
+   */
+  public void setWidget(IsWidget content) {
+    contentContainer.setWidget(content);
+
+    // Do not animate the first time we show a widget.
+    if (firstContentWidget) {
+      firstContentWidget = false;
+      contentContainer.animate(0);
+    }
+  }
+
+  @Override
+  protected void adjustOrientation(boolean isPortrait) {
+    if (isPortrait) {
+      // Portrait.
+      layoutPanel.setWidgetTopHeight(titleBar, 0, Unit.PX, PORTRAIT_MENU_HEIGHT_PT, Unit.PT);
+      layoutPanel.setWidgetLeftRight(titleBar, 0, Unit.PX, 0, Unit.PX);
+      titleElem.getStyle().clearDisplay();
+
+      layoutPanel
+          .setWidgetTopBottom(contentContainer, PORTRAIT_MENU_HEIGHT_PT, Unit.PT, 0, Unit.PX);
+      layoutPanel.setWidgetLeftRight(contentContainer, 0, Unit.EX, 0, Unit.PX);
+
+      layoutPanel.setWidgetTopHeight(addButtonContainer, 0, Unit.PX, PORTRAIT_MENU_HEIGHT_PT,
+          Unit.PT);
+      layoutPanel.setWidgetRightWidth(addButtonContainer, 8, Unit.PX, 3, Unit.EX);
+
+      layoutPanel.setWidgetTopHeight(backButtonContainer, 0, Unit.PX, PORTRAIT_MENU_HEIGHT_PT,
+          Unit.PT);
+      layoutPanel.setWidgetLeftWidth(backButtonContainer, 8, Unit.PX, 6, Unit.EX);
+    } else {
+      // Landscape.
+      layoutPanel.setWidgetTopBottom(titleBar, 0, Unit.PX, 0, Unit.PX);
+      layoutPanel.setWidgetLeftWidth(titleBar, 0, Unit.PX, LANDSCAPE_MENU_WIDTH_EX, Unit.EX);
+      titleElem.getStyle().setDisplay(Display.NONE);
+
+      layoutPanel.setWidgetTopBottom(contentContainer, 0, Unit.PX, 0, Unit.PX);
+      layoutPanel
+          .setWidgetLeftRight(contentContainer, LANDSCAPE_MENU_WIDTH_EX, Unit.EX, 0, Unit.PX);
+
+      layoutPanel.setWidgetTopHeight(addButtonContainer, 5, Unit.PX, 4, Unit.EX);
+      layoutPanel.setWidgetLeftWidth(addButtonContainer, 0, Unit.PX, LANDSCAPE_MENU_WIDTH_EX,
+          Unit.EX);
+
+      layoutPanel.setWidgetBottomHeight(backButtonContainer, 5, Unit.PX, 4, Unit.EX);
+      layoutPanel.setWidgetLeftWidth(backButtonContainer, 0, Unit.PX, LANDSCAPE_MENU_WIDTH_EX,
+          Unit.EX);
+    }
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.ui.xml
new file mode 100644
index 0000000..f25a9b3
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.ui.xml
@@ -0,0 +1,85 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+  xmlns:ui="urn:ui:com.google.gwt.uibinder"
+  xmlns:g="urn:import:com.google.gwt.user.client.ui"
+  xmlns:c="urn:import:com.google.gwt.user.cellview.client">
+
+  <ui:style>
+    .backButton {
+      display: none;
+    }
+    
+    .header {
+      background: #1a1a1a;
+      color: white;
+      font-size: 18pt;
+      line-height: 28pt;
+      text-align: center;
+      font-weight: bold;
+    }
+    
+    .addButton {
+      color: white;
+      font-size: 18pt;
+      background: none;
+      border: none;
+      text-align: right;
+      font-weight: bold;
+    }
+  </ui:style>
+
+  <g:LayoutPanel
+    ui:field='layoutPanel'>
+
+    <!-- Title -->
+    <g:layer
+      top='0px'
+      height='28pt'
+      left='0px'
+      right='0px'>
+      <g:HTMLPanel
+        ui:field='titleBar'
+        addStyleNames="{style.header}">
+        <div
+          ui:field='titleElem'>Task List</div>
+      </g:HTMLPanel>
+    </g:layer>
+
+    <!-- Back button. -->
+    <g:layer
+      top='0px'
+      height='28pt'
+      left='0px'
+      width='6ex'>
+      <g:HTMLPanel
+        ui:field='backButtonContainer'>
+        <g:Button
+          styleName="{style.backButton}">Back</g:Button>
+      </g:HTMLPanel>
+    </g:layer>
+
+    <!-- Add button. -->
+    <g:layer
+      top="0px"
+      height="28pt"
+      right="8px"
+      width="3ex">
+      <g:HTMLPanel
+        ui:field='addButtonContainer'>
+        <g:Button
+          ui:field="addButton"
+          styleName="{style.addButton}">+</g:Button>
+      </g:HTMLPanel>
+    </g:layer>
+
+    <!-- Content -->
+    <g:layer
+      top='28pt'
+      bottom='0px'
+      left='0px'
+      right='0px'>
+      <g:DeckLayoutPanel
+        ui:field="contentContainer" />
+    </g:layer>
+  </g:LayoutPanel>
+</ui:UiBinder> 
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/TaskProxyCell.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/TaskProxyCell.java
new file mode 100644
index 0000000..d1d4292
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/mobile/TaskProxyCell.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.mobile;
+
+import com.google.gwt.cell.client.AbstractCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
+import com.google.gwt.safehtml.client.SafeHtmlTemplates;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.Date;
+
+/**
+ * A {@link com.google.gwt.cell.client.Cell} used to render a {@link TaskProxy}.
+ */
+public class TaskProxyCell extends AbstractCell<TaskProxy> {
+
+  /**
+   * The template used by this cell.
+   * 
+   */
+  interface Template extends SafeHtmlTemplates {
+    @SafeHtmlTemplates.Template("{0}<div style=\"font-size:80%;\">&nbsp;</div>")
+    SafeHtml noDate(String name);
+
+    @SafeHtmlTemplates.Template("{0}<div style=\"font-size:80%;color:#999;\">Due: {1}</div>")
+    SafeHtml onTime(String name, String date);
+
+    @SafeHtmlTemplates.Template("{0}<div style=\"font-size:80%;color:red;\">Due: {1}</div>")
+    SafeHtml pastDue(String name, String date);
+  }
+
+  private static Template template;
+  private final DateTimeFormat dateFormat = DateTimeFormat.getFormat(PredefinedFormat.DATE_LONG);
+
+  public TaskProxyCell() {
+    if (template == null) {
+      template = GWT.create(Template.class);
+    }
+  }
+
+  @Override
+  @SuppressWarnings("deprecation")
+  public void render(com.google.gwt.cell.client.Cell.Context context, TaskProxy value,
+      SafeHtmlBuilder sb) {
+    if (value == null) {
+      return;
+    }
+
+    Date date = value.getDueDate();
+    Date today = new Date();
+    today.setHours(0);
+    today.setMinutes(0);
+    today.setSeconds(0);
+    if (date == null) {
+      sb.append(template.noDate(value.getName()));
+    } else if (date.before(today)) {
+      sb.append(template.pastDue(value.getName(), dateFormat.format(date)));
+    } else {
+      sb.append(template.onTime(value.getName(), dateFormat.format(date)));
+    }
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/AppPlaceHistoryMapper.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/AppPlaceHistoryMapper.java
new file mode 100644
index 0000000..cc48944
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/AppPlaceHistoryMapper.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * 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.gwt.sample.mobilewebapp.client.place;
+
+import com.google.gwt.place.shared.PlaceHistoryMapper;
+import com.google.gwt.place.shared.WithTokenizers;
+
+/**
+ * This interface is the hub of your application's navigation system. It links
+ * the {@link com.google.gwt.place.shared.Place Place}s your user navigates to
+ * with the browser history system &mdash; that is, it makes the browser's back
+ * and forth buttons work for you, and also makes each spot in your app
+ * bookmarkable.
+ */
+@WithTokenizers({TaskListPlace.Tokenizer.class, TaskEditPlace.Tokenizer.class})
+public interface AppPlaceHistoryMapper extends PlaceHistoryMapper {
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskEditPlace.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskEditPlace.java
new file mode 100644
index 0000000..025654f
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskEditPlace.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.place;
+
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.place.shared.PlaceTokenizer;
+
+/**
+ * The place in the app that show a task in an editable view.
+ */
+public class TaskEditPlace extends Place {
+
+  /**
+   * The tokenizer for this place.
+   */
+  public static class Tokenizer implements PlaceTokenizer<TaskEditPlace> {
+
+    private static final String NO_ID = "create";
+
+    public TaskEditPlace getPlace(String token) {
+      try {
+        // Parse the task ID from the URL.
+        Long taskId = Long.parseLong(token);
+        return new TaskEditPlace(taskId);
+      } catch (NumberFormatException e) {
+        // If the ID cannot be parsed, assume we are creating a task.
+        return new TaskEditPlace(null);
+      }
+    }
+
+    public String getToken(TaskEditPlace place) {
+      Long taskId = place.getTaskId();
+      return (taskId == null) ? NO_ID : taskId.toString();
+    }
+  }
+
+  /**
+   * The singleton instance of this place used for creation.
+   */
+  private static TaskEditPlace singleton;
+
+  /**
+   * Create an instance of {@link TaskEditPlace} associated with the specified
+   * task ID.
+   * 
+   * @param taskId the ID of the task to edit
+   * @return the place
+   */
+  public static TaskEditPlace createTaskEditPlace(Long taskId) {
+    return new TaskEditPlace(taskId);
+  }
+
+  /**
+   * Get the singleton instance of the {@link TaskEditPlace} used to create a
+   * new task.
+   * 
+   * @return the place
+   */
+  public static TaskEditPlace getTaskCreatePlace() {
+    if (singleton == null) {
+      singleton = new TaskEditPlace(null);
+    }
+    return singleton;
+  }
+
+  private final Long taskId;
+
+  /**
+   * Construct a new {@link TaskEditPlace} for the specified task id.
+   * 
+   * @param taskId the ID of the task to edit
+   */
+  private TaskEditPlace(Long taskId) {
+    this.taskId = taskId;
+  }
+
+  /**
+   * Get the ID of the task to edit.
+   * 
+   * @return the ID of the task, or null if creating a new task
+   */
+  public Long getTaskId() {
+    return taskId;
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskListPlace.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskListPlace.java
new file mode 100644
index 0000000..8f9f539
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/place/TaskListPlace.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.place;
+
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.place.shared.PlaceTokenizer;
+
+/**
+ * The place in the app that shows a list of tasks.
+ */
+public class TaskListPlace extends Place {
+
+  /**
+   * The tokenizer for this place. TaskList doesn't have any state, so we don't
+   * have anything to encode.
+   */
+  public static class Tokenizer implements PlaceTokenizer<TaskListPlace> {
+
+    public TaskListPlace getPlace(String token) {
+      return new TaskListPlace(true);
+    }
+
+    public String getToken(TaskListPlace place) {
+      return "";
+    }
+  }
+
+  private final boolean taskListStale;
+
+  /**
+   * Construct a new {@link TaskListPlace}.
+   * 
+   * @param taskListStale true if the task list is stale and should be cleared
+   */
+  public TaskListPlace(boolean taskListStale) {
+    this.taskListStale = taskListStale;
+  }
+
+  /**
+   * Check if the task list is stale and should be cleared.
+   * 
+   * @return true if stale, false if not
+   */
+  public boolean isTaskListStale() {
+    return taskListStale;
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.java
new file mode 100644
index 0000000..966abbe
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.tablet;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.DeckLayoutPanel;
+import com.google.gwt.user.client.ui.DockLayoutPanel;
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.MobileWebAppShellBase;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskListActivity;
+import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+
+/**
+ * Tablet version of the UI shell.
+ */
+public class MobileWebAppShellTablet extends MobileWebAppShellBase {
+
+  interface MobileWebAppShellTabletUiBinder extends UiBinder<Widget, MobileWebAppShellTablet> {
+  }
+
+  private static MobileWebAppShellTabletUiBinder uiBinder = GWT
+      .create(MobileWebAppShellTabletUiBinder.class);
+
+  /**
+   * The width of the task list in landscape mode in PCT.
+   */
+  private static final double LANDSCAPE_TASK_LIST_WIDTH_PCT = 30.0;
+
+  /**
+   * The button used to add items.
+   */
+  @UiField
+  Button addButton;
+
+  /**
+   * The container that holds content.
+   */
+  @UiField
+  DeckLayoutPanel contentContainer;
+
+  /**
+   * The widget displayed when the user has not selected a task.
+   */
+  @UiField
+  Widget contentEmptyMessage;
+
+  /**
+   * The DockLayoutPanel that splits the task list and the task edit views.
+   */
+  @UiField
+  DockLayoutPanel splitPanel;
+
+  /**
+   * The container that holds the tast list.
+   */
+  @UiField
+  SimplePanel taskListContainer;
+
+  /**
+   * A reference to the handler for the add button.
+   */
+  private HandlerRegistration addButtonHandler;
+
+  /**
+   * The {@link ClientFactory} of shared resources.
+   */
+  private final ClientFactory clientFactory;
+
+  /**
+   * A boolean indicating that we have not yet seen the first content widget.
+   */
+  private boolean firstContentWidget = true;
+
+  /**
+   * The main task list, which is always visible.
+   */
+  private TaskListActivity taskListActivity;
+
+  /**
+   * Construct a new {@link MobileWebAppShellTablet}.
+   * 
+   * @param clientFactory the {@link ClientFactory} of shared resources
+   */
+  public MobileWebAppShellTablet(final ClientFactory clientFactory) {
+    this.clientFactory = clientFactory;
+
+    // Inject the tablet specific styles.
+    TabletResources resources = GWT.create(TabletResources.class);
+    resources.tabletStyles().ensureInjected();
+
+    // Initialize this widget.
+    initWidget(uiBinder.createAndBindUi(this));
+
+    // Initialize the add button.
+    setAddButtonHandler(null);
+  }
+
+  public boolean isTaskListIncluded() {
+    return !isOrientationPortrait();
+  }
+
+  /**
+   * Set the handler to invoke when the add button is pressed. If no handler is
+   * specified, the button is hidden.
+   * 
+   * @param handler the handler to add to the button, or null to hide
+   */
+  public void setAddButtonHandler(ClickHandler handler) {
+    // Clear the old handler.
+    if (addButtonHandler != null) {
+      addButtonHandler.removeHandler();
+      addButtonHandler = null;
+    }
+
+    if (handler == null) {
+      // Hide the button.
+      addButton.setVisible(false);
+    } else {
+      // Show the button and add the handler.
+      addButton.setVisible(true);
+      addButtonHandler = addButton.addClickHandler(handler);
+    }
+  }
+
+  /**
+   * Set the widget to display in content area.
+   * 
+   * @param content the {@link Widget} to display
+   */
+  public void setWidget(IsWidget content) {
+    contentContainer.setWidget((content == null) ? contentEmptyMessage : content);
+
+    // Do not animate the first time we show a widget.
+    if (firstContentWidget) {
+      firstContentWidget = false;
+      contentContainer.animate(0);
+    }
+  }
+
+  @Override
+  protected void adjustOrientation(boolean isPortrait) {
+    if (isPortrait) {
+      // Hide the static task list view.
+      if (taskListActivity != null) {
+        taskListActivity.onStop();
+        taskListActivity = null;
+      }
+      splitPanel.setWidgetSize(taskListContainer, 0);
+
+      /*
+       * Add both views to the DeckLayoutPanel so we can animate between them.
+       * Using a DeckLayoutPanel here works because we only have two views, and
+       * we always know that the edit view should animate in from the right side
+       * of the screen. A more complex app will require more complex logic to
+       * figure out which direction to animate.
+       */
+      contentContainer.insert(clientFactory.getTaskListView(), 0);
+      contentContainer.setAnimationDuration(500);
+
+      // Ensure that something is displayed.
+      Widget curWidget = contentContainer.getVisibleWidget();
+      if (curWidget == null || curWidget == contentEmptyMessage) {
+        clientFactory.getPlaceController().goTo(new TaskListPlace(false));
+        contentContainer.animate(0);
+      }
+    } else {
+      // Show the static task list view.
+      splitPanel.setWidgetSize(taskListContainer, LANDSCAPE_TASK_LIST_WIDTH_PCT);
+      if (taskListActivity == null) {
+        taskListActivity = new TaskListActivity(clientFactory, false);
+        taskListActivity.start(taskListContainer, clientFactory.getEventBus());
+
+        // DeckLayoutPanel sets the display to none, so we need to clear it.
+        clientFactory.getTaskListView().asWidget().getElement().getStyle().clearDisplay();
+      }
+
+      // Do not use animations when the task list is always visible.
+      contentContainer.setAnimationDuration(0);
+
+      // Ensure that the task list view is not displayed as content.
+      if (contentContainer.getVisibleWidget() == null) {
+        contentContainer.setWidget(contentEmptyMessage);
+      }
+    }
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.ui.xml
new file mode 100644
index 0000000..a29c961
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.ui.xml
@@ -0,0 +1,106 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+  xmlns:ui="urn:ui:com.google.gwt.uibinder"
+  xmlns:a="urn:import:com.google.gwt.sample.mobilewebapp.client.activity"
+  xmlns:g="urn:import:com.google.gwt.user.client.ui"
+  xmlns:c="urn:import:com.google.gwt.user.cellview.client">
+
+  <ui:style>
+    .header {
+      background: #1a1a1a;
+      position: relative;
+    }
+    
+    .headerText {
+      color: white;
+      font-size: 18pt;
+      line-height: 28pt;
+      text-align: center;
+      font-weight: bold;
+    }
+    
+    .addButtonContainer {
+      position: absolute;
+      top: 5px;
+      right: 3px;
+    }
+    
+    .addButton {
+      color: white;
+      font-size: 18pt;
+      background: none;
+      border: none;
+      text-align: right;
+      font-weight: bold;
+    }
+    
+    .taskList {
+      border-right: 4px solid #eee;
+    }
+    
+    .contentEmptyMessage {
+      padding: 10px;
+      font-size: 28pt;
+    }
+  </ui:style>
+
+  <g:DockLayoutPanel
+    unit="PT">
+
+    <!-- Header -->
+    <g:north
+      size="32">
+      <g:HTMLPanel
+        addStyleNames="{style.header}">
+        <table
+          style="width:100%;height:100%">
+          <tr>
+            <td
+              align="right"
+              valign="middle">
+              <g:Label
+                addStyleNames="{style.headerText}">
+                App Name
+              </g:Label>
+            </td>
+          </tr>
+        </table>
+
+        <!-- Add Button. -->
+        <div
+          class="{style.addButtonContainer}">
+          <g:Button
+            ui:field="addButton"
+            styleName="{style.addButton}">+</g:Button>
+        </div>
+      </g:HTMLPanel>
+    </g:north>
+
+    <g:center>
+      <g:DockLayoutPanel
+        unit="PCT"
+        ui:field="splitPanel">
+
+        <!-- Task List. -->
+        <g:west
+          size="30">
+          <g:SimpleLayoutPanel
+            addStyleNames="{style.taskList}"
+            ui:field="taskListContainer" />
+        </g:west>
+
+        <!-- Content. -->
+        <g:center>
+          <g:DeckLayoutPanel
+            ui:field="contentContainer">
+            <g:HTML
+              ui:field="contentEmptyMessage"
+              addStyleNames="{style.contentEmptyMessage}">
+              &laquo; Select a task from the list</g:HTML>
+          </g:DeckLayoutPanel>
+        </g:center>
+      </g:DockLayoutPanel>
+    </g:center>
+
+  </g:DockLayoutPanel>
+</ui:UiBinder> 
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletResources.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletResources.java
new file mode 100644
index 0000000..53d4f54
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletResources.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.tablet;
+
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.resources.client.CssResource.NotStrict;
+
+/**
+ * Resources used by the tablet version of the app.
+ */
+public interface TabletResources extends ClientBundle {
+
+  /**
+   * Style overrides specific to the tablet version.
+   */
+  @NotStrict
+  CssResource tabletStyles();
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.java
new file mode 100644
index 0000000..1e29850
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.client.tablet;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DecoratedPopupPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.TextBoxBase;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.datepicker.client.DatePicker;
+import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
+
+import java.util.Date;
+
+/**
+ * View used to edit a task.
+ */
+public class TabletTaskEditView extends Composite implements TaskEditView {
+
+  /**
+   * The UiBinder interface.
+   */
+  interface TabletTaskEditViewUiBinder extends UiBinder<Widget, TabletTaskEditView> {
+  }
+
+  /**
+   * The UiBinder used to generate the view.
+   */
+  private static TabletTaskEditViewUiBinder uiBinder = GWT.create(TabletTaskEditViewUiBinder.class);
+
+  /**
+   * The formatter used to format the date.
+   */
+  private static DateTimeFormat dateFormat = DateTimeFormat.getFormat("EE, MMM d, yyyy");
+
+  /**
+   * The glass panel used to lock the UI.
+   */
+  private static PopupPanel glassPanel;
+
+  /**
+   * Show or hide the glass panel used to lock the UI will the task loads.
+   * 
+   * @param visible true to show, false to hide
+   */
+  private static void setGlassPanelVisible(boolean visible) {
+    // Initialize the panel.
+    if (glassPanel == null) {
+      glassPanel = new DecoratedPopupPanel(false, true);
+      glassPanel.setGlassEnabled(true);
+      glassPanel.setWidget(new Label("Loading..."));
+    }
+
+    if (visible) {
+      // Show the loading panel.
+      glassPanel.center();
+    } else {
+      // Hide the loading panel.
+      glassPanel.hide();
+    }
+  }
+
+  /**
+   * The checkbox used to specify that the task should be added to the calendar.
+   */
+  @UiField
+  CheckBox calendarCheckbox;
+
+  /**
+   * The button used to select the date.
+   */
+  @UiField
+  Button dateButton;
+
+  /**
+   * The button used to delete a task or cancel changes.
+   */
+  @UiField
+  Button deleteButton;
+
+  /**
+   * The text box used to enter the task name.
+   */
+  @UiField
+  TextBoxBase nameBox;
+
+  /**
+   * The text box used to enter task notes.
+   */
+  @UiField
+  TextBoxBase notesBox;
+
+  /**
+   * The text box used to save changes or create a new task.
+   */
+  @UiField
+  Button saveButton;
+
+  /**
+   * The current date value.
+   */
+  private Date currentDate;
+
+  /**
+   * The popup panel that contains a date picker for selecting the date.
+   */
+  private final PopupPanel datePickerPopup;
+
+  /**
+   * The {@link Presenter} for this view.
+   */
+  private Presenter presenter;
+
+  /**
+   * Construct a new {@link TabletTaskEditView}.
+   * 
+   * @param presenter the {@link Presenter} that handles interactions
+   */
+  public TabletTaskEditView() {
+    initWidget(uiBinder.createAndBindUi(this));
+
+    // Create the datePickerPopup.
+    final DatePicker datePicker = new DatePicker();
+    datePickerPopup = new PopupPanel(true, true);
+    datePickerPopup.setWidget(datePicker);
+    datePickerPopup.setGlassEnabled(true);
+
+    /*
+     * When the user clicks on the date button, open a date picker so they can
+     * select a date.
+     */
+    dateButton.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        datePicker.setValue(currentDate, false);
+        datePickerPopup.center();
+      }
+    });
+    datePicker.addValueChangeHandler(new ValueChangeHandler<Date>() {
+      public void onValueChange(ValueChangeEvent<Date> event) {
+        setDueDate(event.getValue());
+        datePickerPopup.hide();
+      }
+    });
+
+    // Create a new task or modify the current task when done is pressed.
+    saveButton.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        if (presenter != null) {
+          presenter.saveTask(calendarCheckbox.getValue());
+        }
+      }
+    });
+
+    // Delete the current task or cancel when delete is pressed.
+    deleteButton.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        if (presenter != null) {
+          presenter.deleteTask();
+        }
+      }
+    });
+  }
+
+  public Date getDueDate() {
+    return currentDate;
+  }
+
+  public String getName() {
+    return nameBox.getValue();
+  }
+
+  public String getNotes() {
+    return notesBox.getValue();
+  }
+
+  public void setDueDate(Date date) {
+    boolean wasNull = (currentDate == null);
+    currentDate = date;
+    if (date == null) {
+      dateButton.setText("Set due date");
+      calendarCheckbox.setValue(false);
+      calendarCheckbox.setEnabled(false);
+    } else {
+      dateButton.setText(dateFormat.format(date));
+      calendarCheckbox.setEnabled(true);
+      if (wasNull) {
+        calendarCheckbox.setValue(true);
+      }
+    }
+  }
+
+  public void setEditing(boolean isEditing) {
+    if (isEditing) {
+      deleteButton.setText("Delete item");
+    } else {
+      deleteButton.setText("Cancel");
+    }
+  }
+
+  public void setLocked(boolean locked) {
+    setGlassPanelVisible(locked);
+  }
+
+  public void setName(String name) {
+    nameBox.setText(name);
+  }
+
+  public void setNotes(String notes) {
+    notesBox.setText(notes);
+  }
+
+  public void setPresenter(Presenter presenter) {
+    this.presenter = presenter;
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.ui.xml b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.ui.xml
new file mode 100644
index 0000000..a957f54
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.ui.xml
@@ -0,0 +1,160 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+  xmlns:ui="urn:ui:com.google.gwt.uibinder"
+  xmlns:g="urn:import:com.google.gwt.user.client.ui">
+
+  <ui:style>
+    .outer {
+      background: #eee;
+    }
+    
+    .title {
+      background: #393939;
+      color: white;
+      padding: 4px 10px;
+      font-size: 20pt;
+    }
+    
+    .editForm {
+      padding: 10px;
+      background: white;
+    }
+    
+    .label {
+      color: #666;
+      font-size: 20pt;
+      padding-bottom: 3px;
+    }
+    
+    .field {
+      width: 100%;
+      margin-bottom: 12px;
+      font-size: 20pt;
+    }
+    
+    .textBoxWrapper {
+      margin-right: 10px;
+    }
+    
+    .checkbox {
+      color: #3f3f3f;
+      font-size: 20pt;
+    }
+    
+    .nameBox {
+      height: 2em;
+    }
+    
+    .notesBox {
+      height: 4em;
+    }
+    
+    .button {
+      padding-top: 8px;
+      padding-bottom: 8px;
+      color: #3f3f3f;
+      font-size: 20pt;
+    }
+    
+    .dateButton {
+      text-align: left;
+    }
+    
+    .buttonPanel {
+      width: 100%;
+      padding: 10px;
+      margin-top: 15px;
+    }
+    
+    .saveButton {
+      width: 100%;
+    }
+    
+    .deleteButton {
+      color: white;
+      background: #940000;
+      width: 100%;
+      border-radius: 3px;
+      -moz-border-radius: 3px;
+    }
+  </ui:style>
+
+  <g:DockLayoutPanel
+    unit="PT">
+    <!-- Title. -->
+    <g:north
+      size="28">
+      <g:Label
+        addStyleNames="{style.title}">DETAILS</g:Label>
+    </g:north>
+
+    <g:center>
+      <g:ScrollPanel
+        addStyleNames="{style.outer}">
+        <g:HTMLPanel>
+
+          <!-- Edit Form. -->
+          <div
+            class="{style.editForm}">
+            <!-- Task name. -->
+            <div
+              class="{style.label}">What</div>
+            <div
+              class="{style.textBoxWrapper}">
+              <g:TextArea
+                addStyleNames="{style.field} {style.nameBox}"
+                ui:field="nameBox" />
+            </div>
+
+            <!-- Task notes. -->
+            <div
+              class="{style.label}">Notes</div>
+            <div
+              class="{style.textBoxWrapper}">
+              <g:TextArea
+                addStyleNames="{style.field} {style.notesBox}"
+                ui:field="notesBox" />
+            </div>
+
+            <!-- Task due date. -->
+            <div
+              class="{style.label}">Due date</div>
+            <g:Button
+              addStyleNames="{style.field} {style.button} {style.dateButton}"
+              ui:field="dateButton">Set due date</g:Button>
+
+            <!-- Add to calendar. -->
+            <g:CheckBox
+              addStyleNames="{style.checkbox}"
+              ui:field="calendarCheckbox">Add to my calender</g:CheckBox>
+          </div>
+
+          <!-- Button panel. -->
+          <table
+            class="{style.buttonPanel}"
+            cellspacing="0"
+            cellpadding="0">
+            <tr>
+              <td
+                align="center"
+                style="width:50%;padding-right:5px;">
+                <g:Button
+                  ui:field="saveButton"
+                  addStyleNames="{style.button} {style.saveButton}">Done</g:Button>
+              </td>
+              <td
+                align="center"
+                style="width:50%;padding-left:5px;">
+                <g:Button
+                  ui:field="deleteButton"
+                  addStyleNames="{style.button} {style.deleteButton}">Delete Item</g:Button>
+              </td>
+            </tr>
+          </table>
+
+        </g:HTMLPanel>
+      </g:ScrollPanel>
+    </g:center>
+  </g:DockLayoutPanel>
+
+</ui:UiBinder> 
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/tabletStyles.css b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/tabletStyles.css
new file mode 100644
index 0000000..064c117
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/client/tablet/tabletStyles.css
@@ -0,0 +1,4 @@
+.datePickerDay,.datePickerWeekdayLabel,.datePickerWeekendLabel,td.datePickerMonth
+  {
+  font-size: 30pt;
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/EMF.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/EMF.java
new file mode 100644
index 0000000..1fb7db8
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/EMF.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.server.domain;
+
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.Persistence;
+
+/**
+ * Factory for creating EntityManager.
+ */
+public final class EMF {
+
+  private static final EntityManagerFactory emfInstance =
+    Persistence.createEntityManagerFactory("transactions-optional");
+
+  public static EntityManagerFactory get() {
+    return emfInstance;
+  }
+
+  private EMF() {
+    // nothing
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/Task.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/Task.java
new file mode 100644
index 0000000..46e7183
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/server/domain/Task.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.server.domain;
+
+import java.util.Date;
+import java.util.List;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EntityManager;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Query;
+import javax.persistence.Version;
+
+/**
+ * A task used in the task list.
+ */
+@Entity
+public class Task {
+
+  /**
+   * Find all tasks.
+   */
+  @SuppressWarnings("unchecked")
+  public static List<Task> findAllTasks() {
+    EntityManager em = entityManager();
+    try {
+      Query query = em.createQuery("select o from Task o");
+      List<Task> list = query.getResultList();
+
+      /*
+       * If this is the first time running the app, populate the datastore with
+       * some default tasks and re-query the datastore for them.
+       */
+      if (list.size() == 0) {
+        populateDatastore();
+        list = query.getResultList();
+
+        /*
+         * Workaround for this issue: http://code.google.com/p/datanucleus-appengine/issues/detail?id=24
+         */
+        list.size();
+      }
+
+      return list;
+    } finally {
+      em.close();
+    }
+  };
+
+  /**
+   * Find a {@link Task} by id.
+   * 
+   * @param id the {@link Task} id
+   * @return the associated {@link Task}, or null if not found
+   */
+  public static Task findTask(Long id) {
+    if (id == null) {
+      return null;
+    }
+    EntityManager em = entityManager();
+    try {
+      Task task = em.find(Task.class, id);
+      return task;
+    } finally {
+      em.close();
+    }
+  }
+
+  /**
+   * Create an entity manager to interact with the database.
+   * 
+   * @return an {@link EntityManager} instance
+   */
+  private static EntityManager entityManager() {
+    return EMF.get().createEntityManager();
+  }
+
+  /**
+   * Populate the datastore with some default tasks. We do this to make the app
+   * more intuitive on first use.
+   * 
+   * @return the local {@link Task} map
+   */
+  @SuppressWarnings("deprecation")
+  private static void populateDatastore() {
+    {
+      // Task 0.
+      Task task0 = new Task();
+      task0.setName("Beat Angry Birds");
+      task0.setNotes("This game is impossible!");
+      task0.setDueDate(new Date(100, 4, 20));
+      task0.persist();
+    }
+    {
+      // Task 1.
+      Task task1 = new Task();
+      task1.setName("Make a million dollars");
+      task1.setNotes("Then spend it all on Android apps");
+      task1.persist();
+    }
+    {
+      // Task 2.
+      Task task2 = new Task();
+      task2.setName("Buy a dozen eggs");
+      task2.setNotes("of the chicken variety");
+      task2.persist();
+    }
+    {
+      // Task 3.
+      Task task3 = new Task();
+      task3.setName("Complete all tasks");
+      task3.persist();
+    }
+  }
+
+  @Id
+  @Column(name = "id")
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  private Long id;
+
+  @Version
+  @Column(name = "version")
+  private Integer version;
+
+  private Date dueDate;
+  private String name;
+  private String notes;
+
+  /**
+   * Get the due date of the Task.
+   */
+  public Date getDueDate() {
+    return dueDate;
+  }
+
+  /**
+   * Get the unique ID of the Task.
+   */
+  public Long getId() {
+    return id;
+  }
+
+  /**
+   * Get the name of the Task.
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Get the notes associated with the task.
+   */
+  public String getNotes() {
+    return notes;
+  }
+
+  /**
+   * Get the version of this datastore object.
+   */
+  public Integer getVersion() {
+    return version;
+  }
+
+  /**
+   * Persist this object in the data store.
+   */
+  public void persist() {
+    EntityManager em = entityManager();
+    try {
+      em.persist(this);
+    } finally {
+      em.close();
+    }
+  }
+
+  /**
+   * Remove this object from the data store.
+   */
+  public void remove() {
+    EntityManager em = entityManager();
+    try {
+      Task task = em.find(Task.class, this.id);
+      em.remove(task);
+    } finally {
+      em.close();
+    }
+  }
+
+  /**
+   * Set the due date of the task.
+   * 
+   * @param dueDate the due date, or null if no due date
+   */
+  public void setDueDate(Date dueDate) {
+    this.dueDate = dueDate;
+  }
+
+  public void setId(Long id) {
+    this.id = id;
+  }
+
+  /**
+   * Set the name of the task.
+   * 
+   * @param name the task name
+   */
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  /**
+   * Set the notes associated with the task.
+   * 
+   * @param notes the notes
+   */
+  public void setNotes(String notes) {
+    this.notes = notes;
+  }
+
+  public void setVersion(Integer version) {
+    this.version = version;
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/FieldVerifier.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/FieldVerifier.java
new file mode 100644
index 0000000..d6ddb9d
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/FieldVerifier.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.shared;
+
+/**
+ * <p>
+ * FieldVerifier validates that the name the user enters is valid.
+ * </p>
+ * <p>
+ * This class is in the <code>shared</code> package because we use it in both
+ * the client code and on the server. On the client, we verify that the name is
+ * valid before sending an RPC request so the user doesn't have to wait for a
+ * network round trip to get feedback. On the server, we verify that the name is
+ * correct to ensure that the input is correct regardless of where the RPC
+ * originates.
+ * </p>
+ * <p>
+ * When creating a class that is used on both the client and the server, be sure
+ * that all code is translatable and does not use native JavaScript. Code that
+ * is note translatable (such as code that interacts with a database or the file
+ * system) cannot be compiled into client side JavaScript. Code that uses native
+ * JavaScript (such as Widgets) cannot be run on the server.
+ * </p>
+ */
+public class FieldVerifier {
+
+  /**
+   * Verifies that the specified name is valid for our service.
+   * 
+   * In this example, we only require that the name is at least four
+   * characters. In your application, you can use more complex checks to ensure
+   * that usernames, passwords, email addresses, URLs, and other fields have the
+   * proper syntax.
+   * 
+   * @param name the name to validate
+   * @return true if valid, false if invalid
+   */
+  public static boolean isValidName(String name) {
+    if (name == null) {
+      return false;
+    }
+    return name.length() > 3;
+  }
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxy.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxy.java
new file mode 100644
index 0000000..59ed5bf
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxy.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.shared;
+
+import com.google.gwt.sample.mobilewebapp.server.domain.Task;
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+
+import java.util.Date;
+
+/**
+ * A task used in the task list.
+ */
+@ProxyFor(Task.class)
+public interface TaskProxy extends EntityProxy {
+
+  Date getDueDate();
+
+  Long getId();
+
+  String getName();
+
+  String getNotes();
+
+  void setDueDate(Date dueDate);
+
+  void setName(String name);
+
+  void setNotes(String notes);
+}
diff --git a/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxyImpl.java b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxyImpl.java
new file mode 100644
index 0000000..0b0572c
--- /dev/null
+++ b/samples/mobilewebapp/src/com/google/gwt/sample/mobilewebapp/shared/TaskProxyImpl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.sample.mobilewebapp.shared;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxyId;
+
+import java.util.Date;
+
+/**
+ * A task used in the task list.
+ */
+public class TaskProxyImpl implements TaskProxy {
+  private Date dueDate;
+  private Long id;
+  private String name;
+  private String notes;
+
+  public TaskProxyImpl() {
+  }
+
+  public Date getDueDate() {
+    return dueDate;
+  }
+
+  public Long getId() {
+    return id;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public String getNotes() {
+    return notes;
+  }
+
+  public void setDueDate(Date dueDate) {
+    this.dueDate = dueDate;
+  }
+
+  public void setId(Long id) {
+    this.id = id;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public void setNotes(String notes) {
+    this.notes = notes;
+  }
+
+  public EntityProxyId<?> stableId() {
+    return null;
+  }
+}
diff --git a/samples/mobilewebapp/src/log4j.properties b/samples/mobilewebapp/src/log4j.properties
new file mode 100644
index 0000000..d9c3edc
--- /dev/null
+++ b/samples/mobilewebapp/src/log4j.properties
@@ -0,0 +1,24 @@
+# A default log4j configuration for log4j users.
+#
+# To use this configuration, deploy it into your application's WEB-INF/classes
+# directory.  You are also encouraged to edit it as you like.
+
+# Configure the console as our one appender
+log4j.appender.A1=org.apache.log4j.ConsoleAppender
+log4j.appender.A1.layout=org.apache.log4j.PatternLayout
+log4j.appender.A1.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n
+
+# tighten logging on the DataNucleus Categories
+log4j.category.DataNucleus.JDO=WARN, A1
+log4j.category.DataNucleus.Persistence=WARN, A1
+log4j.category.DataNucleus.Cache=WARN, A1
+log4j.category.DataNucleus.MetaData=WARN, A1
+log4j.category.DataNucleus.General=WARN, A1
+log4j.category.DataNucleus.Utility=WARN, A1
+log4j.category.DataNucleus.Transaction=WARN, A1
+log4j.category.DataNucleus.Datastore=WARN, A1
+log4j.category.DataNucleus.ClassLoading=WARN, A1
+log4j.category.DataNucleus.Plugin=WARN, A1
+log4j.category.DataNucleus.ValueGeneration=WARN, A1
+log4j.category.DataNucleus.Enhancer=WARN, A1
+log4j.category.DataNucleus.SchemaTool=WARN, A1
diff --git a/samples/mobilewebapp/user-build.xml b/samples/mobilewebapp/user-build.xml
new file mode 100644
index 0000000..9d05428
--- /dev/null
+++ b/samples/mobilewebapp/user-build.xml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<project name="MobileWebApp" default="build" basedir=".">
+  <property file="local.properties" />
+  <!-- Arguments to gwtc and devmode targets -->
+  <property name="gwt.args" value="" />
+  <property name="src.dir" value="src" />
+  <property name="war.dir" value="war" />
+  <property name="gwt.sdk" value="../.." />
+
+  <fail unless="appengine.sdk">Missing property:
+    The property 'appengine.sdk' is not set.
+    Either specity the property by passing an argument
+
+    -Dappengine.sdk="path-to-your-appengine-sdk"
+
+    or create a 'local.properties' file containing the
+    location of the App Engine SDK. i.e. the line:
+
+    appengine.sdk=path-to-your-appengine-sdk
+  </fail>
+
+  <!-- Configure AppEngine macros -->
+  <import file="${appengine.sdk}/config/user/ant-macros.xml" />
+
+  <path id="project.class.path">
+    <pathelement location="${war.dir}/WEB-INF/classes"/>
+    <pathelement location="${gwt.sdk}/gwt-user.jar"/>
+    <fileset dir="${gwt.sdk}" includes="gwt-dev*.jar"/>
+    <fileset dir="${gwt.sdk}" includes="validation-api-1.0.0.GA-sources.jar"/>
+    <fileset dir="${gwt.sdk}" includes="validation-api-1.0.0.GA.jar"/>
+    <!-- Add any additional non-server libs (such as JUnit) -->
+    <fileset dir="${war.dir}/WEB-INF/lib" includes="**/*.jar"/>
+  </path>
+
+  <path id="tools.class.path">
+    <path refid="project.class.path"/>
+    <pathelement location="${appengine.sdk}/lib/appengine-tools-api.jar"/>
+    <fileset dir="${appengine.sdk}/lib/tools">
+      <include name="**/asm-*.jar"/>
+      <include name="**/datanucleus-enhancer-*.jar"/>
+    </fileset>
+  </path>
+
+  <target name="appengine-copyjars"
+      description="Copies the App Engine JARs to the WAR.">
+    <copy
+        todir="${war.dir}/WEB-INF/lib"
+        flatten="true">
+      <fileset dir="${appengine.sdk}/lib/user">
+        <include name="**/*.jar" />
+      </fileset>
+    </copy>
+  </target>
+
+  <target name="libs" description="Copy libs to WEB-INF/lib">
+    <mkdir dir="${war.dir}/WEB-INF/lib" />
+    <copy todir="${war.dir}/WEB-INF/lib" file="${gwt.sdk}/gwt-servlet.jar" />
+    <copy todir="${war.dir}/WEB-INF/lib" file="${gwt.sdk}/gwt-servlet-deps.jar" />
+    <copy todir="${war.dir}/WEB-INF/lib" file="${gwt.sdk}/validation-api-1.0.0.GA.jar"/>
+    <copy todir="${war.dir}/WEB-INF/lib" file="${gwt.sdk}/validation-api-1.0.0.GA-sources.jar"/>
+    <!-- Add any additional server libs that need to be copied -->
+  </target>
+
+  <target name="javac" depends="libs,appengine-copyjars" description="Compile java source to bytecode">
+    <mkdir dir="${war.dir}/WEB-INF/classes"/>
+    <javac srcdir="${src.dir}/" includes="**" encoding="utf-8"
+        destdir="${war.dir}/WEB-INF/classes"
+        source="1.5" target="1.5" nowarn="true"
+        debug="true" debuglevel="lines,vars,source">
+      <classpath refid="project.class.path"/>
+    </javac>
+    <copy todir="${war.dir}/WEB-INF/classes">
+      <fileset dir="${src.dir}/" excludes="**/*.java"/>
+    </copy>
+  </target>
+
+  <target name="gwtc" depends="javac" description="GWT compile to JavaScript (production mode)">
+    <java failonerror="true" fork="true" classname="com.google.gwt.dev.Compiler">
+      <classpath>
+        <pathelement location="${src.dir}/"/>
+        <path refid="project.class.path"/>
+      </classpath>
+      <!-- add jvmarg -Xss16M or similar if you see a StackOverflowError -->
+      <jvmarg value="-Xmx256M"/>
+      <arg line="-war"/>
+      <arg value="${war.dir}/"/>
+      <!-- Additional arguments like -style PRETTY or -logLevel DEBUG -->
+      <arg line="${gwt.args}"/>
+      <arg value="com.google.gwt.sample.mobilewebapp.MobileWebApp"/>
+    </java>
+  </target>
+
+  <target name="datanucleusenhance" depends="javac"
+      description="Performs JDO enhancement on compiled data classes.">
+    <enhance_war war="${war.dir}/" />
+  </target>
+
+  <target name="devmode" depends="javac,datanucleusenhance" description="Run development mode">
+    <java failonerror="true" fork="true" classname="com.google.gwt.dev.DevMode">
+      <classpath>
+        <pathelement location="${src.dir}/"/>
+        <path refid="project.class.path"/>
+        <path refid="tools.class.path"/>
+      </classpath>
+      <jvmarg value="-Xmx256M"/>
+      <jvmarg value="-javaagent:${appengine.sdk}/lib/agent/appengine-agent.jar"/>
+      <arg value="-startupUrl"/>
+      <arg value="MobileWebApp.html"/>
+      <arg line="-war"/>
+      <arg value="${war.dir}/"/>
+      <arg value="-server"/>
+      <arg value="com.google.appengine.tools.development.gwt.AppEngineLauncher"/>
+      <!-- Additional arguments like -style PRETTY or -logLevel DEBUG -->
+      <arg line="${gwt.args}"/>
+      <arg value="com.google.gwt.sample.mobilewebapp.MobileWebApp"/>
+    </java>
+  </target>
+
+  <target name="build" depends="gwtc,datanucleusenhance" description="Build this project" />
+
+  <target name="war" depends="build" description="Create a war file">
+    <zip destfile="MobileWebApp.war" basedir="${war.dir}/"/>
+  </target>
+
+  <target name="clean" description="Cleans this project">
+    <delete file="MobileWebApp.war" failonerror="false" />
+    <delete dir="war/WEB-INF/appengine-generated/" failonerror="false" />
+    <delete dir="war/WEB-INF/classes/com" failonerror="false" />
+    <delete dir="war/WEB-INF/classes/META-INF" failonerror="false" />
+    <delete file="war/WEB-INF/classes/log4j.properties" failonerror="false" />
+    <delete dir="war/WEB-INF/deploy/" failonerror="false" />
+    <delete dir="war/WEB-INF/lib/" failonerror="false" />
+    <delete dir="war/mobilewebapp/" failonerror="false" />
+  </target>
+
+</project>
diff --git a/samples/mobilewebapp/war/MobileWebApp.css b/samples/mobilewebapp/war/MobileWebApp.css
new file mode 100644
index 0000000..73d994b
--- /dev/null
+++ b/samples/mobilewebapp/war/MobileWebApp.css
@@ -0,0 +1,38 @@
+/** Add css rules here for your application. */
+
+
+/** Example rules used by the template application (remove for your app) */
+h1 {
+  font-size: 2em;
+  font-weight: bold;
+  color: #777777;
+  margin: 40px 0px 70px;
+  text-align: center;
+}
+
+.sendButton {
+  display: block;
+  font-size: 16pt;
+}
+
+/** Most GWT widgets already have a style name defined */
+.gwt-DialogBox {
+  width: 400px;
+}
+
+.dialogVPanel {
+  margin: 5px;
+}
+
+.gwt-CheckBox-disabled {
+  color: #bbb !important;
+}
+
+.serverResponseLabelError {
+  color: red;
+}
+
+/** Set ids using widget.getElement().setId("idOfElement") */
+#closeButton {
+  margin: 15px 6px 6px;
+}
diff --git a/samples/mobilewebapp/war/MobileWebApp.html b/samples/mobilewebapp/war/MobileWebApp.html
new file mode 100644
index 0000000..d8508cc
--- /dev/null
+++ b/samples/mobilewebapp/war/MobileWebApp.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<!-- The DOCTYPE declaration above will set the    -->
+<!-- browser's rendering engine into               -->
+<!-- "Standards Mode". Replacing this declaration  -->
+<!-- with a "Quirks Mode" doctype may lead to some -->
+<!-- differences in layout.                        -->
+
+<html>
+  <head>
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+
+    <!--                                                               -->
+    <!-- These meta tags tell the mobile browser that the app is       -->
+    <!-- formatted for mobile browsers and should not be zoomed in by  -->
+    <!-- default.                                                      -->
+    <!--                                                               -->
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <meta name="viewport" content="width=device-width, user-scalable=no">
+
+    <!--                                                               -->
+    <!-- Consider inlining CSS to reduce the number of requested files -->
+    <!--                                                               -->
+    <link type="text/css" rel="stylesheet" href="MobileWebApp.css">
+
+    <!--                                           -->
+    <!-- Any title is fine                         -->
+    <!--                                           -->
+    <title>Web Application Starter Project</title>
+    
+    <!--                                           -->
+    <!-- This script loads your compiled module.   -->
+    <!-- If you add any GWT meta tags, they must   -->
+    <!-- be added before this line.                -->
+    <!--                                           -->
+    <script type="text/javascript" language="javascript" src="mobilewebapp/mobilewebapp.nocache.js"></script>
+  </head>
+
+  <!--                                           -->
+  <!-- The body can have arbitrary html, or      -->
+  <!-- you can leave the body empty if you want  -->
+  <!-- to create a completely dynamic UI.        -->
+  <!--                                           -->
+  <body>
+
+    <!-- OPTIONAL: include this if you want history support -->
+    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
+    
+    <!-- RECOMMENDED if your web app will not function without JavaScript enabled -->
+    <noscript>
+      <div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif">
+        Your web browser must have JavaScript enabled
+        in order for this application to display correctly.
+      </div>
+    </noscript>
+  </body>
+</html>
diff --git a/samples/mobilewebapp/war/WEB-INF/appengine-web.xml b/samples/mobilewebapp/war/WEB-INF/appengine-web.xml
new file mode 100644
index 0000000..3e626c5
--- /dev/null
+++ b/samples/mobilewebapp/war/WEB-INF/appengine-web.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
+
+  <!-- Configure serving/caching of GWT files -->
+  <static-files>
+    <include path="**" />
+
+    <!-- The following line requires App Engine 1.3.2 SDK -->
+    <include path="**.nocache.*" expiration="0s" />
+
+    <include path="**.cache.*" expiration="365d" />
+    <exclude path="**.gwt.rpc" />
+  </static-files>
+
+  <!-- Configure java.util.logging -->
+  <system-properties>
+    <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
+  </system-properties>
+
+</appengine-web-app>
diff --git a/samples/mobilewebapp/war/WEB-INF/classes/marker b/samples/mobilewebapp/war/WEB-INF/classes/marker
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/samples/mobilewebapp/war/WEB-INF/classes/marker
diff --git a/samples/mobilewebapp/war/WEB-INF/logging.properties b/samples/mobilewebapp/war/WEB-INF/logging.properties
new file mode 100644
index 0000000..a172066
--- /dev/null
+++ b/samples/mobilewebapp/war/WEB-INF/logging.properties
@@ -0,0 +1,13 @@
+# A default java.util.logging configuration.
+# (All App Engine logging is through java.util.logging by default).
+#
+# To use this configuration, copy it into your application's WEB-INF
+# folder and add the following to your appengine-web.xml:
+# 
+# <system-properties>
+#   <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
+# </system-properties>
+#
+
+# Set the default logging level for all loggers to WARNING
+.level = WARNING
diff --git a/samples/mobilewebapp/war/WEB-INF/web.xml b/samples/mobilewebapp/war/WEB-INF/web.xml
new file mode 100644
index 0000000..e19e911
--- /dev/null
+++ b/samples/mobilewebapp/war/WEB-INF/web.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE web-app
+    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
+    "http://java.sun.com/dtd/web-app_2_3.dtd">
+
+<web-app>
+
+  <!-- RequestFactory -->
+  <servlet>
+    <servlet-name>requestFactoryServlet</servlet-name>
+    <servlet-class>com.google.web.bindery.requestfactory.server.RequestFactoryServlet</servlet-class>
+  </servlet>
+
+  <servlet-mapping>
+    <servlet-name>requestFactoryServlet</servlet-name>
+    <url-pattern>/gwtRequest</url-pattern>
+  </servlet-mapping>
+  
+  <!-- Default page to serve -->
+  <welcome-file-list>
+    <welcome-file>MobileWebApp.html</welcome-file>
+  </welcome-file-list>
+
+</web-app>