Cleans up the mobile web app sample for 2.4.

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

Review by: jlabanca@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10358 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/mobilewebapp/pom.xml b/samples/mobilewebapp/pom.xml
index 530873e..38641da 100644
--- a/samples/mobilewebapp/pom.xml
+++ b/samples/mobilewebapp/pom.xml
@@ -12,7 +12,7 @@
   
   <properties>
     <!-- Convenience property to set the GWT version -->
-    <gwtVersion>2.3.0</gwtVersion>
+    <gwtVersion>2.4.0</gwtVersion>
 
     <!-- GWT needs at least java 1.6 -->
     <maven.compiler.source>1.6</maven.compiler.source>
@@ -56,6 +56,12 @@
 
   <dependencies>
     <dependency>
+      <groupId>javax.inject</groupId>
+      <artifactId>javax.inject</artifactId>
+      <version>1</version>
+    </dependency>
+
+    <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <version>4.8.1</version>
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/GaeAuthRequestTransport.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/GaeAuthRequestTransport.java
index 3c87b17..5e224d7 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/GaeAuthRequestTransport.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/GaeAuthRequestTransport.java
@@ -15,13 +15,13 @@
  */
 package com.google.gwt.sample.gaerequest.client;
 
-import com.google.gwt.event.shared.EventBus;
 import com.google.gwt.http.client.Request;
 import com.google.gwt.http.client.RequestBuilder;
 import com.google.gwt.http.client.RequestCallback;
 import com.google.gwt.http.client.Response;
 import com.google.gwt.sample.gaerequest.shared.GaeHelper;
 import com.google.gwt.user.client.Window.Location;
+import com.google.web.bindery.event.shared.EventBus;
 import com.google.web.bindery.requestfactory.gwt.client.DefaultRequestTransport;
 import com.google.web.bindery.requestfactory.shared.ServerFailure;
 
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/GaeAuthenticationFailureEvent.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/GaeAuthenticationFailureEvent.java
index 3720e95..d7fa66a 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/GaeAuthenticationFailureEvent.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/GaeAuthenticationFailureEvent.java
@@ -15,20 +15,20 @@
  */
 package com.google.gwt.sample.gaerequest.client;
 
-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.event.shared.HandlerRegistration;
+import com.google.web.bindery.event.shared.Event;
+import com.google.web.bindery.event.shared.EventBus;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+
 
 /**
  * An event posted when an authentication failure is detected.
  */
-public class GaeAuthenticationFailureEvent extends GwtEvent<GaeAuthenticationFailureEvent.Handler> {
+public class GaeAuthenticationFailureEvent extends Event<GaeAuthenticationFailureEvent.Handler> {
 
   /**
    * Implemented by handlers of this type of event.
    */
-  public interface Handler extends EventHandler {
+  public interface Handler {
     /**
      * Called when a {@link GaeAuthenticationFailureEvent} is fired.
      * 
@@ -64,7 +64,7 @@
   }
 
   @Override
-  public GwtEvent.Type<Handler> getAssociatedType() {
+  public Type<Handler> getAssociatedType() {
     return TYPE;
   }
 
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/ReloadOnAuthenticationFailure.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/ReloadOnAuthenticationFailure.java
index 1560a65..a345f1d 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/ReloadOnAuthenticationFailure.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/gaerequest/client/ReloadOnAuthenticationFailure.java
@@ -16,9 +16,9 @@
 
 package com.google.gwt.sample.gaerequest.client;
 
-import com.google.gwt.event.shared.EventBus;
-import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.Window.Location;
+import com.google.web.bindery.event.shared.EventBus;
+import com.google.web.bindery.event.shared.HandlerRegistration;
 
 /**
  * A minimal auth failure handler which takes the user a login page.
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
index da6dd17..c701f4a 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/MobileWebApp.gwt.xml
@@ -17,6 +17,9 @@
   
   <inherits name='com.google.gwt.sample.gaerequest.GaeRequest'/>
   <inherits name='com.google.gwt.sample.core.Core'/>
+  <inherits name='com.google.gwt.sample.ui.UI'/>
+
+  <inherits name="com.google.gwt.logging.Logging"/>
 
   <!-- Specify the app entry point class.                         -->
   <entry-point class='com.google.gwt.sample.mobilewebapp.client.MobileWebApp'/>
@@ -43,5 +46,6 @@
 
   <!-- Specify the paths for translatable code                    -->
   <source path='client'/>
+  <source path='presenter'/>
   <source path='shared'/>
 </module>
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/App.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/App.java
index 1fb6997..b1eb60b 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/App.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/App.java
@@ -15,34 +15,38 @@
  */
 package com.google.gwt.sample.mobilewebapp.client;
 
+import com.google.gwt.activity.shared.ActivityManager;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
-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.sample.gaerequest.client.ReloadOnAuthenticationFailure;
-import com.google.gwt.sample.mobilewebapp.client.event.AddTaskEvent;
-import com.google.gwt.sample.mobilewebapp.client.event.EditingCanceledEvent;
-import com.google.gwt.sample.mobilewebapp.client.event.GoHomeEvent;
+import com.google.gwt.sample.mobilewebapp.client.activity.AppPlaceHistoryMapper;
+import com.google.gwt.sample.mobilewebapp.client.event.ActionEvent;
+import com.google.gwt.sample.mobilewebapp.client.event.ActionNames;
 import com.google.gwt.sample.mobilewebapp.client.event.ShowTaskEvent;
-import com.google.gwt.sample.mobilewebapp.client.event.TaskSavedEvent;
-import com.google.gwt.sample.mobilewebapp.client.place.AppPlaceHistoryMapper;
-import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
-import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskPlace;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListPlace;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
 import com.google.gwt.storage.client.Storage;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.web.bindery.event.shared.EventBus;
 import com.google.web.bindery.event.shared.UmbrellaException;
 
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
 /**
  * The heart of the applicaiton, mainly concerned with bootstrapping.
  */
 public class App {
   private static final String HISTORY_SAVE_KEY = "SAVEPLACE";
 
+  private static final Logger log = Logger.getLogger(App.class.getName());
+
   private final Storage storage;
 
   /**
@@ -61,6 +65,8 @@
    */
   private final MobileWebAppShell shell;
 
+  private final ActivityManager activityManager;
+
   private final AppPlaceHistoryMapper historyMapper;
 
   private final PlaceHistoryHandler historyHandler;
@@ -68,12 +74,14 @@
   private final ReloadOnAuthenticationFailure reloadOnAuthenticationFailure;
 
   public App(Storage storage, EventBus eventBus, PlaceController placeController,
-      AppPlaceHistoryMapper historyMapper, PlaceHistoryHandler historyHandler,
+      ActivityManager activityManager, AppPlaceHistoryMapper historyMapper,
+      PlaceHistoryHandler historyHandler,
       ReloadOnAuthenticationFailure reloadOnAuthenticationFailure, MobileWebAppShell shell) {
 
     this.storage = storage;
     this.eventBus = eventBus;
     this.placeController = placeController;
+    this.activityManager = activityManager;
     this.historyMapper = historyMapper;
     this.historyHandler = historyHandler;
     this.reloadOnAuthenticationFailure = reloadOnAuthenticationFailure;
@@ -86,12 +94,14 @@
    * @param parentView where to show the app's widget
    */
   public void run(HasWidgets.ForIsWidget parentView) {
+    activityManager.setDisplay(shell);
+
     parentView.add(shell);
 
-    eventBus.addHandler(AddTaskEvent.TYPE, new AddTaskEvent.Handler() {
+    ActionEvent.register(eventBus, ActionNames.ADD_TASK, new ActionEvent.Handler() {
       @Override
-      public void onAddTask(AddTaskEvent event) {
-        placeController.goTo(TaskEditPlace.getTaskCreatePlace());
+      public void onAction(ActionEvent event) {
+        placeController.goTo(TaskPlace.getTaskCreatePlace());
       }
     });
 
@@ -99,27 +109,27 @@
       @Override
       public void onShowTask(ShowTaskEvent event) {
         TaskProxy task = event.getTask();
-        placeController.goTo(TaskEditPlace.createTaskEditPlace(task.getId(), task));
+        placeController.goTo(TaskPlace.createTaskEditPlace(task.getId(), task));
       }
     });
 
-    eventBus.addHandler(GoHomeEvent.TYPE, new GoHomeEvent.Handler() {
+    ActionEvent.register(eventBus, ActionNames.GO_HOME, new ActionEvent.Handler() {
       @Override
-      public void onGoHome(GoHomeEvent event) {
+      public void onAction(ActionEvent event) {
         placeController.goTo(new TaskListPlace(false));
       }
     });
 
-    eventBus.addHandler(TaskSavedEvent.TYPE, new TaskSavedEvent.Handler() {
+    ActionEvent.register(eventBus, ActionNames.TASK_SAVED, new ActionEvent.Handler() {
       @Override
-      public void onTaskSaved(TaskSavedEvent event) {
+      public void onAction(ActionEvent event) {
         placeController.goTo(new TaskListPlace(true));
       }
     });
 
-    eventBus.addHandler(EditingCanceledEvent.TYPE, new EditingCanceledEvent.Handler() {
+    ActionEvent.register(eventBus, ActionNames.EDITING_CANCELED, new ActionEvent.Handler() {
       @Override
-      public void onEditCanceled(EditingCanceledEvent event) {
+      public void onAction(ActionEvent event) {
         placeController.goTo(new TaskListPlace(false));
       }
     });
@@ -130,8 +140,13 @@
         while (e instanceof UmbrellaException) {
           e = ((UmbrellaException) e).getCauses().iterator().next();
         }
-        Window.alert("An unexpected error occurred: " + e.getMessage());
-        placeController.goTo(new TaskListPlace(false));
+
+        String message = e.getMessage();
+        if (message == null) {
+          message = e.toString();
+        }
+        log.log(Level.SEVERE, "Uncaught exception", e);
+        Window.alert("An unexpected error occurred: " + message);
       }
     });
 
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactory.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactory.java
index 47da8f5..f75fd52 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactory.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactory.java
@@ -15,13 +15,12 @@
  */
 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.sample.mobilewebapp.client.activity.TaskEditView;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskReadView;
-import com.google.gwt.storage.client.Storage;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskReadView;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListView;
 import com.google.gwt.sample.mobilewebapp.shared.MobileWebAppRequestFactory;
+import com.google.web.bindery.event.shared.EventBus;
 
 /**
  * The factory responsible for instantiating everything interesting in this
@@ -29,8 +28,6 @@
  * "http://google-gin.googlecode.com/svn/trunk/javadoc/com/google/gwt/inject/client/Ginjector.html"
  * >Ginjector</a> code generated by <a
  * href="http://code.google.com/p/google-gin/">GIN</a>.
- * <p>
- * Note that {@link #init} must be called before anything else.
  */
 public interface ClientFactory {
   /**
@@ -41,20 +38,13 @@
   App getApp();
 
   /**
-   * Get the event bus.
+   * Get the {@link EventBus}
    * 
-   * @return the {@link EventBus} used throughout the app
+   * @return the event bus used through the app
    */
   EventBus getEventBus();
 
   /**
-   * Get the local {@link Storage} object if supported.
-   * 
-   * @return the local {@link Storage} object, or null unsupporting browsers
-   */
-  Storage getLocalStorageIfSupported();
-
-  /**
    * Get the {@link PlaceController}.
    * 
    * @return the place controller
@@ -94,9 +84,4 @@
    * Get an implementation of {@link TaskEditView}.
    */
   TaskReadView getTaskReadView();
-
-  /**
-   * Must be called before any get methods.
-   */
-  void init();
 }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImpl.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImpl.java
index ec18f04..93bc04b 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImpl.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImpl.java
@@ -18,25 +18,26 @@
 import com.google.gwt.activity.shared.ActivityManager;
 import com.google.gwt.activity.shared.ActivityMapper;
 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.gwt.place.shared.PlaceHistoryHandler;
 import com.google.gwt.sample.gaerequest.client.GaeAuthRequestTransport;
 import com.google.gwt.sample.gaerequest.client.ReloadOnAuthenticationFailure;
 import com.google.gwt.sample.mobilewebapp.client.activity.AppActivityMapper;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskListActivity;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskReadView;
+import com.google.gwt.sample.mobilewebapp.client.activity.AppPlaceHistoryMapper;
 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.DesktopTaskReadView;
 import com.google.gwt.sample.mobilewebapp.client.desktop.MobileWebAppShellDesktop;
-import com.google.gwt.sample.mobilewebapp.client.place.AppPlaceHistoryMapper;
+import com.google.gwt.sample.mobilewebapp.client.ui.PieChart;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskReadView;
+import com.google.gwt.sample.mobilewebapp.presenter.taskchart.TaskChartPresenter;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListView;
 import com.google.gwt.sample.mobilewebapp.shared.MobileWebAppRequestFactory;
 import com.google.gwt.storage.client.Storage;
 import com.google.gwt.user.client.Window;
+import com.google.web.bindery.event.shared.EventBus;
+import com.google.web.bindery.event.shared.SimpleEventBus;
 import com.google.web.bindery.requestfactory.shared.RequestTransport;
 
 /**
@@ -57,7 +58,7 @@
   private final TaskProxyLocalStorage taskProxyLocalStorage;
   private TaskEditView taskEditView;
   private TaskListView taskListView;
-  private final ActivityManager activityManager;
+  private ActivityManager activityManager;
 
   private final AppPlaceHistoryMapper historyMapper = GWT.create(AppPlaceHistoryMapper.class);
 
@@ -82,34 +83,19 @@
       localStorage = null;
     }
     taskProxyLocalStorage = new TaskProxyLocalStorage(localStorage);
-
-    /*
-     * ActivityMapper determines an Activity to run for a particular place.
-     */
-    ActivityMapper activityMapper = new AppActivityMapper(this, getIsTaskListIncludedProvider());
-    /*
-     * Owns a panel in the window, in this case the entire {@link #shell}.
-     * Monitors the {@link #eventBus} for {@link PlaceChangeEvent}s posted by
-     * the {@link #placeController}, and chooses what {@link Activity} gets to
-     * take over the panel at the current place. Configured by an {@link
-     * AppActivityMapper}.
-     */
-    activityManager = new ActivityManager(activityMapper, eventBus);
   }
 
   public App getApp() {
-    return new App(getLocalStorageIfSupported(), getEventBus(), getPlaceController(),
-        historyMapper, historyHandler, new ReloadOnAuthenticationFailure(), getShell());
+    return new App(getLocalStorageIfSupported(), eventBus, getPlaceController(),
+        getActivityManager(), historyMapper, historyHandler, new ReloadOnAuthenticationFailure(),
+        getShell());
   }
 
+  @Override
   public EventBus getEventBus() {
     return eventBus;
   }
-
-  public Storage getLocalStorageIfSupported() {
-    return localStorage;
-  }
-
+  
   public PlaceController getPlaceController() {
     return placeController;
   }
@@ -150,8 +136,12 @@
     return taskReadView;
   }
 
-  public void init() {
-    activityManager.setDisplay(getShell());
+  /**
+   * ActivityMapper determines an Activity to run for a particular place,
+   * configures the {@link #getActivityManager()}
+   */
+  protected ActivityMapper createActivityMapper() {
+    return new AppActivityMapper(this);
   }
 
   /**
@@ -160,7 +150,13 @@
    * @return the UI shell
    */
   protected MobileWebAppShell createShell() {
-    return new MobileWebAppShellDesktop(eventBus, placeController, getTaskListView(),
+    PieChart pieChart = PieChart.createIfSupported();
+    TaskChartPresenter presenter = null;
+    if (pieChart != null) {
+      presenter = new TaskChartPresenter(pieChart);
+      presenter.start(getEventBus());
+    }
+    return new MobileWebAppShellDesktop(eventBus, presenter, placeController, getTaskListView(),
         getTaskEditView(), getTaskReadView());
   }
 
@@ -187,31 +183,22 @@
   }
 
   /**
-   * Returns provider that indicates whether the task list is always visible.
-   * The default implementation returned by this method always indicates false.
-   * 
-   * @return provider that always provides false
+   * Owns a panel in the window, in this case the entire {@link #shell}.
+   * Monitors the {@link #eventBus} for
+   * {@link com.google.gwt.place.shared.PlaceChangeEvent PlaceChangeEvent}s posted by the
+   * {@link #placeController}, and chooses what
+   * {@link com.google.gwt.activity.shared.Activity Activity} gets to take
+   * over the panel at the current place. Configured by the
+   * {@link #createActivityMapper()}.
    */
-  protected Provider<Boolean> getIsTaskListIncludedProvider() {
-    return new Provider<Boolean>() {
-      @Override
-      public Boolean get() {
-        return false;
-      }
-    };
+  protected ActivityManager getActivityManager() {
+    if (activityManager == null) {
+      activityManager = new ActivityManager(createActivityMapper(), eventBus);
+    }
+    return activityManager;
   }
 
-  protected Provider<TaskListActivity> getTaskListActivityProvider() {
-    return new Provider<TaskListActivity>() {
-      @Override
-      public TaskListActivity get() {
-
-        /*
-         * TODO (rjrjr) the false arg is needed by MobileWebAppShellTablet,
-         * which shouldn't be using activities at all
-         */
-        return new TaskListActivity(ClientFactoryImpl.this, false);
-      }
-    };
+  private Storage getLocalStorageIfSupported() {
+    return localStorage;
   }
 }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplMobile.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplMobile.java
index a2e3d93..e668cbb 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplMobile.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplMobile.java
@@ -15,30 +15,26 @@
  */
 package com.google.gwt.sample.mobilewebapp.client;
 
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskListActivity;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView.Presenter;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskReadView;
 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.MobileTaskReadView;
 import com.google.gwt.sample.mobilewebapp.client.mobile.MobileWebAppShellMobile;
-import com.google.gwt.sample.mobilewebapp.client.ui.OrientationHelper;
-import com.google.gwt.sample.mobilewebapp.client.ui.WindowBasedOrientationHelper;
-import com.google.gwt.user.client.ui.AcceptsOneWidget;
-import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskReadView;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListView;
+import com.google.gwt.sample.ui.client.OrientationHelper;
+import com.google.gwt.sample.ui.client.WindowBasedOrientationHelper;
 
 /**
  * Mobile version of {@link ClientFactory}.
  */
 public class ClientFactoryImplMobile extends ClientFactoryImpl {
   private final OrientationHelper orientationHelper = new WindowBasedOrientationHelper();
-  
+
   @Override
   protected MobileWebAppShell createShell() {
-    return new MobileWebAppShellMobile(orientationHelper, getTaskListView(),
-        getTaskEditView(), getTaskReadView(), getEventBus());
+    return new MobileWebAppShellMobile(getEventBus(), orientationHelper, getTaskListView(),
+        getTaskEditView(), getTaskReadView());
   }
 
   @Override
@@ -48,21 +44,7 @@
 
   @Override
   protected TaskListView createTaskListView() {
-    ProvidesPresenter<TaskListView.Presenter, TaskListView> factory =
-      new ProvidesPresenter<TaskListView.Presenter, TaskListView>() {
-        @Override
-        public Presenter getPresenter(TaskListView view) {
-          TaskListActivity taskListActivity = new TaskListActivity(ClientFactoryImplMobile.this, false);
-          taskListActivity.start(new AcceptsOneWidget() {
-            @Override
-            public void setWidget(IsWidget w) {
-              // No op until we separate presenter and activity
-            }
-          }, getEventBus());
-          return taskListActivity;
-        }
-      };
-    return new MobileTaskListView(factory);
+    return new MobileTaskListView();
   }
 
   @Override
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplTablet.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplTablet.java
index 65257ca..d0b3b04 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplTablet.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ClientFactoryImplTablet.java
@@ -15,13 +15,15 @@
  */
 package com.google.gwt.sample.mobilewebapp.client;
 
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskReadView;
+import com.google.gwt.activity.shared.ActivityMapper;
+import com.google.gwt.sample.mobilewebapp.client.activity.AppActivityMapperTablet;
 import com.google.gwt.sample.mobilewebapp.client.tablet.MobileWebAppShellTablet;
 import com.google.gwt.sample.mobilewebapp.client.tablet.TabletTaskEditView;
 import com.google.gwt.sample.mobilewebapp.client.tablet.TabletTaskReadView;
-import com.google.gwt.sample.mobilewebapp.client.ui.OrientationHelper;
-import com.google.gwt.sample.mobilewebapp.client.ui.WindowBasedOrientationHelper;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskReadView;
+import com.google.gwt.sample.ui.client.OrientationHelper;
+import com.google.gwt.sample.ui.client.WindowBasedOrientationHelper;
 
 /**
  * Tablet version of {@link ClientFactory}.
@@ -30,8 +32,13 @@
   private final OrientationHelper orientationHelper = new WindowBasedOrientationHelper();
 
   @Override
+  protected ActivityMapper createActivityMapper() {
+    return new AppActivityMapperTablet(super.createActivityMapper(), orientationHelper);
+  }
+  
+  @Override
   protected MobileWebAppShell createShell() {
-    return new MobileWebAppShellTablet(getEventBus(), orientationHelper, getTaskListView());
+    return new MobileWebAppShellTablet(this, orientationHelper, getTaskListView());
   }
 
   @Override
@@ -43,18 +50,4 @@
   protected TaskReadView createTaskReadView() {
     return new TabletTaskReadView();
   }
-
-  @Override
-  protected Provider<Boolean> getIsTaskListIncludedProvider() {
-    /*
-     * TODO(rjrjr) This is awkward. Seems like we should be wrapping the tablet
-     * version of the ActivityMapper or something.
-     */
-    return new Provider<Boolean>() {
-      @Override
-      public Boolean get() {
-        return !orientationHelper.isPortrait();
-      }
-    };
-  }
 }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/MobileWebApp.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/MobileWebApp.java
index c86304c..a31fe8c 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/MobileWebApp.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/MobileWebApp.java
@@ -25,7 +25,6 @@
 public class MobileWebApp implements EntryPoint {
   public void onModuleLoad() {
     ClientFactory clientFactory = GWT.create(ClientFactory.class);
-    clientFactory.init();
     clientFactory.getApp().run(RootLayoutPanel.get());
   }
 }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShell.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShell.java
index 4b1c792..596b904 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShell.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/MobileWebAppShell.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -15,7 +15,6 @@
  */
 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;
 
@@ -24,10 +23,16 @@
  */
 public interface MobileWebAppShell extends AcceptsOneWidget, IsWidget {
   /**
-   * Set the handler to invoke when the add button is pressed. If no handler is
-   * specified, the button is hidden.
+   * Show or hide the Add button, which must post an
+   * {@link com.google.gwt.sample.mobilewebapp.client.event.ActionEvent
+   * ActionEvent} with a source name of
+   * {@link com.google.gwt.sample.mobilewebapp.client.event.ActionNames#ADD_TASK
+   * ActionEventNames.ADD_TASK} when clicked.
+   * <p>
+   * TODO: this is ridiculous, and is in the process of being changed to
+   * something much simpler
    * 
-   * @param handler the handler to add to the button, or null to hide
+   * @param visible to show the button, false to hide it
    */
-  void setAddButtonHandler(ClickHandler handler);
+  void setAddButtonVisible(boolean visible);
 }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapper.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapper.java
index 36c3000..7fcf29b 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapper.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapper.java
@@ -15,13 +15,16 @@
  */
 package com.google.gwt.sample.mobilewebapp.client.activity;
 
+import com.google.gwt.activity.shared.AbstractActivity;
 import com.google.gwt.activity.shared.Activity;
 import com.google.gwt.activity.shared.ActivityMapper;
+import com.google.gwt.event.shared.EventBus;
 import com.google.gwt.place.shared.Place;
 import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
-import com.google.gwt.sample.mobilewebapp.client.Provider;
-import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
-import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskPlace;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListPlace;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListPresenter;
+import com.google.gwt.user.client.ui.AcceptsOneWidget;
 
 /**
  * A mapping of places to activities used by this application.
@@ -29,25 +32,34 @@
 public class AppActivityMapper implements ActivityMapper {
 
   private final ClientFactory clientFactory;
-  private final Provider<Boolean> isTaskListIncluded;
 
-  public AppActivityMapper(ClientFactory clientFactory, Provider<Boolean> isTaskListIncluded) {
-    this.isTaskListIncluded = isTaskListIncluded;
+  public AppActivityMapper(ClientFactory clientFactory) {
     this.clientFactory = clientFactory;
   }
 
-  public Activity getActivity(Place place) {
+  public Activity getActivity(final Place place) {
     if (place instanceof TaskListPlace) {
       // The list of tasks.
-      if (!isTaskListIncluded.get()) {
-        // Do not start a task list activity if the task list is always visible.
-        return new TaskListActivity(clientFactory, (TaskListPlace) place);
-      }
-    } else if (place instanceof TaskEditPlace) {
-      // Editable view of a task.
-      return new TaskEditActivity(clientFactory, (TaskEditPlace) place);
+      return new AbstractActivity() {
+        @Override
+        public void start(AcceptsOneWidget panel, EventBus eventBus) {
+          TaskListPresenter presenter = new TaskListPresenter(clientFactory, (TaskListPlace) place);
+          presenter.start(eventBus);
+          panel.setWidget(presenter);
+        }
+
+        /*
+         * Note no call to presenter.stop(). The TaskListViews do that
+         * themselves as a side effect of setPresenter.
+         */
+      };
     }
+
+    if (place instanceof TaskPlace) {
+      // Editable view of a task.
+      return new TaskActivity(clientFactory, (TaskPlace) place);
+    }
+
     return null;
   }
-
 }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapperTablet.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapperTablet.java
new file mode 100644
index 0000000..65ccfd6
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/AppActivityMapperTablet.java
@@ -0,0 +1,46 @@
+/*
+ * 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.presenter.tasklist.TaskListPlace;
+import com.google.gwt.sample.ui.client.OrientationHelper;
+
+/**
+ * A mapping of places to activities used by the table version of the
+ * application.
+ */
+public class AppActivityMapperTablet implements ActivityMapper {
+
+  private final ActivityMapper wrapped;
+  private final OrientationHelper orientationHelper;
+
+  public AppActivityMapperTablet(ActivityMapper wrapped, OrientationHelper orientationHelper) {
+    this.wrapped = wrapped;
+    this.orientationHelper = orientationHelper;
+  }
+
+  public Activity getActivity(Place place) {
+    if (place instanceof TaskListPlace && !orientationHelper.isPortrait()) {
+      // The task list is always visible in landscape, so don't start one
+      return null;
+    }
+
+    return wrapped.getActivity(place);
+  }
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/AppPlaceHistoryMapper.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/AppPlaceHistoryMapper.java
new file mode 100644
index 0000000..cdba21e
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/AppPlaceHistoryMapper.java
@@ -0,0 +1,35 @@
+/*
+ * 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.activity;
+
+import com.google.gwt.place.shared.PlaceHistoryMapper;
+import com.google.gwt.place.shared.WithTokenizers;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskPlace;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListPlace;
+
+/**
+ * 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.
+ * <p>
+ * Its implementation is code generated based on the @WithTokenizers
+ * annotation.
+ */
+@WithTokenizers({TaskListPlace.Tokenizer.class, TaskPlace.Tokenizer.class})
+public interface AppPlaceHistoryMapper extends PlaceHistoryMapper {
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskActivity.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskActivity.java
new file mode 100644
index 0000000..c983816
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskActivity.java
@@ -0,0 +1,106 @@
+/*
+ * 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.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.event.TaskEditEvent;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskEditPresenter;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskPlace;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskReadPresenter;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.sample.ui.client.PresentsWidgets;
+import com.google.gwt.user.client.ui.AcceptsOneWidget;
+import com.google.web.bindery.event.shared.ResettableEventBus;
+
+/**
+ * An activity that shows details on a particular task, and allows the user to
+ * edit it.
+ */
+public class TaskActivity extends AbstractActivity {
+  private PresentsWidgets presenter;
+
+  private final TaskPlace place;
+
+  private final ClientFactory clientFactory;
+
+  private ResettableEventBus childEventBus;
+
+  /**
+   * Construct a new {@link TaskActivity}.
+   * 
+   * @param clientFactory the {@link ClientFactory} of shared resources
+   * @param place configuration for this activity
+   */
+  public TaskActivity(ClientFactory clientFactory, TaskPlace place) {
+    this.place = place;
+    this.clientFactory = clientFactory;
+  }
+
+  @Override
+  public String mayStop() {
+    return presenter.mayStop();
+  }
+
+  @Override
+  public void onCancel() {
+    presenter.stop();
+  }
+
+  @Override
+  public void onStop() {
+    childEventBus.removeHandlers();
+    presenter.stop();
+  }
+
+  public void start(final AcceptsOneWidget container, com.google.gwt.event.shared.EventBus eventBus) {
+    this.childEventBus = new ResettableEventBus(eventBus);
+    eventBus.addHandler(TaskEditEvent.TYPE, new TaskEditEvent.Handler() {
+      @Override
+      public void onTaskEdit(TaskEditEvent event) {
+        // Stop the read presenter
+        onStop();
+        presenter = startEdit(event.getReadOnlyTask());
+        container.setWidget(presenter);
+      }
+    });
+
+    if (place.getTaskId() == null) {
+      presenter = startCreate();
+    } else {
+      presenter = startDisplay(place);
+    }
+    container.setWidget(presenter);
+  }
+
+  private PresentsWidgets startCreate() {
+    PresentsWidgets rtn = new TaskEditPresenter(clientFactory);
+    rtn.start(childEventBus);
+    return rtn;
+  }
+
+  private PresentsWidgets startDisplay(TaskPlace place) {
+    PresentsWidgets rtn = new TaskReadPresenter(clientFactory, place);
+    rtn.start(childEventBus);
+    return rtn;
+  }
+
+  private PresentsWidgets startEdit(TaskProxy readOnlyTask) {
+    PresentsWidgets rtn = new TaskEditPresenter(clientFactory, readOnlyTask);
+    rtn.start(childEventBus);
+    return rtn;
+  }
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditActivity.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditActivity.java
deleted file mode 100644
index 62d8159..0000000
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditActivity.java
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
- * 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.event.EditingCanceledEvent;
-import com.google.gwt.sample.mobilewebapp.client.event.TaskSavedEvent;
-import com.google.gwt.sample.mobilewebapp.client.place.TaskEditPlace;
-import com.google.gwt.sample.mobilewebapp.client.ui.SoundEffects;
-import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
-import com.google.gwt.sample.mobilewebapp.shared.TaskRequest;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.AcceptsOneWidget;
-import com.google.web.bindery.requestfactory.shared.Receiver;
-import com.google.web.bindery.requestfactory.shared.Request;
-import com.google.web.bindery.requestfactory.shared.ServerFailure;
-
-import java.util.Set;
-import java.util.logging.Logger;
-
-import javax.validation.ConstraintViolation;
-
-/**
- * Activity that presents a task to be edited.
- */
-public class TaskEditActivity extends AbstractActivity implements TaskEditView.Presenter,
-    TaskReadView.Presenter {
-
-  private static final Logger log = Logger.getLogger(TaskEditActivity.class.getName());
-  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;
-
-  /**
-   * Indicates whether the activity is editing an existing task or creating a
-   * new task.
-   */
-  private boolean isEditing;
-
-  /**
-   * The current task being displayed, might not be possible to edit it.
-   */
-  private TaskProxy readOnlyTask;
-
-  /**
-   * The current task being edited, provided by RequestFactory.
-   */
-  private TaskProxy editTask;
-
-  /**
-   * The ID of the current task being edited.
-   */
-  private final Long taskId;
-
-  /**
-   * The request used to persist the modified task.
-   */
-  private Request<Void> taskPersistRequest;
-  private AcceptsOneWidget container;
-
-  /**
-   * Construct a new {@link TaskEditActivity}.
-   *
-   * @param clientFactory the {@link ClientFactory} of shared resources
-   * @param place configuration for this activity
-   */
-  public TaskEditActivity(ClientFactory clientFactory, TaskEditPlace place) {
-    this.taskId = place.getTaskId();
-    this.readOnlyTask = place.getTask();
-    this.clientFactory = clientFactory;
-  }
-
-  public void deleteTask() {
-    if (isEditing) {
-      doDeleteTask();
-    } else {
-      doCancelTask();
-    }
-  }
-
-  @Override
-  public void editTask() {
-    // Load the existing task.
-    final TaskEditView editView = clientFactory.getTaskEditView();
-  
-    if (taskId == null) {
-      isEditing = false;
-      editView.setEditing(false);
-      TaskRequest request = clientFactory.getRequestFactory().taskRequest();
-      editTask = request.create(TaskProxy.class);
-      editView.getEditorDriver().edit(editTask, request);
-    } else {
-      editView.setLocked(true);
-      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;
-              }
-  
-              // Task not found.
-              if (response == null) {
-                Window.alert("The task with id '" + taskId + "' could not be found."
-                    + " Please select a different task from the task list.");
-                doCancelTask();
-                return;
-              }
-  
-              // Show the task.
-              editTask = response;
-              editView.getEditorDriver().edit(response,
-                  clientFactory.getRequestFactory().taskRequest());
-              editView.setLocked(false);
-            }
-          });
-    }
-    container.setWidget(editView);
-  }
-
-  @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() {
-    // Flush the changes into the editable task.
-    TaskRequest context = (TaskRequest) clientFactory.getTaskEditView().getEditorDriver().flush();
-
-    /*
-     * Create a persist request the first time we try to save this task. If a
-     * request already exists, reuse it.
-     */
-    if (taskPersistRequest == null) {
-      taskPersistRequest = context.persist().using(editTask);
-    }
-
-    // Fire the request.
-    taskPersistRequest.fire(new Receiver<Void>() {
-      @Override
-      public void onConstraintViolation(Set<ConstraintViolation<?>> violations) {
-        handleConstraintViolations(violations);
-      }
-
-      @Override
-      public void onSuccess(Void response) {
-        // Notify the user that the task was updated.
-        TaskEditActivity.this.notify("Task Saved");
-
-        // Return to the task list.
-        clientFactory.getEventBus().fireEvent(new TaskSavedEvent());
-      }
-    });
-  }
-
-  public void start(AcceptsOneWidget container, EventBus eventBus) {
-    this.container = container;
-    // Prefetch the sounds used in this activity.
-    SoundEffects.get().prefetchError();
-
-    // Hide the 'add' button in the shell.
-    clientFactory.getShell().setAddButtonHandler(null);
-
-    // Set the presenter on the views.
-    final TaskEditView editView = clientFactory.getTaskEditView();
-    editView.setPresenter(this);
-    editView.setNameViolation(null);
-
-    final TaskReadView readView = clientFactory.getTaskReadView();
-    readView.setPresenter(this);
-
-    if (taskId == null) {
-      editTask();
-      return;
-    } else {
-      // Lock the display until the task is loaded.
-      isEditing = true;
-      editView.setEditing(true);
-
-      // Try to load the task from local storage.
-      if (readOnlyTask == null) {
-        readOnlyTask = clientFactory.getTaskProxyLocalStorage().getTask(taskId);
-      }
-
-      if (readOnlyTask == null) {
-        // 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;
-                }
-
-                // Task not found.
-                if (response == null) {
-                  Window.alert("The task with id '" + taskId + "' could not be found."
-                      + " Please select a different task from the task list.");
-                  doCancelTask();
-                  return;
-                }
-
-                // Show the task.
-                readOnlyTask = response;
-                readView.getEditorDriver().edit(response);
-              }
-            });
-      } else {
-        // Use the task that was passed with the place.
-        readView.getEditorDriver().edit(readOnlyTask);
-      }
-    }
-
-    // Display the view.
-    container.setWidget(readView);
-  }
-
-  /**
-   * Cancel the current task.
-   */
-  private void doCancelTask() {
-    clientFactory.getEventBus().fireEvent(new EditingCanceledEvent());
-  }
-
-  /**
-   * Delete the current task.
-   */
-  private void doDeleteTask() {
-    if (editTask == null) {
-      return;
-    }
-
-    // Delete the task in the data store.
-    final TaskProxy toDelete = this.editTask;
-    clientFactory.getRequestFactory().taskRequest().remove().using(toDelete).fire(
-        new Receiver<Void>() {
-          @Override
-          public void onFailure(ServerFailure error) {
-            Window.alert("An error occurred on the server while deleting this task: \"."
-                + error.getMessage() + "\".");
-          }
-
-          @Override
-          public void onSuccess(Void response) {
-            onTaskDeleted();
-          }
-        });
-  }
-
-  /**
-   * Handle constraint violations.
-   */
-  private void handleConstraintViolations(Set<ConstraintViolation<?>> violations) {
-    // Display the violations.
-    clientFactory.getTaskEditView().getEditorDriver().setConstraintViolations(violations);
-
-    // Play a sound.
-    SoundEffects.get().playError();
-  }
-
-  /**
-   * Notify the user of a message.
-   * 
-   * @param message the message to display
-   */
-  private void notify(String message) {
-    // TODO Add notification pop-up
-    log.fine("Tell the user: " + message);
-  }
-
-  /**
-   * Called when a task has been successfully deleted.
-   */
-  private void onTaskDeleted() {
-    // Notify the user that the task was deleted.
-    notify("Task Deleted");
-
-    // Return to the task list.
-    clientFactory.getEventBus().fireEvent(new TaskSavedEvent());
-  }
-}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskListActivity.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskListActivity.java
deleted file mode 100644
index e543e98..0000000
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskListActivity.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * 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.sample.mobilewebapp.client.ClientFactory;
-import com.google.gwt.sample.mobilewebapp.client.event.AddTaskEvent;
-import com.google.gwt.sample.mobilewebapp.client.event.ShowTaskEvent;
-import com.google.gwt.sample.mobilewebapp.client.place.TaskListPlace;
-import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
-import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.ui.AcceptsOneWidget;
-import com.google.web.bindery.requestfactory.shared.Receiver;
-import com.google.web.bindery.requestfactory.shared.ServerFailure;
-
-import java.util.Collections;
-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);
-  }
-
-  /**
-   * The delay in milliseconds between calls to refresh the task list.
-   */
-  private static final int REFRESH_DELAY = 5000;
-
-  /**
-   * The handler that handlers add button clicks.
-   */
-  private final ClickHandler addButtonHandler = new ClickHandler() {
-    public void onClick(ClickEvent event) {
-      clientFactory.getEventBus().fireEvent(new AddTaskEvent());
-    }
-  };
-
-  /**
-   * 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;
-
-  public TaskListActivity(ClientFactory clientFactory, boolean clearTaskList) {
-    this.clientFactory = clientFactory;
-    this.clearTaskList = clearTaskList;
-  }
-
-  /**
-   * Construct a new {@link TaskListActivity}.
-   * 
-   * @param clientFactory the {@link ClientFactory} of shared resources
-   * @param place configuration for this activity
-   */
-  public TaskListActivity(ClientFactory clientFactory, TaskListPlace place) {
-    this(clientFactory, place.isTaskListStale());
-  }
-
-  public ClickHandler getAddButtonHandler() {
-    return addButtonHandler;
-  }
-
-  @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.getEventBus().fireEvent(new ShowTaskEvent(selected));
-  }
-
-  public void start(AcceptsOneWidget container, EventBus eventBus) {
-    // Add a handler to the 'add' button in the shell.
-    clientFactory.getShell().setAddButtonHandler(addButtonHandler);
-
-    // 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
-    List<TaskProxy> list = clientFactory.getTaskProxyLocalStorage().getTasks();
-    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) {
-              response = Collections.<TaskProxy> emptyList();
-            }
-            setTasks(response);
-
-            // save the response to storage
-            clientFactory.getTaskProxyLocalStorage().setTasks(response);
-
-            // 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/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.java
index 4bae38c..81d6677 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskEditView.java
@@ -36,9 +36,9 @@
 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.client.activity.TaskEditView;
 import com.google.gwt.sample.mobilewebapp.client.ui.DateButton;
 import com.google.gwt.sample.mobilewebapp.client.ui.EditorDecorator;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskEditView;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxyImpl;
 import com.google.gwt.uibinder.client.UiBinder;
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.java
index 69fac21..f93acf2 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskListView.java
@@ -16,7 +16,7 @@
 package com.google.gwt.sample.mobilewebapp.client.desktop;
 
 import com.google.gwt.cell.client.DateCell;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListView;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
 import com.google.gwt.user.cellview.client.Column;
 import com.google.gwt.user.cellview.client.DataGrid;
@@ -106,8 +106,10 @@
     taskList.setVisibleRangeAndClearData(taskList.getVisibleRange(), true);
   }
 
-  @Override
   public void setPresenter(Presenter presenter) {
+    if (this.presenter != null) {
+      this.presenter.stop();
+    }
     this.presenter = presenter;
   }
 
@@ -119,16 +121,4 @@
   public void setTasks(List<TaskProxy> tasks) {
     taskList.setRowData(tasks);
   }
-
-  @Override
-  public void start() {
-    //TODO
-    throw new UnsupportedOperationException("Auto-generated method stub");
-  }
-
-  @Override
-  public void stop() {
-    //TODO
-    throw new UnsupportedOperationException("Auto-generated method stub");
-  }
 }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskReadView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskReadView.java
index 9c6eb11..290dff9 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskReadView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskReadView.java
@@ -19,7 +19,7 @@
 import com.google.gwt.editor.client.SimpleBeanEditorDriver;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskReadView;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskReadView;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -32,7 +32,7 @@
 import com.google.gwt.user.client.ui.Widget;
 
 /**
- * View used to edit a task.
+ * View used to see the details of a task.
  */
 public class DesktopTaskReadView extends Composite implements TaskReadView {
 
@@ -42,8 +42,7 @@
   interface DesktopTaskReadViewUiBinder extends UiBinder<Widget, DesktopTaskReadView> {
   }
 
-
-  interface Driver extends SimpleBeanEditorDriver<TaskProxy, DesktopTaskReadView> {
+  interface EditorDriver extends SimpleBeanEditorDriver<TaskProxy, DesktopTaskReadView> {
   }
 
   /**
@@ -68,9 +67,9 @@
   @UiField
   Label notesEditor;
   @UiField
-  Button saveButton;
+  Button editButton;
 
-  private final Driver driver = GWT.create(Driver.class);
+  private final EditorDriver editorDriver = GWT.create(EditorDriver.class);
 
   /**
    * The {@link TaskReadView.Presenter} for this view.
@@ -82,10 +81,9 @@
    */
   public DesktopTaskReadView() {
     initWidget(uiBinder.createAndBindUi(this));
-    driver.initialize(this);
-
+    editorDriver.initialize(this);
     // Create a new task or modify the current task when done is pressed.
-    saveButton.addClickHandler(new ClickHandler() {
+    editButton.addClickHandler(new ClickHandler() {
       public void onClick(ClickEvent event) {
         if (presenter != null) {
           presenter.editTask();
@@ -95,7 +93,7 @@
   }
 
   public SimpleBeanEditorDriver<TaskProxy, ?> getEditorDriver() {
-    return driver;
+    return editorDriver;
   }
 
   public void setPresenter(Presenter presenter) {
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskReadView.ui.xml b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskReadView.ui.xml
index 941bc98..bf08bab 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskReadView.ui.xml
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/DesktopTaskReadView.ui.xml
@@ -127,7 +127,7 @@
               colspan='2'
               align='center'>
               <g:Button
-                ui:field="saveButton"
+                ui:field="editButton"
                 addStyleNames="{style.button} {style.saveButton}">Edit</g:Button>
             </td>
           </tr>
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.java
index 117dfec..fd53bf9 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/desktop/MobileWebAppShellDesktop.java
@@ -15,7 +15,6 @@
  */
 package com.google.gwt.sample.mobilewebapp.client.desktop;
 
-import com.google.gwt.canvas.dom.client.CssColor;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.dom.client.VideoElement;
@@ -26,14 +25,12 @@
 import com.google.gwt.place.shared.PlaceChangeEvent;
 import com.google.gwt.place.shared.PlaceController;
 import com.google.gwt.sample.mobilewebapp.client.MobileWebAppShell;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
-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.activity.TaskListView;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskReadView;
-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.sample.mobilewebapp.presenter.task.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskPlace;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskReadView;
+import com.google.gwt.sample.mobilewebapp.presenter.taskchart.TaskChartPresenter;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListPlace;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListView;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.cellview.client.CellList;
@@ -57,7 +54,6 @@
 import com.google.web.bindery.event.shared.EventBus;
 
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.List;
 
 /**
@@ -131,11 +127,6 @@
   private boolean firstContentWidget = true;
 
   /**
-   * A pie chart showing a snapshot of the tasks.
-   */
-  private PieChart pieChart;
-
-  /**
    * The {@link DialogBox} used to display the tutorial.
    */
   private PopupPanel tutoralPopup;
@@ -147,10 +138,10 @@
 
   /**
    * Construct a new {@link MobileWebAppShellDesktop}.
-   * @param clientFactory the {@link ClientFactory} of shared resources
    */
-  public MobileWebAppShellDesktop(EventBus bus, final PlaceController placeController,
-      TaskListView taskListView, TaskEditView taskEditView, TaskReadView taskReadView) {
+  public MobileWebAppShellDesktop(EventBus bus, TaskChartPresenter pieChart,
+      final PlaceController placeController, TaskListView taskListView, TaskEditView taskEditView,
+      TaskReadView taskReadView) {
 
     // Initialize the main menu.
     Resources resources = GWT.create(Resources.class);
@@ -169,7 +160,7 @@
         return p instanceof TaskListPlace;
       }
     });
-    menuItems.add(new MainMenuItem("Add Task", TaskEditPlace.getTaskCreatePlace()));
+    menuItems.add(new MainMenuItem("Add Task", TaskPlace.getTaskCreatePlace()));
     mainMenu.setRowData(menuItems);
 
     // Choose a place when a menu item is selected.
@@ -206,7 +197,6 @@
     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.
@@ -217,36 +207,27 @@
       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);
+      Widget pieWidget = pieChart.asWidget();
+      pieWidget.setWidth("90%");
+      pieWidget.setHeight("90%");
+      pieWidget.getElement().getStyle().setMarginLeft(5.0, Unit.PCT);
+      pieWidget.getElement().getStyle().setMarginTop(5.0, Unit.PCT);
+      
       pieChartContainer.setWidget(pieChart);
     }
 
-    // Initialize the add button.
-    setAddButtonHandler(null);
-
     /*
      * Add all views to the DeckLayoutPanel so we can animate between them.
-     * Using a DeckLayoutPanel here works because we only have a few views, and we
-     * always know that the task views 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.
+     * Using a DeckLayoutPanel here works because we only have a few views, and
+     * we always know that the task views 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(taskListView);
     contentContainer.add(taskReadView);
     contentContainer.add(taskEditView);
     contentContainer.setAnimationDuration(800);
 
-    // Listen for events from the task list activity.
-    bus.addHandler(TaskListUpdateEvent.getType(),
-        new TaskListActivity.TaskListUpdateHandler() {
-          public void onTaskListUpdated(TaskListUpdateEvent event) {
-            updatePieChart(event.getTasks());
-          }
-        });
-
     // Show a tutorial when the help link is clicked.
     helpLink.addClickHandler(new ClickHandler() {
       public void onClick(ClickEvent event) {
@@ -255,13 +236,8 @@
     });
   }
 
-  /**
-   * 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) {
+  @Override
+  public void setAddButtonVisible(boolean isVisible) {
     // No-op: Adding a task is handled in the main menu.
   }
 
@@ -347,52 +323,4 @@
     tutoralPopup = popup;
     popup.center();
   }
-
-  /**
-   * 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();
-    if (pastDue > 0) {
-      pieChart.addSlice(pastDue, CssColor.make(255, 100, 100));
-    }
-    if (dueSoon > 0) {
-      pieChart.addSlice(dueSoon, CssColor.make(255, 200, 100));
-    }
-    if (onTime > 0) {
-      pieChart.addSlice(onTime, CssColor.make(100, 255, 100));
-    }
-    if (noDate > 0) {
-      pieChart.addSlice(noDate, CssColor.make(200, 200, 200));
-    }
-    pieChart.redraw();
-  }
 }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/ActionEvent.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/ActionEvent.java
new file mode 100644
index 0000000..7dcd011
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/ActionEvent.java
@@ -0,0 +1,67 @@
+/*
+ * 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.event;
+
+import com.google.web.bindery.event.shared.Event;
+import com.google.web.bindery.event.shared.EventBus;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+
+
+/**
+ * Fired when the user wants the app to do something. Action events are fired
+ * with a string name, and handlers monitor the event bus for them based on that
+ * name.
+ */
+public class ActionEvent extends Event<ActionEvent.Handler> {
+
+  /**
+   * Implemented by objects that handle {@link ActionEvent}.
+   */
+  public interface Handler {
+    void onAction(ActionEvent event);
+  }
+
+  /**
+   * The event type.
+   */
+  private static final Type<ActionEvent.Handler> TYPE = new Type<ActionEvent.Handler>();
+
+  public static void fire(EventBus eventBus, String sourceName) {
+    eventBus.fireEventFromSource(new ActionEvent(), sourceName);
+  }
+
+  public static HandlerRegistration register(EventBus eventBus, String sourceName, Handler handler) {
+    return eventBus.addHandlerToSource(TYPE, sourceName, handler);
+  }
+
+  /**
+   * Protected contructor to encourage the use of
+   * {@link #fire(EventBus, String)}.
+   */
+  protected ActionEvent() {
+  }
+
+  @Override
+  public final Type<ActionEvent.Handler> getAssociatedType() {
+    return TYPE;
+  }
+
+  @Override
+  protected void dispatch(ActionEvent.Handler handler) {
+    handler.onAction(this);
+  }
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/Provider.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/ActionNames.java
similarity index 65%
rename from samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/Provider.java
rename to samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/ActionNames.java
index 7ce2b32..5a29f9f 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/Provider.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/ActionNames.java
@@ -13,11 +13,14 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.sample.mobilewebapp.client;
+package com.google.gwt.sample.mobilewebapp.client.event;
 
 /**
- * Just like javax.inject.Provider, makes things.
+ * Names of the {@link ActionEvent}s used in this app.
  */
-public interface Provider<T> {
-  T get();
-}
\ No newline at end of file
+public interface ActionNames {
+  final String ADD_TASK = "addTask";
+  final String EDITING_CANCELED = "editingCanceled";
+  final String GO_HOME = "goHome";
+  final String TASK_SAVED = "taskSaved";
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/ShowTaskEvent.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/ShowTaskEvent.java
index 9e0ea11..feeccce 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/ShowTaskEvent.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/ShowTaskEvent.java
@@ -32,17 +32,17 @@
     void onShowTask(ShowTaskEvent event);
   }
 
-  private final TaskProxy task;
-  
   /**
    * The event type.
    */
   public static final Type<ShowTaskEvent.Handler> TYPE = new Type<ShowTaskEvent.Handler>();
 
+  private final TaskProxy task;
+
   public ShowTaskEvent(TaskProxy task) {
     this.task = task;
   }
- 
+
   @Override
   public final Type<ShowTaskEvent.Handler> getAssociatedType() {
     return TYPE;
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/TaskEditEvent.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/TaskEditEvent.java
new file mode 100644
index 0000000..c1e0479
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/TaskEditEvent.java
@@ -0,0 +1,58 @@
+/*
+ * 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.event;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+/**
+ * Fired when the user wants to edit a task.
+ */
+public class TaskEditEvent extends GwtEvent<TaskEditEvent.Handler> {
+  /**
+   * Implemented by objects that handle {@link TaskEditEvent}.
+   */
+  public interface Handler extends EventHandler {
+    void onTaskEdit(TaskEditEvent event);
+  }
+
+  /**
+   * The event type.
+   */
+  public static final Type<TaskEditEvent.Handler> TYPE = new Type<TaskEditEvent.Handler>();
+
+  private final TaskProxy task;
+
+  public TaskEditEvent(TaskProxy task) {
+    this.task = task;
+  }
+
+  @Override
+  public final Type<TaskEditEvent.Handler> getAssociatedType() {
+    return TYPE;
+  }
+
+  public TaskProxy getReadOnlyTask() {
+    return task;
+  }
+
+  @Override
+  protected void dispatch(TaskEditEvent.Handler handler) {
+    handler.onTaskEdit(this);
+  }
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/TaskListUpdateEvent.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/TaskListUpdateEvent.java
new file mode 100644
index 0000000..22a29a2
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/event/TaskListUpdateEvent.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.event;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+import java.util.List;
+
+/**
+ * Event fired when the task list is updated.
+ */
+public class TaskListUpdateEvent extends GwtEvent<TaskListUpdateEvent.Handler> {
+
+  /**
+   * Handler for {@link TaskListUpdateEvent}.
+   */
+  public interface Handler extends EventHandler {
+  
+    /**
+     * Called when the task list is updated.
+     */
+    void onTaskListUpdated(TaskListUpdateEvent event);
+  }
+
+  public static final Type<TaskListUpdateEvent.Handler> TYPE = new Type<TaskListUpdateEvent.Handler>();
+
+  private final List<TaskProxy> tasks;
+
+  public TaskListUpdateEvent(List<TaskProxy> tasks) {
+    this.tasks = tasks;
+  }
+
+  @Override
+  public Type<TaskListUpdateEvent.Handler> getAssociatedType() {
+    return TYPE;
+  }
+
+  public List<TaskProxy> getTasks() {
+    return tasks;
+  }
+
+  @Override
+  protected void dispatch(TaskListUpdateEvent.Handler handler) {
+    handler.onTaskListUpdated(this);
+  }
+}
\ No newline at end of file
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.java
index 449e136..ee99f4a 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskEditView.java
@@ -19,9 +19,9 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
 import com.google.gwt.sample.mobilewebapp.client.ui.DateButton;
 import com.google.gwt.sample.mobilewebapp.client.ui.EditorDecorator;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskEditView;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.java
index 5d7b1ce..3fe8f33 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskListView.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -16,8 +16,7 @@
 package com.google.gwt.sample.mobilewebapp.client.mobile;
 
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.sample.mobilewebapp.client.ProvidesPresenter;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListView;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -27,7 +26,6 @@
 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 java.util.List;
 
@@ -38,35 +36,27 @@
   /**
    * Resources used by the mobile CellList.
    */
-  static interface CellListResources extends CellList.Resources {
+  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 {
+  interface CellListStyle extends CellList.Style {
   }
 
   /**
    * The UiBinder interface.
    */
-  interface MobileTaskListViewUiBinder extends
-      UiBinder<Widget, MobileTaskListView> {
+  interface MobileTaskListViewUiBinder extends UiBinder<Widget, MobileTaskListView> {
   }
 
   /**
-   * Provides a new presenter for each session with this view.
-   */
-  private final ProvidesPresenter<TaskListView.Presenter, TaskListView> presenterFactory;
-
-  /**
    * The UiBinder used to generate the view.
    */
-  private static MobileTaskListViewUiBinder uiBinder = GWT
-      .create(MobileTaskListViewUiBinder.class);
+  private static MobileTaskListViewUiBinder uiBinder = GWT.create(MobileTaskListViewUiBinder.class);
 
   /**
    * Displays the list of tasks.
@@ -82,9 +72,8 @@
   /**
    * Construct a new {@link MobileTaskListView}.
    */
-  public MobileTaskListView(ProvidesPresenter<TaskListView.Presenter, TaskListView> presenterFactory) {
-    this.presenterFactory = presenterFactory;
-    
+  public MobileTaskListView() {
+
     // Create the CellList.
     CellListResources cellListRes = GWT.create(CellListResources.class);
     taskList = new CellList<TaskProxy>(new TaskProxyCell(), cellListRes);
@@ -97,15 +86,14 @@
      */
     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());
-            }
-          }
-        });
+    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));
@@ -116,24 +104,13 @@
   }
 
   public void setPresenter(Presenter presenter) {
+    if (this.presenter != null) {
+      this.presenter.stop();
+    }
     this.presenter = presenter;
   }
 
-  public void setSelectionModel(SelectionModel<TaskProxy> selectionModel) {
-    taskList.setSelectionModel(selectionModel);
-  }
-
   public void setTasks(List<TaskProxy> tasks) {
     taskList.setRowData(tasks);
   }
-
-  @Override
-  public void start() {
-    setPresenter(presenterFactory.getPresenter(this));
-  }
-
-  @Override
-  public void stop() {
-    presenter.onStop();
-  }
 }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskReadView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskReadView.java
index c901caf..695db93 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskReadView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileTaskReadView.java
@@ -19,7 +19,7 @@
 import com.google.gwt.editor.client.SimpleBeanEditorDriver;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskReadView;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskReadView;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -89,7 +89,7 @@
   public SimpleBeanEditorDriver<TaskProxy, ?> getEditorDriver() {
     return driver;
   }
-
+ 
   public void setPresenter(Presenter presenter) {
     this.presenter = presenter;
   }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.java
index 1521ba6..758e0ee 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/MobileWebAppShellMobile.java
@@ -21,14 +21,13 @@
 import com.google.gwt.dom.client.Style.Unit;
 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.HandlerRegistration;
 import com.google.gwt.sample.mobilewebapp.client.MobileWebAppShell;
-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.activity.TaskReadView;
-import com.google.gwt.sample.mobilewebapp.client.event.GoHomeEvent;
-import com.google.gwt.sample.mobilewebapp.client.ui.OrientationHelper;
+import com.google.gwt.sample.mobilewebapp.client.event.ActionEvent;
+import com.google.gwt.sample.mobilewebapp.client.event.ActionNames;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskEditView;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskReadView;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListView;
+import com.google.gwt.sample.ui.client.OrientationHelper;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.Command;
@@ -38,6 +37,7 @@
 import com.google.gwt.user.client.ui.LayoutPanel;
 import com.google.gwt.user.client.ui.ResizeComposite;
 import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.EventBus;
 
 /**
  * Mobile version of the UI shell.
@@ -47,8 +47,8 @@
   interface MobileWebAppShellMobileUiBinder extends UiBinder<Widget, MobileWebAppShellMobile> {
   }
 
-  private static MobileWebAppShellMobileUiBinder uiBinder =
-      GWT.create(MobileWebAppShellMobileUiBinder.class);
+  private static MobileWebAppShellMobileUiBinder uiBinder = GWT
+      .create(MobileWebAppShellMobileUiBinder.class);
 
   /**
    * The width of the menu bar in landscape mode in EX.
@@ -97,11 +97,6 @@
   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;
@@ -109,20 +104,27 @@
   /**
    * Construct a new {@link MobileWebAppShellMobile}.
    */
-  public MobileWebAppShellMobile(OrientationHelper orientationHelper, TaskListView taskListView,
-      TaskEditView taskEditView, TaskReadView taskReadView, final EventBus eventBus) {
+  public MobileWebAppShellMobile(final EventBus eventBus, OrientationHelper orientationHelper,
+      TaskListView taskListView, TaskEditView taskEditView, TaskReadView taskReadView) {
 
     initWidget(uiBinder.createAndBindUi(this));
 
     // Initialize the add button.
-    setAddButtonHandler(null);
+    // Initialize the add button.
+    addButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        ActionEvent.fire(eventBus, ActionNames.ADD_TASK);
+      }
+    });
+    setAddButtonVisible(false);
 
     /*
      * Add all views to the DeckLayoutPanel so we can animate between them.
-     * Using a DeckLayoutPanel here works because we only have a few views, and we
-     * always know that the task views 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.
+     * Using a DeckLayoutPanel here works because we only have a few views, and
+     * we always know that the task views 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(taskListView);
     contentContainer.add(taskReadView);
@@ -142,35 +144,17 @@
     });
 
     // Return to the task list when the title is clicked.
-    titleBar.addDomHandler(new ClickHandler() {      
+    titleBar.addDomHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
-        eventBus.fireEvent(new GoHomeEvent());
+        ActionEvent.fire(eventBus, ActionNames.GO_HOME);
       }
     }, ClickEvent.getType());
   }
 
-  /**
-   * 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);
-    }
+  @Override
+  public void setAddButtonVisible(boolean visible) {
+    addButton.setVisible(visible);
   }
 
   /**
@@ -199,7 +183,8 @@
     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
+        .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,
@@ -215,7 +200,8 @@
     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
+        .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,
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/TaskProxyCell.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/TaskProxyCell.java
index c6b881c..d46aad5 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/TaskProxyCell.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/mobile/TaskProxyCell.java
@@ -30,7 +30,7 @@
 /**
  * A {@link com.google.gwt.cell.client.Cell} used to render a {@link TaskProxy}.
  */
-public class TaskProxyCell extends AbstractCell<TaskProxy> {
+class TaskProxyCell extends AbstractCell<TaskProxy> {
 
   /**
    * The template used by this cell.
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.java
index e98b290..49066ec 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/MobileWebAppShellTablet.java
@@ -18,14 +18,13 @@
 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.shared.EventBus;
-import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
 import com.google.gwt.sample.mobilewebapp.client.MobileWebAppShell;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskListView;
-import com.google.gwt.sample.mobilewebapp.client.event.AddTaskEvent;
-import com.google.gwt.sample.mobilewebapp.client.event.GoHomeEvent;
-import com.google.gwt.sample.mobilewebapp.client.ui.OrientationHelper;
+import com.google.gwt.sample.mobilewebapp.client.event.ActionEvent;
+import com.google.gwt.sample.mobilewebapp.client.event.ActionNames;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListPresenter;
+import com.google.gwt.sample.mobilewebapp.presenter.tasklist.TaskListView;
+import com.google.gwt.sample.ui.client.OrientationHelper;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.Command;
@@ -37,25 +36,29 @@
 import com.google.gwt.user.client.ui.ResizeComposite;
 import com.google.gwt.user.client.ui.SimplePanel;
 import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.EventBus;
 
 /**
  * Tablet version of the UI shell.
  * 
- * TODO(rjrjr): this thing needs a presenter. Not an activity. A presenter.
+ * TODO(rjrjr): this thing needs a presenter or two. Also, too much copy paste
+ * btw. this and the desktop version.
  */
 public class MobileWebAppShellTablet extends ResizeComposite implements MobileWebAppShell {
 
   interface MobileWebAppShellTabletUiBinder extends UiBinder<Widget, MobileWebAppShellTablet> {
   }
 
-  private static MobileWebAppShellTabletUiBinder uiBinder =
-      GWT.create(MobileWebAppShellTabletUiBinder.class);
+  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;
 
+  private final ClientFactory clientFactory;
+
   /**
    * The button used to add items.
    */
@@ -93,30 +96,23 @@
   Label titleLabel;
 
   /**
-   * 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;
 
-  private final EventBus eventBus;
-
   private final TaskListView taskListView;
 
   private boolean isShowingTaskList;
 
+  private final EventBus eventBus;
+
   /**
    * Construct a new {@link MobileWebAppShellTablet}.
-   * 
-   * @param clientFactory the {@link ClientFactory} of shared resources
    */
-  public MobileWebAppShellTablet(final EventBus eventBus, OrientationHelper orientationHelper,
+  public MobileWebAppShellTablet(ClientFactory clientFactory, OrientationHelper orientationHelper,
       TaskListView taskListView) {
-    this.eventBus = eventBus;
-
+    this.clientFactory = clientFactory;
+    this.eventBus = clientFactory.getEventBus();
     this.taskListView = taskListView;
 
     // Inject the tablet specific styles.
@@ -127,7 +123,13 @@
     initWidget(uiBinder.createAndBindUi(this));
 
     // Initialize the add button.
-    setAddButtonHandler(null);
+    addButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        ActionEvent.fire(eventBus, ActionNames.ADD_TASK);
+      }
+    });
+    setAddButtonVisible(false);
 
     orientationHelper.setCommands(this, new Command() {
       @Override
@@ -145,32 +147,14 @@
     titleLabel.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
-        eventBus.fireEvent(new GoHomeEvent());
+        ActionEvent.fire(eventBus, ActionNames.GO_HOME);
       }
     });
   }
 
-  /**
-   * 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);
-    }
+  @Override
+  public void setAddButtonVisible(boolean visible) {
+    addButton.setVisible(visible);
   }
 
   /**
@@ -183,12 +167,7 @@
 
     // If the content is null and we are in landscape mode, show the add button.
     if (content == null && isShowingTaskList) {
-      setAddButtonHandler(new ClickHandler() {
-        @Override
-        public void onClick(ClickEvent event) {
-          eventBus.fireEvent(new AddTaskEvent());
-        }
-      });
+      setAddButtonVisible(true);
     }
 
     // Do not animate the first time we show a widget.
@@ -202,12 +181,19 @@
     // Show the static task list view.
     splitPanel.setWidgetSize(taskListContainer, LANDSCAPE_TASK_LIST_WIDTH_PCT);
 
-    // TODO(rjrjr) View managing activity is an abomination
     if (!isShowingTaskList) {
-      taskListView.start();
       taskListContainer.add(taskListView);
-      // DeckLayoutPanel sets the display to none, so we need to clear it.
-      taskListView.asWidget().getElement().getStyle().clearDisplay();
+      TaskListPresenter taskListPresenter = new TaskListPresenter(clientFactory, false);
+
+      /*
+       * Sleaze alert: We know that TaskListPresenter doesn't add any event
+       * handlers to the bus If it did, we should have to give it a
+       * ResettableEventBus and clear it out on stop
+       */
+      taskListPresenter.start(eventBus);
+
+      // DeckLayoutPanel hides the view, so we need to show it.
+      taskListView.asWidget().setVisible(true);
       isShowingTaskList = true;
     }
 
@@ -223,8 +209,9 @@
   private void onShiftToPortrait() {
     // Hide the static task list view.
     if (isShowingTaskList) {
-      taskListView.stop();
       isShowingTaskList = false;
+      // We don't stop the presenter started in onShiftToLandscape,
+      // because we know TaskListView#setPresenter will do so for us.
     }
     splitPanel.setWidgetSize(taskListContainer, 0);
 
@@ -241,7 +228,7 @@
     // Ensure that something is displayed.
     Widget curWidget = contentContainer.getVisibleWidget();
     if (curWidget == null || curWidget == contentEmptyMessage) {
-      eventBus.fireEvent(new GoHomeEvent());
+      ActionEvent.fire(eventBus, ActionNames.GO_HOME);
       contentContainer.animate(0);
     }
   }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.java
index 025b80e..0320ef8 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskEditView.java
@@ -19,9 +19,9 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
 import com.google.gwt.sample.mobilewebapp.client.ui.DateButton;
 import com.google.gwt.sample.mobilewebapp.client.ui.EditorDecorator;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskEditView;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -35,7 +35,7 @@
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver;
 
 /**
- * View used to edit a task.
+ * Tablet version of the {@link TaskEditView}.
  */
 public class TabletTaskEditView extends Composite implements TaskEditView {
 
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskReadView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskReadView.java
index 84ceee1..d503129 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskReadView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/tablet/TabletTaskReadView.java
@@ -19,8 +19,7 @@
 import com.google.gwt.editor.client.SimpleBeanEditorDriver;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskEditView;
-import com.google.gwt.sample.mobilewebapp.client.activity.TaskReadView;
+import com.google.gwt.sample.mobilewebapp.presenter.task.TaskReadView;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -31,7 +30,7 @@
 import com.google.gwt.user.client.ui.Widget;
 
 /**
- * View used to edit a task.
+ * Tablet version of the {@link TaskReadView}.
  */
 public class TabletTaskReadView extends Composite implements TaskReadView {
 
@@ -68,7 +67,8 @@
   private final Driver driver = GWT.create(Driver.class);
 
   /**
-   * The {@link TaskEditView.Presenter} for this view.
+   * The {@link com.google.gwt.sample.mobilewebapp.presenter.task.TaskEditView.Presenter
+   * TaskEditView.Presenter} for this view.
    */
   private Presenter presenter;
 
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ui/PieChart.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ui/PieChart.java
new file mode 100644
index 0000000..3eb4bc4
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/ui/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.ui;
+
+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/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskEditPresenter.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskEditPresenter.java
new file mode 100644
index 0000000..81bf42c
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskEditPresenter.java
@@ -0,0 +1,285 @@
+/*
+ * 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.presenter.task;
+
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.event.ActionEvent;
+import com.google.gwt.sample.mobilewebapp.client.event.ActionNames;
+import com.google.gwt.sample.mobilewebapp.client.ui.SoundEffects;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.sample.mobilewebapp.shared.TaskRequest;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.EventBus;
+import com.google.web.bindery.requestfactory.shared.Receiver;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.ServerFailure;
+
+import java.util.Set;
+import java.util.logging.Logger;
+
+import javax.validation.ConstraintViolation;
+
+/**
+ * Drives a {@link TaskEditView} to fetch and edit a given task, or to create a
+ * new one.
+ */
+public class TaskEditPresenter implements TaskEditView.Presenter {
+
+  private static final Logger log = Logger.getLogger(TaskEditPresenter.class.getName());
+  private final ClientFactory clientFactory;
+
+  /**
+   * Indicates whether the activity is editing an existing task or creating a
+   * new task.
+   */
+  private boolean isEditing;
+
+  /**
+   * The current task being edited, provided by RequestFactory.
+   */
+  private TaskProxy editTask;
+
+  /**
+   * The ID of the current task being edited.
+   */
+  private final Long taskId;
+
+  /**
+   * The request used to persist the modified task.
+   */
+  private Request<Void> taskPersistRequest;
+  private EventBus eventBus;
+
+  /**
+   * For creating a new task.
+   */
+  public TaskEditPresenter(ClientFactory clientFactory) {
+    this.taskId = null;
+    this.clientFactory = clientFactory;
+    clientFactory.getTaskEditView().setPresenter(this);
+  }
+
+  /**
+   * For editing an existing task.
+   */
+  public TaskEditPresenter(ClientFactory clientFactory, TaskProxy readOnlyTask) {
+    /*
+     * TODO surely we can find a way to show the read-only values while waiting
+     * for the async fetch
+     */
+    this.taskId = readOnlyTask.getId();
+    this.clientFactory = clientFactory;
+    clientFactory.getTaskEditView().setPresenter(this);
+  }
+
+  @Override
+  public Widget asWidget() {
+    return getView().asWidget();
+  }
+
+  public void deleteTask() {
+    if (isEditing) {
+      doDeleteTask();
+    } else {
+      doCancelTask();
+    }
+  }
+
+  @Override
+  public String mayStop() {
+    if ((eventBus != null && editTask != null) && getView().getEditorDriver().isDirty()) {
+      return "Are you sure you want to discard these changes?";
+    }
+    return null;
+  }
+
+  public void saveTask() {
+    // Flush the changes into the editable task.
+    TaskRequest context = (TaskRequest) clientFactory.getTaskEditView().getEditorDriver().flush();
+
+    /*
+     * Create a persist request the first time we try to save this task. If a
+     * request already exists, reuse it.
+     */
+    if (taskPersistRequest == null) {
+      taskPersistRequest = context.persist().using(editTask);
+    }
+
+    // Fire the request.
+    taskPersistRequest.fire(new Receiver<Void>() {
+      @Override
+      public void onConstraintViolation(Set<ConstraintViolation<?>> violations) {
+        handleConstraintViolations(violations);
+      }
+
+      @Override
+      public void onSuccess(Void response) {
+        editTask = null;
+
+        // Notify the user that the task was updated.
+        TaskEditPresenter.this.notify("Task Saved");
+        
+        // Return to the task list.
+        ActionEvent.fire(eventBus, ActionNames.TASK_SAVED);
+      }
+    });
+  }
+
+  public void start(EventBus eventBus) {
+    this.eventBus = eventBus;
+    getView().setNameViolation(null);
+
+    // Prefetch the sounds used in this activity.
+    SoundEffects.get().prefetchError();
+
+    // Hide the 'add' button in the shell.
+    // TODO(rjrjr) Ick!
+    clientFactory.getShell().setAddButtonVisible(false);
+
+    if (taskId == null) {
+      startCreate();
+    } else {
+      startEdit();
+    }
+  }
+
+  @Override
+  public void stop() {
+    eventBus = null;
+    clientFactory.getTaskEditView().setLocked(false);
+  }
+
+  /**
+   * Cancel the current task.
+   */
+  private void doCancelTask() {
+    ActionEvent.fire(eventBus, ActionNames.EDITING_CANCELED);
+  }
+
+  /**
+   * Delete the current task.
+   */
+  private void doDeleteTask() {
+    if (editTask == null) {
+      return;
+    }
+
+    // Delete the task in the data store.
+    final TaskProxy toDelete = this.editTask;
+    clientFactory.getRequestFactory().taskRequest().remove().using(toDelete).fire(
+        new Receiver<Void>() {
+          @Override
+          public void onFailure(ServerFailure error) {
+            Window.alert("An error occurred on the server while deleting this task: \"."
+                + error.getMessage() + "\".");
+          }
+
+          @Override
+          public void onSuccess(Void response) {
+            onTaskDeleted();
+          }
+        });
+  }
+
+  private TaskEditView getView() {
+    return clientFactory.getTaskEditView();
+  }
+
+  /**
+   * Handle constraint violations.
+   */
+  private void handleConstraintViolations(Set<ConstraintViolation<?>> violations) {
+    // Display the violations.
+    getView().getEditorDriver().setConstraintViolations(violations);
+
+    // Play a sound.
+    SoundEffects.get().playError();
+  }
+
+  /**
+   * Notify the user of a message.
+   * 
+   * @param message the message to display
+   */
+  private void notify(String message) {
+    // TODO Add notification pop-up
+    log.fine("Tell the user: " + message);
+  }
+
+  /**
+   * Called when a task has been successfully deleted.
+   */
+  private void onTaskDeleted() {
+    // Notify the user that the task was deleted.
+    notify("Task Deleted");
+
+    // Return to the task list.
+    ActionEvent.fire(eventBus, ActionNames.TASK_SAVED);
+  }
+
+  private void startCreate() {
+    isEditing = false;
+    getView().setEditing(false);
+    TaskRequest request = clientFactory.getRequestFactory().taskRequest();
+    editTask = request.create(TaskProxy.class);
+    getView().getEditorDriver().edit(editTask, request);
+  }
+
+  private void startEdit() {
+    isEditing = true;
+    getView().setEditing(true);
+    // Lock the display until the task is loaded.
+    getView().setLocked(true);
+    clientFactory.getRequestFactory().taskRequest().findTask(this.taskId).fire(
+        new Receiver<TaskProxy>() {
+          @Override
+          public void onConstraintViolation(Set<ConstraintViolation<?>> violations) {
+            getView().setLocked(false);
+            getView().getEditorDriver().setConstraintViolations(violations);
+          }
+
+          @Override
+          public void onFailure(ServerFailure error) {
+            getView().setLocked(false);
+            doCancelTask();
+            super.onFailure(error);
+          }
+
+          @Override
+          public void onSuccess(TaskProxy response) {
+            // Early exit if we have already stopped.
+            if (eventBus == null) {
+              return;
+            }
+
+            // Task not found.
+            if (response == null) {
+              Window.alert("The task with id '" + taskId + "' could not be found."
+                  + " Please select a different task from the task list.");
+              doCancelTask();
+              return;
+            }
+
+            // Show the task.
+            editTask = response;
+            getView().getEditorDriver().edit(response,
+                clientFactory.getRequestFactory().taskRequest());
+            getView().setLocked(false);
+          }
+        });
+  }
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskEditView.java
similarity index 87%
rename from samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditView.java
rename to samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskEditView.java
index f1d4cde..859dfc8 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskEditView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskEditView.java
@@ -13,22 +13,23 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.sample.mobilewebapp.client.activity;
+package com.google.gwt.sample.mobilewebapp.presenter.task;
 
 import com.google.gwt.editor.client.Editor;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.sample.ui.client.PresentsWidgets;
 import com.google.gwt.user.client.ui.IsWidget;
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver;
 
 /**
- * A view of {@link TaskEditActivity}.
+ * Implemented by widgets that edit tasks.
  */
-public interface TaskEditView extends IsWidget, Editor<TaskProxy> {
+public interface TaskEditView extends Editor<TaskProxy>, IsWidget {
 
   /**
    * The presenter for this view.
    */
-  public static interface Presenter {
+  public interface Presenter extends PresentsWidgets {
     /**
      * Delete the current task or cancel the creation of a task.
      */
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskPlace.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskPlace.java
new file mode 100644
index 0000000..71d0477
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskPlace.java
@@ -0,0 +1,114 @@
+/*
+ * 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.presenter.task;
+
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.place.shared.PlaceTokenizer;
+import com.google.gwt.place.shared.Prefix;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+
+/**
+ * The place in the app that shows and edits details of a task.
+ */
+public class TaskPlace extends Place {
+
+  /**
+   * The tokenizer for this place.
+   */
+  @Prefix("edit")
+  public static class Tokenizer implements PlaceTokenizer<TaskPlace> {
+
+    private static final String NO_ID = "create";
+
+    public TaskPlace getPlace(String token) {
+      try {
+        // Parse the task ID from the URL.
+        Long taskId = Long.parseLong(token);
+        return new TaskPlace(taskId, null);
+      } catch (NumberFormatException e) {
+        // If the ID cannot be parsed, assume we are creating a task.
+        return TaskPlace.getTaskCreatePlace();
+      }
+    }
+
+    public String getToken(TaskPlace place) {
+      Long taskId = place.getTaskId();
+      return (taskId == null) ? NO_ID : taskId.toString();
+    }
+  }
+
+  /**
+   * The singleton instance of this place used for creation.
+   */
+  private static TaskPlace singleton;
+
+  /**
+   * Create an instance of {@link TaskPlace} associated with the specified task
+   * ID.
+   * 
+   * @param taskId the ID of the task to edit
+   * @param task the task to edit, or null if not available
+   * @return the place
+   */
+  public static TaskPlace createTaskEditPlace(Long taskId, TaskProxy task) {
+    return new TaskPlace(taskId, task);
+  }
+
+  /**
+   * Get the singleton instance of the {@link TaskPlace} used to create a new
+   * task.
+   * 
+   * @return the place
+   */
+  public static TaskPlace getTaskCreatePlace() {
+    if (singleton == null) {
+      singleton = new TaskPlace(null, null);
+    }
+    return singleton;
+  }
+
+  private final TaskProxy task;
+  private final Long taskId;
+
+  /**
+   * Construct a new {@link TaskPlace} for the specified task id.
+   * 
+   * @param taskId the ID of the task to edit
+   * @param task the task to edit, or null if not available
+   */
+  private TaskPlace(Long taskId, TaskProxy task) {
+    this.taskId = taskId;
+    this.task = task;
+  }
+
+  /**
+   * Get the task to edit.
+   * 
+   * @return the task to edit, or null if not available
+   */
+  public TaskProxy getTask() {
+    return task;
+  }
+
+  /**
+   * 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/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskReadPresenter.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskReadPresenter.java
new file mode 100644
index 0000000..7c6ab03
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskReadPresenter.java
@@ -0,0 +1,133 @@
+/*
+ * 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.presenter.task;
+
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.event.ActionEvent;
+import com.google.gwt.sample.mobilewebapp.client.event.ActionNames;
+import com.google.gwt.sample.mobilewebapp.client.event.TaskEditEvent;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.EventBus;
+import com.google.web.bindery.requestfactory.shared.Receiver;
+
+/**
+ * Makes a TaskReadView display a task.
+ */
+public class TaskReadPresenter implements TaskReadView.Presenter {
+
+  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 displayed, might not be possible to edit it.
+   */
+  private TaskProxy task;
+
+  /**
+   * The ID of the current task being edited.
+   */
+  private final Long taskId;
+  private EventBus eventBus;
+
+  /**
+   * Construct a new {@link TaskReadPresenter}.
+   * 
+   * @param clientFactory the {@link ClientFactory} of shared resources
+   * @param place configuration for this activity
+   */
+  public TaskReadPresenter(ClientFactory clientFactory, TaskPlace place) {
+    this.taskId = place.getTaskId();
+    this.task = place.getTask();
+    this.clientFactory = clientFactory;
+    clientFactory.getTaskReadView().setPresenter(this);
+  }
+
+  @Override
+  public Widget asWidget() {
+    return getView().asWidget();
+  }
+
+  @Override
+  public void editTask() {
+    eventBus.fireEvent(new TaskEditEvent(task));
+  }
+
+  @Override
+  public String mayStop() {
+    return null;
+  }
+
+  public void start(EventBus newEventBus) {
+    this.eventBus = newEventBus;
+
+    // Hide the 'add' button in the shell.
+    // TODO(rjrjr) Ick!
+    clientFactory.getShell().setAddButtonVisible(false);
+
+    // Try to load the task from local storage.
+    if (task == null) {
+      task = clientFactory.getTaskProxyLocalStorage().getTask(taskId);
+    }
+
+    if (task == null) {
+      // 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;
+              }
+
+              // Task not found.
+              if (response == null) {
+                Window.alert("The task with id '" + taskId + "' could not be found."
+                    + " Please select a different task from the task list.");
+                ActionEvent.fire(eventBus, ActionNames.EDITING_CANCELED);
+                return;
+              }
+
+              // Show the task.
+              task = response;
+              getView().getEditorDriver().edit(response);
+            }
+          });
+    } else {
+      // Use the task that was passed with the place.
+      getView().getEditorDriver().edit(task);
+    }
+  }
+
+  @Override
+  public void stop() {
+    eventBus = null;
+    // Ignore all incoming responses to the requests from this activity.
+    isDead = true;
+  }
+
+  private TaskReadView getView() {
+    return clientFactory.getTaskReadView();
+  }
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskReadView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskReadView.java
similarity index 83%
rename from samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskReadView.java
rename to samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskReadView.java
index d6c6a66..3246cf3 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskReadView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/task/TaskReadView.java
@@ -13,22 +13,23 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.sample.mobilewebapp.client.activity;
+package com.google.gwt.sample.mobilewebapp.presenter.task;
 
 import com.google.gwt.editor.client.Editor;
 import com.google.gwt.editor.client.SimpleBeanEditorDriver;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.sample.ui.client.PresentsWidgets;
 import com.google.gwt.user.client.ui.IsWidget;
 
 /**
  * A readonly view of a task.
  */
-public interface TaskReadView extends IsWidget, Editor<TaskProxy> {
+public interface TaskReadView extends Editor<TaskProxy>, IsWidget {
 
   /**
    * The presenter for this view.
    */
-  public static interface Presenter {
+  public interface Presenter extends PresentsWidgets {
     /**
      * Switch to an edit view of this task.
      */
@@ -39,10 +40,9 @@
    * Get the driver used to edit tasks in the view.
    */
   SimpleBeanEditorDriver<TaskProxy, ?> getEditorDriver();
-
+  
   /**
    * Set the {@link Presenter} for this view.
-   * 
    * @param presenter the presenter
    */
   void setPresenter(Presenter presenter);
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/taskchart/TaskChartPresenter.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/taskchart/TaskChartPresenter.java
new file mode 100644
index 0000000..6692092
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/taskchart/TaskChartPresenter.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.presenter.taskchart;
+
+import com.google.gwt.canvas.dom.client.CssColor;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.sample.mobilewebapp.client.event.TaskListUpdateEvent;
+import com.google.gwt.sample.mobilewebapp.client.ui.PieChart;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.sample.ui.client.PresentsWidgets;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.EventBus;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Drives a {@link PieChart} to summarize a set of tasks.
+ */
+public class TaskChartPresenter implements PresentsWidgets {
+
+  /**
+   * A pie chart showing a snapshot of the tasks.
+   */
+  private final PieChart pieChart;
+
+  /**
+   * Construct a new {@link TaskChartPresenter}.
+   * 
+   * @param pieChart the {@link PieChart}
+   */
+  public TaskChartPresenter(PieChart pieChart) {
+    this.pieChart = pieChart;
+  }
+
+  public Widget asWidget() {
+    return pieChart;
+  }
+
+  @Override
+  public String mayStop() {
+    return null;
+  }
+
+  public void start(EventBus eventBus) {
+    pieChart.setWidth("90%");
+    pieChart.setHeight("90%");
+    pieChart.getElement().getStyle().setMarginLeft(5.0, Unit.PCT);
+    pieChart.getElement().getStyle().setMarginTop(5.0, Unit.PCT);
+
+    // Listen for events from the task list activity.
+    eventBus.addHandler(TaskListUpdateEvent.TYPE, new TaskListUpdateEvent.Handler() {
+      public void onTaskListUpdated(TaskListUpdateEvent event) {
+        updatePieChart(event.getTasks());
+      }
+    });
+  }
+
+  @Override
+  public void stop() {
+  }
+
+  /**
+   * 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();
+    if (pastDue > 0) {
+      pieChart.addSlice(pastDue, CssColor.make(255, 100, 100));
+    }
+    if (dueSoon > 0) {
+      pieChart.addSlice(dueSoon, CssColor.make(255, 200, 100));
+    }
+    if (onTime > 0) {
+      pieChart.addSlice(onTime, CssColor.make(100, 255, 100));
+    }
+    if (noDate > 0) {
+      pieChart.addSlice(noDate, CssColor.make(200, 200, 200));
+    }
+    pieChart.redraw();
+  }
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/tasklist/TaskListPlace.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/tasklist/TaskListPlace.java
new file mode 100644
index 0000000..6f1616e
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/tasklist/TaskListPlace.java
@@ -0,0 +1,62 @@
+/*
+ * 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.presenter.tasklist;
+
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.place.shared.PlaceTokenizer;
+import com.google.gwt.place.shared.Prefix;
+
+/**
+ * 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.
+   */
+  @Prefix("tl")
+  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/main/java/com/google/gwt/sample/mobilewebapp/presenter/tasklist/TaskListPresenter.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/tasklist/TaskListPresenter.java
new file mode 100644
index 0000000..f47e779
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/tasklist/TaskListPresenter.java
@@ -0,0 +1,167 @@
+/*
+ * 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.presenter.tasklist;
+
+import com.google.gwt.sample.mobilewebapp.client.ClientFactory;
+import com.google.gwt.sample.mobilewebapp.client.event.ShowTaskEvent;
+import com.google.gwt.sample.mobilewebapp.client.event.TaskListUpdateEvent;
+import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.EventBus;
+import com.google.web.bindery.requestfactory.shared.Receiver;
+import com.google.web.bindery.requestfactory.shared.ServerFailure;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Activity that presents a list of tasks.
+ */
+public class TaskListPresenter implements TaskListView.Presenter {
+
+  /**
+   * The delay in milliseconds between calls to refresh the task list.
+   */
+  private static final int REFRESH_DELAY = 5000;
+
+  /**
+   * A boolean indicating that we should clear the task list when started.
+   */
+  private final boolean clearTaskList;
+
+  private final ClientFactory clientFactory;
+
+  private EventBus eventBus;
+  
+  /**
+   * The refresh timer used to periodically refresh the task list.
+   */
+  private Timer refreshTimer;
+
+  public TaskListPresenter(ClientFactory clientFactory, boolean clearTaskList) {
+    this.clientFactory = clientFactory;
+    this.clearTaskList = clearTaskList;
+    clientFactory.getTaskListView().setPresenter(this);
+  }
+
+  /**
+   * Construct a new {@link TaskListPresenter}.
+   * 
+   * @param clientFactory the {@link ClientFactory} of shared resources
+   * @param place configuration for this activity
+   */
+  public TaskListPresenter(ClientFactory clientFactory, TaskListPlace place) {
+    this(clientFactory, place.isTaskListStale());
+  }
+
+  @Override
+  public Widget asWidget() {
+    return getView().asWidget();
+  }
+
+  @Override
+  public String mayStop() {
+    return null; // always happy to stop
+  }
+
+  public void selectTask(TaskProxy selected) {
+    // Go into edit mode when a task is selected.
+    eventBus.fireEvent(new ShowTaskEvent(selected));
+  }
+
+  @Override
+  public void start(EventBus eventBus) {
+    this.eventBus = eventBus;
+    // Add a handler to the 'add' button in the shell.
+    clientFactory.getShell().setAddButtonVisible(true);
+
+    // Clear the task list and display it.
+    if (clearTaskList) {
+      getView().clearList();
+    }
+
+    // Create a timer to periodically refresh the task list.
+    refreshTimer = new Timer() {
+      @Override
+      public void run() {
+        refreshTaskList();
+      }
+    };
+
+    // Load the saved task list from storage
+    List<TaskProxy> list = clientFactory.getTaskProxyLocalStorage().getTasks();
+    setTasks(list);
+
+    // Request the task list now.
+    refreshTaskList();
+  }
+
+  @Override
+  public void stop() {
+    eventBus = null;
+
+    // Kill the refresh timer.
+    if (refreshTimer != null) {
+      refreshTimer.cancel();
+    }
+  }
+
+  private TaskListView getView() {
+    return clientFactory.getTaskListView();
+  }
+
+  /**
+   * 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 (eventBus == null) {
+              return;
+            }
+
+            // Display the tasks in the view.
+            if (response == null) {
+              response = Collections.<TaskProxy> emptyList();
+            }
+            setTasks(response);
+
+            // save the response to storage
+            clientFactory.getTaskProxyLocalStorage().setTasks(response);
+
+            // Restart the timer.
+            refreshTimer.schedule(REFRESH_DELAY);
+          }
+        });
+  }
+
+  /**
+   * Set the list of tasks.
+   */
+  private void setTasks(List<TaskProxy> tasks) {
+    getView().setTasks(tasks);
+    eventBus.fireEventFromSource(new TaskListUpdateEvent(tasks), this);
+  }
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskListView.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/tasklist/TaskListView.java
similarity index 72%
rename from samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskListView.java
rename to samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/tasklist/TaskListView.java
index 90faccb..6a16da5 100644
--- a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/client/activity/TaskListView.java
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/mobilewebapp/presenter/tasklist/TaskListView.java
@@ -1,40 +1,35 @@
 /*
  * 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;
+package com.google.gwt.sample.mobilewebapp.presenter.tasklist;
 
-import com.google.gwt.user.client.ui.IsWidget;
 import com.google.gwt.sample.mobilewebapp.shared.TaskProxy;
+import com.google.gwt.sample.ui.client.PresentsWidgets;
+import com.google.gwt.user.client.ui.IsWidget;
 
 import java.util.List;
 
 /**
- * A view of a {@link TaskListActivity}.
+ * Implemented by views that display a list of tasks.
  */
 public interface TaskListView extends IsWidget {
 
   /**
    * The presenter for this view.
    */
-  public static interface Presenter {
-
-    /**
-     * View was told to stop. Don't really expect to submit this.
-     */
-    void onStop();
-
+  public interface Presenter extends PresentsWidgets {
     /**
      * Select a task.
      * 
@@ -49,9 +44,8 @@
   void clearList();
 
   /**
-   * Set the {@link Presenter} for this view.
-   * 
-   * @param presenter the presenter
+   * Sets the new presenter, and calls {@link Presenter#stop()} on the previous
+   * one.
    */
   void setPresenter(Presenter presenter);
 
@@ -61,14 +55,4 @@
    * @param tasks the list of tasks
    */
   void setTasks(List<TaskProxy> tasks);
-  
-  /**
-   * Start a new session of this view.
-   */
-  void start();
-
-  /**
-   * Stop it! 
-   */
-  void stop();
 }
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/UI.gwt.xml b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/UI.gwt.xml
new file mode 100644
index 0000000..b2ede8b
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/UI.gwt.xml
@@ -0,0 +1,21 @@
+<!--
+  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.
+-->
+
+<!-- Candidates for addition to com.google.gwt.user.client.ui --> 
+<module>
+	<inherits name="com.google.gwt.user.User" />
+	<source path="client"/>
+</module>
\ No newline at end of file
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/client/OrientationHelper.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/client/OrientationHelper.java
new file mode 100644
index 0000000..8fd05dc
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/client/OrientationHelper.java
@@ -0,0 +1,53 @@
+/*
+ * 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.ui.client;
+
+import com.google.gwt.event.logical.shared.HasAttachHandlers;
+import com.google.gwt.user.client.Command;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+
+/**
+ * Accepts {@link Command}s to be run when the browser window or device's
+ * orientation changes. Commands operate only while the associated widget is
+ * attached.
+ * TODO(rjrjr) just use the event bus
+ */
+public interface OrientationHelper {
+  /**
+   * Gives efficient on demand access to the orientation of the window or
+   * device.
+   * 
+   * @return true for portrait, false for landscape
+   */
+  boolean isPortrait();
+
+  /**
+   * Set commands to run on orientation change, one for portrait and one for
+   * landscape. The appropriate command is fired immediately if this method is
+   * called while the widget is attached.
+   * <p>
+   * Commands are also fired each time the widget is attached. If that is not
+   * desired a widget should call {@link HandlerRegistration#removeHandler()} on
+   * the returned object when it detaches.
+   * 
+   * @param widget the widget to help
+   * @param forPortrait command to run when on shift to portrait
+   * @param forLandscape command to run when on shift to landscape
+   * @return registration object to be used to stop and dereference the commands
+   */
+  HandlerRegistration setCommands(HasAttachHandlers widget, Command forPortrait,
+      Command forLandscape);
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/client/PresentsWidgets.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/client/PresentsWidgets.java
new file mode 100644
index 0000000..cc47f8c
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/client/PresentsWidgets.java
@@ -0,0 +1,46 @@
+/*
+ * 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.ui.client;
+
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.web.bindery.event.shared.EventBus;
+
+/**
+ * Implemented by specialized widget drivers that can answer
+ * {@link com.google.gwt.activity.shared.Activity#mayStop() mayStop()} calls for
+ * {@link com.google.gwt.activity.shared.Activity Activities}.
+ * <p>
+ * Note that this interface extends {@link IsWidget}. This is to make it easier
+ * to evolve app code in MVP directions piecemeal, just where it is useful. If
+ * code that assembles widgets thinks of them strictly as IsWidget instances, it
+ * doesn't need to notice as they get refactored in to Presenter / View pairs.
+ * Or as they don't.
+ */
+public interface PresentsWidgets extends IsWidget {
+  /**
+   * Called (probably from
+   * {@link com.google.gwt.activity.shared.Activity#mayStop Activity.mayStop})
+   * to see if it's safe to stop this presenter.
+   * 
+   * @return null if it's okay to stop, else a message to ask the user if she's
+   *         sure
+   */
+  String mayStop();
+  
+  void start(EventBus eventBus);
+
+  void stop();
+}
diff --git a/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/client/WindowBasedOrientationHelper.java b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/client/WindowBasedOrientationHelper.java
new file mode 100644
index 0000000..d388384
--- /dev/null
+++ b/samples/mobilewebapp/src/main/java/com/google/gwt/sample/ui/client/WindowBasedOrientationHelper.java
@@ -0,0 +1,130 @@
+/*
+ * 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.ui.client;
+
+import com.google.gwt.event.logical.shared.AttachEvent;
+import com.google.gwt.event.logical.shared.HasAttachHandlers;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Window;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+
+import java.util.HashSet;
+
+/**
+ * OrientationHelper implementation that works by by monitoring window size
+ * changes, and so works on both mobile devices and desktop browsers.
+ * <p>
+ * Expected to be used as an app-wide singleton.
+ */
+public class WindowBasedOrientationHelper implements OrientationHelper {
+  private class CommandSet implements AttachEvent.Handler, HandlerRegistration {
+    final Command portraitCommand;
+    final Command landscapeCommand;
+    final HandlerRegistration attachEventReg;
+
+    boolean active;
+
+    CommandSet(HasAttachHandlers widget, Command portrait, Command landscape) {
+      this.portraitCommand = portrait;
+      this.landscapeCommand = landscape;
+
+      attachEventReg = widget.addAttachHandler(this);
+      active = widget.isAttached();
+    }
+
+    @Override
+    public void onAttachOrDetach(AttachEvent event) {
+      active = event.isAttached();
+      fire();
+    }
+
+    @Override
+    public void removeHandler() {
+      commandSets.remove(this);
+      attachEventReg.removeHandler();
+    }
+
+    void fire() {
+      if (active) {
+        if (isPortrait) {
+          portraitCommand.execute();
+        } else {
+          landscapeCommand.execute();
+        }
+      }
+    }
+  }
+
+  private static boolean calculateIsPortrait() {
+    return Window.getClientHeight() > Window.getClientWidth();
+  }
+
+  private boolean isPortrait;
+  private HandlerRegistration windowResizeReg;
+  private HashSet<CommandSet> commandSets = new HashSet<CommandSet>();
+
+  public WindowBasedOrientationHelper() {
+    isPortrait = calculateIsPortrait();
+    windowResizeReg = Window.addResizeHandler(new ResizeHandler() {
+      public void onResize(ResizeEvent event) {
+        update();
+      }
+    });
+  }
+
+  @Override
+  public boolean isPortrait() {
+    assertLive();
+    return isPortrait;
+  }
+
+  @Override
+  public HandlerRegistration setCommands(HasAttachHandlers widget, Command forPortrait,
+      Command forLandscape) {
+    assertLive();
+    CommandSet commandSet = new CommandSet(widget, forPortrait, forLandscape);
+    commandSets.add(commandSet);
+    commandSet.fire();
+    return commandSet;
+  }
+
+  public void stop() {
+    assertLive();
+    windowResizeReg.removeHandler();
+    windowResizeReg = null;
+
+    for (CommandSet commandSet : commandSets) {
+      commandSet.attachEventReg.removeHandler();
+    }
+    commandSets = null;
+  }
+
+  private void assertLive() {
+    assert windowResizeReg != null : "Cannot do this after stop() is called";
+  }
+
+  private void update() {
+    boolean was = isPortrait;
+    isPortrait = calculateIsPortrait();
+    if (was != isPortrait) {
+      for (CommandSet helper : commandSets) {
+        helper.fire();
+      }
+    }
+  }
+}
diff --git a/samples/mobilewebapp/src/test/java/com/google/gwt/sample/core/linker/SimpleAppCacheLinkerTest.java b/samples/mobilewebapp/src/test/java/com/google/gwt/sample/core/linker/SimpleAppCacheLinkerTest.java
index a64ee40..0c029ed 100644
--- a/samples/mobilewebapp/src/test/java/com/google/gwt/sample/core/linker/SimpleAppCacheLinkerTest.java
+++ b/samples/mobilewebapp/src/test/java/com/google/gwt/sample/core/linker/SimpleAppCacheLinkerTest.java
@@ -1,5 +1,18 @@
-// Copyright 2011 Google Inc. All Rights Reserved.
-
+/*
+ * 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.core.linker;
 
 import com.google.gwt.core.ext.LinkerContext;
@@ -10,7 +23,6 @@
 import com.google.gwt.core.ext.linker.SelectionProperty;
 import com.google.gwt.core.ext.linker.SyntheticArtifact;
 import com.google.gwt.core.ext.linker.impl.SelectionInformation;
-import com.google.gwt.sample.core.linker.SimpleAppCacheLinker;
 
 import junit.framework.TestCase;
 
@@ -21,131 +33,20 @@
 import java.util.TreeSet;
 
 /**
- * Tests {@link SimpleAppCacheLinker}
+ * Tests {@link SimpleAppCacheLinker}.
  */
 public class SimpleAppCacheLinkerTest extends TestCase {
-  private ArtifactSet artifacts;
-  private TreeLogger logger;
-  
-  @Override
-  public void setUp() {
-    artifacts = new ArtifactSet();
-    artifacts.add(new SelectionInformation("foo", 0, new TreeMap<String, String>()));
-    logger = TreeLogger.NULL;
-  }
-
-  public void testAddCachableArtifacts() throws UnableToCompleteException {
-    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
-
-    // Some cacheable artifact
-    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.bar", new byte[0]));
-
-    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, false);
-
-    assertEquals(3, result.size());
-    assertHasOneManifest(result);
-    assertTrue(getManifestContents(result).contains("foo.bar"));
-  }
-
-  public void testNoNonCachableArtifacts() throws UnableToCompleteException {
-    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
-
-    // Some non-cacheable artifacts
-    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.symbolMap", new byte[0]));
-    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.xml.gz", new byte[0]));
-    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.rpc.log", new byte[0]));
-    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.gwt.rpc", new byte[0]));
-    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "rpcPolicyManifest.bar", new byte[0]));
-
-    ArtifactSet result = linker.link(TreeLogger.NULL, new MockLinkerContext(), artifacts, false);
-
-    assertEquals(7, result.size());
-    assertHasOneManifest(result);
-    assertFalse(getManifestContents(result).contains("symbolMap"));
-    assertFalse(getManifestContents(result).contains("xml.gz"));
-    assertFalse(getManifestContents(result).contains("rpc.log"));
-    assertFalse(getManifestContents(result).contains("gwt.rpc"));
-    assertFalse(getManifestContents(result).contains("rpcPolicyManifest"));
-  }
-
-  public void testAddStaticFiles() throws UnableToCompleteException {
-    SimpleAppCacheLinker linker = new OneStaticFileAppCacheLinker();
-
-    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, false);
-
-    assertEquals(2, result.size());
-    assertHasOneManifest(result);
-    assertTrue(getManifestContents(result).contains("aStaticFile"));
-  }
-
-  public void testEmptyManifestDevMode() throws UnableToCompleteException {
-    // No SelectionInformation artifact
-    artifacts = new ArtifactSet();
-    
-    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
-
-    // Some cacheable artifact
-    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.bar", new byte[0]));
-
-    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, false);
-
-    assertHasOneManifest(result);
-    assertFalse(getManifestContents(result).contains("foo.bar"));
-  }
-
-  public void testManifestOnlyOnLastPass() throws UnableToCompleteException {
-    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
-
-    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, true);
-
-    assertEquals(artifacts, result);
-    
-    result = linker.link(logger, new MockLinkerContext(), artifacts, false);
-    
-    assertEquals(2, result.size());
-    assertHasOneManifest(result);
-  }
-  
-  private void assertHasOneManifest(ArtifactSet artifacts) {
-    int manifestCount = 0;
-    for (SyntheticArtifact artifact : artifacts.find(SyntheticArtifact.class)) {
-      if ("appcache.nocache.manifest".equals(artifact.getPartialPath())) {
-        assertEquals("appcache.nocache.manifest", artifact.getPartialPath());
-        manifestCount++;
-      }
-    }
-    assertEquals(1, manifestCount);
-  }
-  
-  private SyntheticArtifact getManifest(ArtifactSet artifacts) {
-    for (SyntheticArtifact artifact : artifacts.find(SyntheticArtifact.class)) {
-      if ("appcache.nocache.manifest".equals(artifact.getPartialPath())) {
-        assertEquals("appcache.nocache.manifest", artifact.getPartialPath());
-        return artifact;
-      }
-    }
-    fail("Manifest not found");
-    return null;
-  }
-  
-  private String getManifestContents(ArtifactSet artifacts) throws UnableToCompleteException {
-    return getArtifactContents(getManifest(artifacts));
-  }
-
-  private String getArtifactContents(SyntheticArtifact artifact) throws UnableToCompleteException {
-    InputStream is = artifact.getContents(logger);
-    String contents = new Scanner(is).useDelimiter("\\A").next();
-    return contents;
-  }
-
+  /**
+   * A {@code SimpleAppCacheLinker} mocking the addition of a static entry.
+   */
   public static class OneStaticFileAppCacheLinker extends SimpleAppCacheLinker {
-    
+
     @Override
     protected String[] otherCachedFiles() {
       return new String[] {"aStaticFile"};
     }
   }
-  
+
   private static class MockLinkerContext implements LinkerContext {
 
     public SortedSet<ConfigurationProperty> getConfigurationProperties() {
@@ -176,4 +77,119 @@
       return jsProgram;
     }
   }
+
+  private ArtifactSet artifacts;
+
+  private TreeLogger logger;
+
+  @Override
+  public void setUp() {
+    artifacts = new ArtifactSet();
+    artifacts.add(new SelectionInformation("foo", 0, new TreeMap<String, String>()));
+    logger = TreeLogger.NULL;
+  }
+
+  public void testAddCachableArtifacts() throws UnableToCompleteException {
+    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
+
+    // Some cacheable artifact
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.bar", new byte[0]));
+
+    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, false);
+
+    assertEquals(3, result.size());
+    assertHasOneManifest(result);
+    assertTrue(getManifestContents(result).contains("foo.bar"));
+  }
+
+  public void testAddStaticFiles() throws UnableToCompleteException {
+    SimpleAppCacheLinker linker = new OneStaticFileAppCacheLinker();
+
+    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, false);
+
+    assertEquals(2, result.size());
+    assertHasOneManifest(result);
+    assertTrue(getManifestContents(result).contains("aStaticFile"));
+  }
+  
+  public void testEmptyManifestDevMode() throws UnableToCompleteException {
+    // No SelectionInformation artifact
+    artifacts = new ArtifactSet();
+    
+    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
+
+    // Some cacheable artifact
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.bar", new byte[0]));
+
+    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, false);
+
+    assertHasOneManifest(result);
+    assertFalse(getManifestContents(result).contains("foo.bar"));
+  }
+
+  public void testManifestOnlyOnLastPass() throws UnableToCompleteException {
+    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
+
+    ArtifactSet result = linker.link(logger, new MockLinkerContext(), artifacts, true);
+
+    assertEquals(artifacts, result);
+    
+    result = linker.link(logger, new MockLinkerContext(), artifacts, false);
+    
+    assertEquals(2, result.size());
+    assertHasOneManifest(result);
+  }
+  
+  public void testNoNonCachableArtifacts() throws UnableToCompleteException {
+    SimpleAppCacheLinker linker = new SimpleAppCacheLinker();
+
+    // Some non-cacheable artifacts
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.symbolMap", new byte[0]));
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.xml.gz", new byte[0]));
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.rpc.log", new byte[0]));
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "foo.gwt.rpc", new byte[0]));
+    artifacts.add(new SyntheticArtifact(SimpleAppCacheLinker.class, "rpcPolicyManifest.bar", new byte[0]));
+
+    ArtifactSet result = linker.link(TreeLogger.NULL, new MockLinkerContext(), artifacts, false);
+
+    assertEquals(7, result.size());
+    assertHasOneManifest(result);
+    assertFalse(getManifestContents(result).contains("symbolMap"));
+    assertFalse(getManifestContents(result).contains("xml.gz"));
+    assertFalse(getManifestContents(result).contains("rpc.log"));
+    assertFalse(getManifestContents(result).contains("gwt.rpc"));
+    assertFalse(getManifestContents(result).contains("rpcPolicyManifest"));
+  }
+  
+  private void assertHasOneManifest(ArtifactSet artifacts) {
+    int manifestCount = 0;
+    for (SyntheticArtifact artifact : artifacts.find(SyntheticArtifact.class)) {
+      if ("appcache.nocache.manifest".equals(artifact.getPartialPath())) {
+        assertEquals("appcache.nocache.manifest", artifact.getPartialPath());
+        manifestCount++;
+      }
+    }
+    assertEquals(1, manifestCount);
+  }
+
+  private String getArtifactContents(SyntheticArtifact artifact) throws UnableToCompleteException {
+    InputStream is = artifact.getContents(logger);
+    String contents = new Scanner(is).useDelimiter("\\A").next();
+    return contents;
+  }
+
+  private SyntheticArtifact getManifest(ArtifactSet artifacts) {
+    for (SyntheticArtifact artifact : artifacts.find(SyntheticArtifact.class)) {
+      if ("appcache.nocache.manifest".equals(artifact.getPartialPath())) {
+        assertEquals("appcache.nocache.manifest", artifact.getPartialPath());
+        return artifact;
+      }
+    }
+    fail("Manifest not found");
+    return null;
+  }
+  
+  private String getManifestContents(ArtifactSet artifacts) throws UnableToCompleteException {
+    return getArtifactContents(getManifest(artifacts));
+  }
 }
diff --git a/tools/scripts/maven_script.sh b/tools/scripts/maven_script.sh
index a7770f2..2439582 100755
--- a/tools/scripts/maven_script.sh
+++ b/tools/scripts/maven_script.sh
@@ -15,7 +15,7 @@
 # the License.
 
 MAVEN_REPO=${MAVEN_REPO:-"~/.m2/repository"}
-GWT_VERSION=${GWT_VERSION:-"2.3.0"}
+GWT_VERSION=${GWT_VERSION:-"2.4.0"}
 GWT_DIR=${GWT_DIR:-"build/lib"}
 
 echo "Pushing GWT jars from ${GWT_DIR} into local maven repo with version ${GWT_VERSION}."