Merge changes I36bf7b37,Id835f12d,I2861135e,If0ad7302,I91ce3d1e, ...

* changes:
  Implement change list for reviewing changes
  Make avatar size and text size of UserView configurable
  Refactor: Implement avatar/user control as view
  Refactor: Implement project/branch/topic/age control as view
  Add expandable commit message to abandon and restore screens
  Refactor: Implement expandable commit message as view
diff --git a/app/src/main/java/com/google/reviewit/AbandonFragment.java b/app/src/main/java/com/google/reviewit/AbandonFragment.java
index 34625e8..dcc2137 100644
--- a/app/src/main/java/com/google/reviewit/AbandonFragment.java
+++ b/app/src/main/java/com/google/reviewit/AbandonFragment.java
@@ -22,6 +22,7 @@
 
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.reviewit.app.Change;
+import com.google.reviewit.widget.ExpandableCommitMessageView;
 import com.urswolfer.gerrit.client.rest.http.HttpStatusException;
 
 /**
@@ -49,12 +50,14 @@
   public void onActivityCreated(Bundle savedInstanceState) {
     super.onActivityCreated(savedInstanceState);
 
-    Change change = getApp().getActionHandler().getCurrentChange();
+    Change change = getApp().getSortActionHandler().getCurrentChange();
     setTitle(getString(R.string.abandon_change_title, change.info._number));
     init(change);
   }
 
   private void init(final Change change) {
+    ((ExpandableCommitMessageView)v(R.id.commitMessage)).init(change);
+
     v(R.id.abandonButton).setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(final View v) {
diff --git a/app/src/main/java/com/google/reviewit/AddReviewerFragment.java b/app/src/main/java/com/google/reviewit/AddReviewerFragment.java
index c53071c..7e25d7f 100644
--- a/app/src/main/java/com/google/reviewit/AddReviewerFragment.java
+++ b/app/src/main/java/com/google/reviewit/AddReviewerFragment.java
@@ -77,7 +77,7 @@
   public void onActivityCreated(Bundle savedInstanceState) {
     super.onActivityCreated(savedInstanceState);
 
-    Change change = getApp().getActionHandler().getCurrentChange();
+    Change change = getApp().getSortActionHandler().getCurrentChange();
     initInputField(change);
     try {
       displayReviewers(change);
diff --git a/app/src/main/java/com/google/reviewit/BaseFragment.java b/app/src/main/java/com/google/reviewit/BaseFragment.java
index 9babd93..e64d33a 100644
--- a/app/src/main/java/com/google/reviewit/BaseFragment.java
+++ b/app/src/main/java/com/google/reviewit/BaseFragment.java
@@ -31,7 +31,7 @@
 import android.view.inputmethod.InputMethodManager;
 import android.widget.TextView;
 
-import com.google.reviewit.app.ActionHandler;
+import com.google.reviewit.app.SortActionHandler;
 import com.google.reviewit.app.ReviewItApp;
 import com.google.reviewit.util.TaskObserver;
 import com.google.reviewit.util.WidgetUtil;
@@ -121,8 +121,8 @@
     return ((ReviewItApp) getActivity().getApplication());
   }
 
-  protected ActionHandler getActionHandler() {
-    return getApp().getActionHandler();
+  protected SortActionHandler getSortActionHandler() {
+    return getApp().getSortActionHandler();
   }
 
   public void display(
diff --git a/app/src/main/java/com/google/reviewit/DetailedChangeFragment.java b/app/src/main/java/com/google/reviewit/DetailedChangeFragment.java
index f99d812..2991696 100644
--- a/app/src/main/java/com/google/reviewit/DetailedChangeFragment.java
+++ b/app/src/main/java/com/google/reviewit/DetailedChangeFragment.java
@@ -31,7 +31,7 @@
 
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.FileInfo;
-import com.google.reviewit.app.ActionHandler;
+import com.google.reviewit.app.SortActionHandler;
 import com.google.reviewit.app.Change;
 import com.google.reviewit.util.ChangeUtil;
 import com.google.reviewit.widget.ChangeBox;
@@ -85,7 +85,7 @@
   public void onActivityCreated(Bundle savedInstanceState) {
     super.onActivityCreated(savedInstanceState);
 
-    Change change = getApp().getActionHandler().getCurrentChange();
+    Change change = getApp().getSortActionHandler().getCurrentChange();
     setTitle(getString(R.string.detailed_change_title, change.info._number));
     setHasOptionsMenu(true);
     init();
@@ -217,7 +217,7 @@
 
   @Override
   public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
-    ActionHandler actionHandler = getApp().getActionHandler();
+    SortActionHandler actionHandler = getApp().getSortActionHandler();
     inflater.inflate(R.menu.menu_detailed_change, menu);
     for (int i = 0; i < menu.size(); i++) {
       MenuItem item = menu.getItem(i);
@@ -237,7 +237,7 @@
 
   @Override
   public boolean onOptionsItemSelected(MenuItem item) {
-    ActionHandler actionHandler = getApp().getActionHandler();
+    SortActionHandler actionHandler = getApp().getSortActionHandler();
     switch (item.getItemId()) {
       case R.id.action_add_reviewer:
         display(AddReviewerFragment.create(getClass()));
diff --git a/app/src/main/java/com/google/reviewit/PostReviewFragment.java b/app/src/main/java/com/google/reviewit/PostReviewFragment.java
index 7dc1560..bb431db 100644
--- a/app/src/main/java/com/google/reviewit/PostReviewFragment.java
+++ b/app/src/main/java/com/google/reviewit/PostReviewFragment.java
@@ -26,8 +26,8 @@
 import com.google.reviewit.app.Change;
 import com.google.reviewit.util.ObservableAsynTask;
 import com.google.reviewit.util.TaskObserver;
-import com.google.reviewit.util.WidgetUtil;
 import com.google.reviewit.widget.ApprovalsView;
+import com.google.reviewit.widget.ExpandableCommitMessageView;
 import com.google.reviewit.widget.VoteView;
 import com.urswolfer.gerrit.client.rest.http.HttpStatusException;
 
@@ -71,36 +71,17 @@
 
     int vote = getArguments().getInt(VOTE);
     update(vote);
-    Change change = getApp().getActionHandler().getCurrentChange();
+    Change change = getApp().getSortActionHandler().getCurrentChange();
 
     setTitle(getString(R.string.detailed_change_title, change.info._number));
     init(change);
-    WidgetUtil.setText(v(R.id.subject), change.info.subject);
     initLabels(change, vote);
     ((ApprovalsView) v(R.id.approvals)).displayApprovals(getApp(),
         change.info, this);
   }
 
   private void init(final Change change) {
-    v(R.id.expandCommitMessage).setOnClickListener(new View.OnClickListener() {
-      @Override
-      public void onClick(View v) {
-        setGone(v(R.id.expandCommitMessage));
-        setVisible(v(R.id.collapseCommitMessage));
-        WidgetUtil.setText(v(R.id.subject),
-            change.currentRevision().commit.message);
-      }
-    });
-
-    v(R.id.collapseCommitMessage).setOnClickListener(
-        new View.OnClickListener() {
-      @Override
-      public void onClick(View v) {
-        setGone(v(R.id.collapseCommitMessage));
-        setVisible(v(R.id.expandCommitMessage));
-        WidgetUtil.setText(v(R.id.subject), change.info.subject);
-      }
-    });
+    ((ExpandableCommitMessageView)v(R.id.commitMessage)).init(change);
 
     v(R.id.postReviewButton).setOnClickListener(new View.OnClickListener() {
       @Override
diff --git a/app/src/main/java/com/google/reviewit/SortSettingFragment.java b/app/src/main/java/com/google/reviewit/QuerySettingsFragment.java
similarity index 97%
rename from app/src/main/java/com/google/reviewit/SortSettingFragment.java
rename to app/src/main/java/com/google/reviewit/QuerySettingsFragment.java
index 5efb057..5ab3f1a 100644
--- a/app/src/main/java/com/google/reviewit/SortSettingFragment.java
+++ b/app/src/main/java/com/google/reviewit/QuerySettingsFragment.java
@@ -25,12 +25,12 @@
 import com.google.reviewit.app.ServerConfig;
 import com.google.reviewit.util.WidgetUtil;
 
-public class SortSettingFragment extends BaseFragment {
+public class QuerySettingsFragment extends BaseFragment {
   private static final String LABEL_REGEXP = "^[a-zA-Z][a-zA-Z0-9]*$";
 
   @Override
   protected @LayoutRes int getLayout() {
-    return R.layout.content_sort_settings;
+    return R.layout.content_query_settings;
   }
 
   @Override
diff --git a/app/src/main/java/com/google/reviewit/RestoreFragment.java b/app/src/main/java/com/google/reviewit/RestoreFragment.java
index b586d55..45d398a 100644
--- a/app/src/main/java/com/google/reviewit/RestoreFragment.java
+++ b/app/src/main/java/com/google/reviewit/RestoreFragment.java
@@ -22,6 +22,7 @@
 
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.reviewit.app.Change;
+import com.google.reviewit.widget.ExpandableCommitMessageView;
 import com.urswolfer.gerrit.client.rest.http.HttpStatusException;
 
 /**
@@ -49,12 +50,14 @@
   public void onActivityCreated(Bundle savedInstanceState) {
     super.onActivityCreated(savedInstanceState);
 
-    Change change = getApp().getActionHandler().getCurrentChange();
+    Change change = getApp().getSortActionHandler().getCurrentChange();
     setTitle(getString(R.string.restore_change_title, change.info._number));
     init(change);
   }
 
   private void init(final Change change) {
+    ((ExpandableCommitMessageView)v(R.id.commitMessage)).init(change);
+
     v(R.id.restoreButton).setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(final View v) {
diff --git a/app/src/main/java/com/google/reviewit/ReviewChangesFragment.java b/app/src/main/java/com/google/reviewit/ReviewChangesFragment.java
index b38f651..5c68c1e 100644
--- a/app/src/main/java/com/google/reviewit/ReviewChangesFragment.java
+++ b/app/src/main/java/com/google/reviewit/ReviewChangesFragment.java
@@ -14,12 +14,184 @@
 
 package com.google.reviewit;
 
+import android.os.Bundle;
 import android.support.annotation.LayoutRes;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.reviewit.app.Change;
+import com.google.reviewit.app.QueryHandler;
+import com.google.reviewit.util.ObservableAsynTask;
+import com.google.reviewit.util.TaskObserver;
+import com.google.reviewit.widget.ChangeEntry;
+
+import java.util.Collections;
+import java.util.List;
+
+import static com.google.reviewit.util.LayoutUtil.matchAndFixedLayout;
+import static com.google.reviewit.util.WidgetUtil.setGone;
+import static com.google.reviewit.util.WidgetUtil.setInvisible;
+import static com.google.reviewit.util.WidgetUtil.setVisible;
 
 public class ReviewChangesFragment extends BaseFragment {
+  private static final String TAG = ReviewChangesFragment.class.getName();
 
   @Override
   protected @LayoutRes int getLayout() {
     return R.layout.content_review_changes;
   }
+
+  @Override
+  public void onActivityCreated(Bundle savedInstanceState) {
+    super.onActivityCreated(savedInstanceState);
+
+    setHasOptionsMenu(true);
+
+    TaskObserver.enableProgressBar(getWindow());
+    init();
+    display();
+  }
+
+  private void init() {
+    v(R.id.reloadButton).setOnClickListener(new View.OnClickListener() {
+      public void onClick(View v) {
+        reloadQuery();
+      }
+    });
+
+    // TODO detect when scrolled to the end and automatically load next page
+  }
+
+  private void display() {
+    if (!isOnline()) {
+      setInvisible(v(R.id.progress));
+      setGone(v(R.id.initialProgress));
+      setVisible(v(R.id.statusText, R.id.reloadButton));
+      TextView statusText = tv(R.id.statusText);
+      statusText.setText(getString(R.string.no_network));
+      statusText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15);
+      return;
+    }
+
+    new ObservableAsynTask<Void, Void, ChangeListData>() {
+      private View progress;
+      private View initialProgress;
+      private View reloadButton;
+      private TextView statusText;
+      private ViewGroup changeList;
+
+      @Override
+      protected void preExecute() {
+        super.preExecute();
+        progress = v(R.id.progress);
+        initialProgress = v(R.id.initialProgress);
+        reloadButton = v(R.id.reloadButton);
+        statusText = tv(R.id.statusText);
+        changeList = vg(R.id.changeList);
+      }
+
+      @Override
+      protected ChangeListData doInBackground(Void... v) {
+        try {
+          QueryHandler queryHandler = getApp().getQueryHandler();
+          if (queryHandler.hasNext()) {
+            return new ChangeListData(queryHandler.next());
+          } else {
+            return new ChangeListData(Collections.<Change>emptyList());
+          }
+        } catch (RestApiException e) {
+          // e.g. server not reachable
+          Log.e(TAG, "Request failed", e);
+          if (e.getCause() != null) {
+            return new ChangeListData(getString(R.string.error_with_cause,
+                e.getMessage(), e.getCause().getMessage()));
+          } else {
+            return new ChangeListData(e.getMessage());
+          }
+        }
+      }
+
+      protected void postExecute(ChangeListData changeListData) {
+        super.postExecute(changeListData);
+
+        if (getActivity() == null) {
+          // user navigated away while we were waiting for the request
+          return;
+        }
+
+        getActivity().invalidateOptionsMenu();
+        setInvisible(progress);
+        setGone(initialProgress, reloadButton);
+
+        if (changeListData.error != null) {
+          statusText.setText(changeListData.error);
+          return;
+        }
+
+        if (!changeListData.changeList.isEmpty()) {
+          setGone(statusText);
+          for (Change change : changeListData.changeList) {
+            ChangeEntry changeEntry = new ChangeEntry(getContext());
+            changeEntry.init(getApp(), change);
+            changeList.addView(changeEntry);
+            addSeparator(changeList);
+          }
+        } else {
+          statusText.setText(getString(R.string.no_changes_match));
+        }
+      }
+    }.execute();
+  }
+
+  private void addSeparator(ViewGroup viewGroup) {
+    View separator = new View(getContext());
+    separator.setLayoutParams(
+        matchAndFixedLayout(widgetUtil.dpToPx(1)));
+    separator.setBackgroundColor(widgetUtil.color(R.color.separator));
+    viewGroup.addView(separator);
+  }
+
+  private void reloadQuery() {
+    getApp().getQueryHandler().reset();
+    display(getClass(), false);
+  }
+
+  @Override
+  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+    inflater.inflate(R.menu.menu_review_changes, menu);
+    super.onCreateOptionsMenu(menu, inflater);
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    switch (item.getItemId()) {
+      case R.id.action_reload_query:
+        reloadQuery();
+        return true;
+      default:
+        return super.onOptionsItemSelected(item);
+    }
+  }
+
+  private static class ChangeListData {
+    final List<Change> changeList;
+    final String error;
+
+    ChangeListData(List<Change> changeList) {
+      this.changeList = changeList;
+      this.error = null;
+    }
+
+    ChangeListData(String error) {
+      this.changeList = null;
+      this.error = error;
+    }
+  }
 }
diff --git a/app/src/main/java/com/google/reviewit/SettingsFragment.java b/app/src/main/java/com/google/reviewit/SettingsFragment.java
index c81210e..67f0c0f 100644
--- a/app/src/main/java/com/google/reviewit/SettingsFragment.java
+++ b/app/src/main/java/com/google/reviewit/SettingsFragment.java
@@ -41,10 +41,10 @@
       }
     });
 
-    v(R.id.sortConfig).setOnClickListener(new View.OnClickListener() {
+    v(R.id.queryConfig).setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
-        display(SortSettingFragment.class);
+        display(QuerySettingsFragment.class);
       }
     });
   }
diff --git a/app/src/main/java/com/google/reviewit/SortChangesFragment.java b/app/src/main/java/com/google/reviewit/SortChangesFragment.java
index df116a0..2969891 100644
--- a/app/src/main/java/com/google/reviewit/SortChangesFragment.java
+++ b/app/src/main/java/com/google/reviewit/SortChangesFragment.java
@@ -37,7 +37,7 @@
 
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.reviewit.app.ActionHandler;
+import com.google.reviewit.app.SortActionHandler;
 import com.google.reviewit.app.Change;
 import com.google.reviewit.app.ConfigManager;
 import com.google.reviewit.app.QueryConfig;
@@ -74,8 +74,8 @@
 
     setHasOptionsMenu(true);
 
-    if (getActionHandler().hasCurrentChange()) {
-      getActionHandler().pushBack();
+    if (getSortActionHandler().hasCurrentChange()) {
+      getSortActionHandler().pushBack();
     }
 
     TaskObserver.enableProgressBar(getWindow());
@@ -92,7 +92,7 @@
     ConfigManager cfgManager = getApp().getConfigManager();
     QueryConfig config = cfgManager.getQueryConfig();
     if (!config.isComplete()) {
-      display(SortSettingFragment.class);
+      display(QuerySettingsFragment.class);
       return;
     }
 
@@ -105,7 +105,7 @@
       return;
     }
 
-    if (getActionHandler().isQueryNeeded()) {
+    if (getSortActionHandler().isQueryNeeded()) {
       if (v(R.id.changeBox) == null) {
         setVisible(v(R.id.loadingBox));
         setInvisible(v(R.id.progress));
@@ -115,7 +115,7 @@
         setVisible(v(R.id.progress));
       }
     } else {
-      Change change = getActionHandler().preview();
+      Change change = getSortActionHandler().preview();
       if (change != null) {
         ChangeUtil.colorBackground(root, change);
       }
@@ -144,7 +144,7 @@
       @Override
       protected ChangeData doInBackground(Void... v) {
         try {
-          ActionHandler actionHandler = getActionHandler();
+          SortActionHandler actionHandler = getSortActionHandler();
           if (actionHandler.hasNext()) {
             Change change = actionHandler.next();
             int queueSize = actionHandler.getQueueSize();
@@ -235,16 +235,16 @@
     v(R.id.skipButton).setOnClickListener(new View.OnClickListener() {
       public void onClick(View v) {
         disableButtons();
-        getActionHandler().skip();
-        animate(changeBox, ActionHandler.Action.SKIP);
+        getSortActionHandler().skip();
+        animate(changeBox, SortActionHandler.Action.SKIP);
       }
     });
 
     View.OnClickListener onStarClickListener = new View.OnClickListener() {
       public void onClick(View v) {
         disableButtons();
-        getActionHandler().star();
-        animate(changeBox, ActionHandler.Action.STAR);
+        getSortActionHandler().star();
+        animate(changeBox, SortActionHandler.Action.STAR);
       }
     };
     v(R.id.starButton).setOnClickListener(onStarClickListener);
@@ -253,8 +253,8 @@
     View.OnClickListener onIgnoreClickListener = new View.OnClickListener() {
       public void onClick(View v) {
         disableButtons();
-        getActionHandler().ignore();
-        animate(changeBox, ActionHandler.Action.IGNORE);
+        getSortActionHandler().ignore();
+        animate(changeBox, SortActionHandler.Action.IGNORE);
       }
     };
     v(R.id.ignoreButton).setOnClickListener(onIgnoreClickListener);
@@ -304,7 +304,7 @@
     final ViewGroup resultBox = vg(R.id.resultBox);
     changeBox.findViewById(R.id.changeBoxUpperPart).setOnTouchListener(
         new View.OnTouchListener() {
-      private ActionHandler.Action action = ActionHandler.Action.NONE;
+      private SortActionHandler.Action action = SortActionHandler.Action.NONE;
       private int x;
       private int y;
 
@@ -330,9 +330,9 @@
               if (eventX > (screenCenter + (screenCenter / 2))) {
                 ((GradientDrawable) changeBox.getBackground())
                     .setColor(widgetUtil.color(R.color.commitMessageStar));
-                action = ActionHandler.Action.STAR;
+                action = SortActionHandler.Action.STAR;
               } else {
-                action = ActionHandler.Action.NONE;
+                action = SortActionHandler.Action.NONE;
                 ((GradientDrawable) changeBox.getBackground())
                     .setColor(widgetUtil.color(R.color.commitMessage));
               }
@@ -342,9 +342,9 @@
               if (eventX < (screenCenter / 2)) {
                 ((GradientDrawable) changeBox.getBackground())
                     .setColor(widgetUtil.color(R.color.commitMessageIgnore));
-                action = ActionHandler.Action.IGNORE;
+                action = SortActionHandler.Action.IGNORE;
               } else {
-                action = ActionHandler.Action.NONE;
+                action = SortActionHandler.Action.NONE;
                 ((GradientDrawable) changeBox.getBackground())
                     .setColor(widgetUtil.color(R.color.commitMessage));
               }
@@ -355,17 +355,17 @@
                 .setColor(widgetUtil.color(R.color.commitMessage));
             switch (action) {
               case STAR:
-                getActionHandler().star();
+                getSortActionHandler().star();
                 resultBox.removeView(changeBox);
                 display();
                 break;
               case IGNORE:
-                getActionHandler().ignore();
+                getSortActionHandler().ignore();
                 resultBox.removeView(changeBox);
                 display();
                 break;
               case SKIP:
-                getActionHandler().skip();
+                getSortActionHandler().skip();
                 resultBox.removeView(changeBox);
                 display();
                 break;
@@ -388,7 +388,7 @@
   }
 
   private void animate(
-      final View changeBox, final ActionHandler.Action action) {
+      final View changeBox, final SortActionHandler.Action action) {
     final ViewGroup resultBox = vg(R.id.resultBox);
     Point screenSize = getScreenSize();
     final int screenCenter = screenSize.x / 2;
@@ -517,7 +517,7 @@
 
   @Override
   public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
-    ActionHandler actionHandler = getActionHandler();
+    SortActionHandler actionHandler = getSortActionHandler();
     inflater.inflate(R.menu.menu_sort_changes, menu);
     for (int i = 0; i < menu.size(); i++) {
       MenuItem item = menu.getItem(i);
@@ -574,7 +574,7 @@
   }
 
   private void undo() {
-    ActionHandler actionHandler = getActionHandler();
+    SortActionHandler actionHandler = getSortActionHandler();
     if (!actionHandler.undoPossible()) {
       return;
     }
@@ -615,17 +615,17 @@
           display(SortChangesFragment.class);
         }
       }
-    }.execute(getActionHandler().getCurrentChange());
+    }.execute(getSortActionHandler().getCurrentChange());
   }
 
   private void reloadQuery() {
-    getActionHandler().reset();
+    getSortActionHandler().reset();
     display(getClass(), false);
   }
 
   @Override
   public boolean onBackPressed() {
-    if (getActionHandler().undoPossible()) {
+    if (getSortActionHandler().undoPossible()) {
       undo();
       return true;
     }
diff --git a/app/src/main/java/com/google/reviewit/UnifiedDiffFragment.java b/app/src/main/java/com/google/reviewit/UnifiedDiffFragment.java
index 4db29a2..4a31052 100644
--- a/app/src/main/java/com/google/reviewit/UnifiedDiffFragment.java
+++ b/app/src/main/java/com/google/reviewit/UnifiedDiffFragment.java
@@ -84,7 +84,7 @@
         ? getArguments().getString(PATH)
         : null;
 
-    Change change = getApp().getActionHandler().getCurrentChange();
+    Change change = getApp().getSortActionHandler().getCurrentChange();
     checkState(change != null, "Change not set");
     Map<String, FileInfo> files = change.currentRevision().files;
     root = (ScrollWithHeadingsView) v(R.id.unifiedDiffRoot);
diff --git a/app/src/main/java/com/google/reviewit/app/QueryConfig.java b/app/src/main/java/com/google/reviewit/app/QueryConfig.java
index ea87e08..4759c9d 100644
--- a/app/src/main/java/com/google/reviewit/app/QueryConfig.java
+++ b/app/src/main/java/com/google/reviewit/app/QueryConfig.java
@@ -16,6 +16,9 @@
 
 import com.google.common.base.Strings;
 
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
 public class QueryConfig {
   public final String serverId;
   public final String query;
@@ -27,6 +30,15 @@
     this.label = label;
   }
 
+  public String encodedQuery() {
+    try {
+      // TODO use StandardCharsets.UTF_8.name() with API level 19
+      return URLEncoder.encode(query, "UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
   public boolean isComplete() {
     return !Strings.isNullOrEmpty(serverId)
         && !Strings.isNullOrEmpty(query)
diff --git a/app/src/main/java/com/google/reviewit/app/QueryHandler.java b/app/src/main/java/com/google/reviewit/app/QueryHandler.java
new file mode 100644
index 0000000..bdaa6f3
--- /dev/null
+++ b/app/src/main/java/com/google/reviewit/app/QueryHandler.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.reviewit.app;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class QueryHandler {
+  /**
+   * Number of changes that should be fetched at once from the server.
+   */
+  private static final int LIMIT_QUERY = 25;
+
+  private final Gerrit gerrit;
+
+  private final LinkedList<Change> result = new LinkedList<>();
+
+  private QueryConfig config;
+
+  /**
+   * Number of changes which should be skipped when querying the next changes
+   * from the server.
+   */
+  private int start = 0;
+
+  private int page = 0;
+
+  /**
+   * Whether there are more changes at the server.
+   */
+  private boolean more = true;
+
+  QueryHandler(ConfigManager cfgManager, Gerrit gerrit) {
+    this.gerrit = gerrit;
+    this.config = cfgManager.getQueryConfig();
+
+    cfgManager.addUpdateListener(new ConfigManager.OnUpdate() {
+      @Override
+      public void onUpdate(QueryConfig cfg) {
+        config = cfg;
+        reset();
+      }
+    });
+  }
+
+  public List<Change> next() throws RestApiException {
+    query();
+    return result.subList(page * LIMIT_QUERY, (page + 1) * LIMIT_QUERY);
+  }
+
+  public boolean hasNext() {
+    return more;
+  }
+
+  private void query() throws RestApiException {
+    result.addAll(Collections2.transform(Collections2.filter(
+        gerrit.api()
+            .changes()
+            .query(config.encodedQuery())
+            .withOption(ListChangesOption.ALL_FILES)
+            .withOption(ListChangesOption.CURRENT_COMMIT)
+            .withOption(ListChangesOption.CURRENT_REVISION)
+            .withOption(ListChangesOption.DETAILED_ACCOUNTS)
+            .withOption(ListChangesOption.DETAILED_LABELS)
+            .withLimit(LIMIT_QUERY)
+            .withStart(start)
+            .get(), new Predicate<ChangeInfo>() {
+          @Override
+          public boolean apply(ChangeInfo changeInfo) {
+            // filter out changes with no revisions
+            return changeInfo.revisions != null;
+          }
+        }), new Function<ChangeInfo, Change>() {
+      @Override
+      public Change apply(ChangeInfo changeInfo) {
+        return new Change(gerrit.api(), changeInfo);
+      }
+    }));
+    start += LIMIT_QUERY;
+    more = !result.isEmpty() && (result.getLast().info._moreChanges != null
+        ? result.getLast().info._moreChanges
+        : false);
+  }
+
+  public void reset() {
+    result.clear();
+    start = 0;
+    more = true;
+    page = 0;
+  }
+}
diff --git a/app/src/main/java/com/google/reviewit/app/ReviewItApp.java b/app/src/main/java/com/google/reviewit/app/ReviewItApp.java
index 10fb519..b2efb88 100644
--- a/app/src/main/java/com/google/reviewit/app/ReviewItApp.java
+++ b/app/src/main/java/com/google/reviewit/app/ReviewItApp.java
@@ -32,7 +32,8 @@
 public class ReviewItApp extends Application {
   private static final String TAG = ReviewItApp.class.getName();
 
-  private ActionHandler actionHandler;
+  private QueryHandler queryHandler;
+  private SortActionHandler sortActionHandler;
   private AvatarCache avatarCache;
   private ConfigManager cfgManager;
   private ExecutorService executor;
@@ -94,11 +95,18 @@
     super.onCreate();
   }
 
-  public ActionHandler getActionHandler() {
-    if (actionHandler == null) {
-      actionHandler = new ActionHandler(getConfigManager(), getGerrit());
+  public QueryHandler getQueryHandler() {
+    if (queryHandler == null) {
+      queryHandler = new QueryHandler(getConfigManager(), getGerrit());
     }
-    return actionHandler;
+    return queryHandler;
+  }
+
+  public SortActionHandler getSortActionHandler() {
+    if (sortActionHandler == null) {
+      sortActionHandler = new SortActionHandler(getConfigManager(), getGerrit());
+    }
+    return sortActionHandler;
   }
 
   public AvatarCache getAvatarCache() {
diff --git a/app/src/main/java/com/google/reviewit/app/ActionHandler.java b/app/src/main/java/com/google/reviewit/app/SortActionHandler.java
similarity index 96%
rename from app/src/main/java/com/google/reviewit/app/ActionHandler.java
rename to app/src/main/java/com/google/reviewit/app/SortActionHandler.java
index c182ea9..2a95b1e 100644
--- a/app/src/main/java/com/google/reviewit/app/ActionHandler.java
+++ b/app/src/main/java/com/google/reviewit/app/SortActionHandler.java
@@ -23,7 +23,7 @@
 
 import java.util.LinkedList;
 
-public class ActionHandler {
+public class SortActionHandler {
   public enum Action {
     /**
      * Do nothing.
@@ -90,7 +90,7 @@
    */
   private boolean more = true;
 
-  ActionHandler(ConfigManager cfgManager, Gerrit gerrit) {
+  SortActionHandler(ConfigManager cfgManager, Gerrit gerrit) {
     this.gerrit = gerrit;
     this.config = cfgManager.getQueryConfig();
 
@@ -147,7 +147,7 @@
     toProcess.addAll(Collections2.transform(Collections2.filter(
         gerrit.api()
             .changes()
-            .query(encodeQuery(config.query))
+            .query(config.encodedQuery())
             .withOption(ListChangesOption.ALL_FILES)
             .withOption(ListChangesOption.CURRENT_COMMIT)
             .withOption(ListChangesOption.CURRENT_REVISION)
@@ -179,10 +179,6 @@
     return toProcess.size() < THRESHOLD_QUEUE && more;
   }
 
-  private static String encodeQuery(String query) {
-    return query.replaceAll(" ", "+");
-  }
-
   /**
    * Stars the current change.
    */
diff --git a/app/src/main/java/com/google/reviewit/util/LayoutUtil.java b/app/src/main/java/com/google/reviewit/util/LayoutUtil.java
index 907ea6c..ff19c94 100644
--- a/app/src/main/java/com/google/reviewit/util/LayoutUtil.java
+++ b/app/src/main/java/com/google/reviewit/util/LayoutUtil.java
@@ -34,6 +34,11 @@
         ViewGroup.LayoutParams.WRAP_CONTENT);
   }
 
+  public static ViewGroup.LayoutParams matchAndFixedLayout(int height) {
+    return new ViewGroup.LayoutParams(
+        ViewGroup.LayoutParams.MATCH_PARENT, height);
+  }
+
   public static TableLayout.LayoutParams matchAndWrapTableLayout() {
     return new TableLayout.LayoutParams(
         TableLayout.LayoutParams.MATCH_PARENT,
diff --git a/app/src/main/java/com/google/reviewit/util/WidgetUtil.java b/app/src/main/java/com/google/reviewit/util/WidgetUtil.java
index df03587..6c2b92e 100644
--- a/app/src/main/java/com/google/reviewit/util/WidgetUtil.java
+++ b/app/src/main/java/com/google/reviewit/util/WidgetUtil.java
@@ -118,6 +118,28 @@
     return (int) context.getResources().getDimension(id);
   }
 
+  public float toDimension(String value, float defaultValue) {
+    if (value == null) {
+      return defaultValue;
+    }
+    try {
+      if (value.endsWith("sp")) {
+        value = value.substring(0, value.length() - 2).trim();
+        return spToPx(Integer.valueOf(value));
+      } else if (value.endsWith("dp")) {
+        value = value.substring(0, value.length() - 2).trim();
+        return dpToPx(Integer.valueOf(value));
+      } else if (value.endsWith("px")) {
+        value = value.substring(0, value.length() - 2).trim();
+        return Integer.valueOf(value);
+      } else {
+        return Integer.valueOf(value);
+      }
+    } catch (NumberFormatException e) {
+      return defaultValue;
+    }
+  }
+
   public void setBackgroundColor(View view, @ColorRes int colorId) {
     view.setBackgroundColor(color(colorId));
   }
diff --git a/app/src/main/java/com/google/reviewit/widget/ChangeBox.java b/app/src/main/java/com/google/reviewit/widget/ChangeBox.java
index bab77d8..dd5288e 100644
--- a/app/src/main/java/com/google/reviewit/widget/ChangeBox.java
+++ b/app/src/main/java/com/google/reviewit/widget/ChangeBox.java
@@ -85,16 +85,11 @@
     configureInfo(app);
 
     ChangeInfo info = change.info;
-    WidgetUtil.setText(v(R.id.project), info.project);
-    WidgetUtil.setText(v(R.id.branch), info.branch);
-    WidgetUtil.setText(v(R.id.topic), info.topic);
-    WidgetUtil.setText(v(R.id.age),
-        FormatUtil.formatDate(getContext(), info.updated));
+    ((ProjectBranchTopicAgeView)v(R.id.projectBranchTopicAge)).init(change);
+    ((UserView)v(R.id.owner)).init(app, info.owner);
     WidgetUtil.setText(v(R.id.subject), info.subject);
     WidgetUtil.setText(v(R.id.commitMessage),
         FormatUtil.formatMessage(change));
-    displayAvatar(app, info.owner);
-    WidgetUtil.setText(v(R.id.owner), FormatUtil.format(info.owner));
     WidgetUtil.setText(v(R.id.patchsets),
         FormatUtil.format(change.currentRevision()._number));
     setInlineCommentCount(app, change);
@@ -208,10 +203,6 @@
     return false;
   }
 
-  private void displayAvatar(ReviewItApp app,AccountInfo account) {
-    WidgetUtil.displayAvatar(app, account, (ImageView) v(R.id.avatar));
-  }
-
   private View v(@IdRes int id) {
     return findViewById(id);
   }
diff --git a/app/src/main/java/com/google/reviewit/widget/ChangeEntry.java b/app/src/main/java/com/google/reviewit/widget/ChangeEntry.java
new file mode 100644
index 0000000..cfd5e37
--- /dev/null
+++ b/app/src/main/java/com/google/reviewit/widget/ChangeEntry.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.reviewit.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.reviewit.R;
+import com.google.reviewit.app.Change;
+import com.google.reviewit.app.ReviewItApp;
+import com.google.reviewit.util.WidgetUtil;
+
+public class ChangeEntry extends LinearLayout {
+  public ChangeEntry(Context context) {
+    this(context, null, 0);
+  }
+
+  public ChangeEntry(Context context, AttributeSet attrs) {
+    this(context, attrs, 0);
+  }
+
+  public ChangeEntry(Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+
+    inflate(context, R.layout.change_entry, this);
+  }
+
+  public void init(ReviewItApp app, Change change) {
+    ((ProjectBranchTopicAgeView)findViewById(R.id.projectBranchTopicAge))
+        .init(change);
+    ((UserView)findViewById(R.id.owner)).init(app, change.info.owner);
+
+    ChangeInfo info = change.info;
+    WidgetUtil.setText(findViewById(R.id.subject), info.subject);
+  }
+}
diff --git a/app/src/main/java/com/google/reviewit/widget/ExpandableCommitMessageView.java b/app/src/main/java/com/google/reviewit/widget/ExpandableCommitMessageView.java
new file mode 100644
index 0000000..08ffca3
--- /dev/null
+++ b/app/src/main/java/com/google/reviewit/widget/ExpandableCommitMessageView.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.reviewit.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.google.reviewit.R;
+import com.google.reviewit.app.Change;
+import com.google.reviewit.util.WidgetUtil;
+
+import static com.google.reviewit.util.WidgetUtil.setGone;
+import static com.google.reviewit.util.WidgetUtil.setVisible;
+
+public class ExpandableCommitMessageView extends LinearLayout {
+  public ExpandableCommitMessageView(Context context) {
+    this(context, null, 0);
+  }
+
+  public ExpandableCommitMessageView(Context context, AttributeSet attrs) {
+    this(context, attrs, 0);
+  }
+
+  public ExpandableCommitMessageView(
+      Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+
+    inflate(context, R.layout.expandable_commit_message, this);
+  }
+
+  public void init(final Change change) {
+    WidgetUtil.setText(findViewById(R.id.subject), change.info.subject);
+
+    findViewById(R.id.expandCommitMessage).setOnClickListener(
+        new View.OnClickListener() {
+          @Override
+          public void onClick(View v) {
+            setGone(findViewById(R.id.expandCommitMessage));
+            setVisible(findViewById(R.id.collapseCommitMessage));
+            WidgetUtil.setText(findViewById(R.id.subject),
+                change.currentRevision().commit.message);
+          }
+    });
+
+    findViewById(R.id.collapseCommitMessage).setOnClickListener(
+        new View.OnClickListener() {
+          @Override
+          public void onClick(View v) {
+            setGone(findViewById(R.id.collapseCommitMessage));
+            setVisible(findViewById(R.id.expandCommitMessage));
+            WidgetUtil.setText(findViewById(R.id.subject), change.info.subject);
+          }
+        });
+  }
+}
diff --git a/app/src/main/java/com/google/reviewit/widget/MaxFontSizeTextView.java b/app/src/main/java/com/google/reviewit/widget/MaxFontSizeTextView.java
index f9a0920..765621f 100644
--- a/app/src/main/java/com/google/reviewit/widget/MaxFontSizeTextView.java
+++ b/app/src/main/java/com/google/reviewit/widget/MaxFontSizeTextView.java
@@ -24,6 +24,7 @@
 import android.widget.TextView;
 
 import com.google.reviewit.R;
+import com.google.reviewit.util.WidgetUtil;
 
 /**
  * TextView that automatically sets the font size as large as possible
@@ -46,13 +47,14 @@
 
   public MaxFontSizeTextView(Context context, AttributeSet attrs) {
     super(context, attrs);
+    WidgetUtil widgetUtil = new WidgetUtil(context);
     TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
         R.styleable.MaxFontSizeTextView, 0, 0);
     maxLineLength = a.getInteger(R.styleable
         .MaxFontSizeTextView_maxLineLength, 0);
-    minTextSize = readAttr(a.getString(
+    minTextSize = widgetUtil.toDimension(a.getString(
         R.styleable.MaxFontSizeTextView_minTextSize), MIN_TEXT_SIZE_DEFAULT);
-    maxTextSize = readAttr(a.getString(
+    maxTextSize = widgetUtil.toDimension(a.getString(
         R.styleable.MaxFontSizeTextView_maxTextSize), MAX_TEXT_SIZE_DEFAULT);
 
     paint = new Paint();
@@ -90,25 +92,6 @@
     }
   }
 
-  private float readAttr(String attr, float defaultValue) {
-    if (attr == null) {
-      return defaultValue;
-    }
-    try {
-      if (attr.endsWith("sp")) {
-        attr = attr.substring(0, attr.length() - 2).trim();
-        return spToPx(Integer.valueOf(attr));
-      } else if (attr.endsWith("px")) {
-        attr = attr.substring(0, attr.length() - 2).trim();
-        return Integer.valueOf(attr);
-      } else {
-        return Integer.valueOf(attr);
-      }
-    } catch (NumberFormatException e) {
-      return defaultValue;
-    }
-  }
-
   public void setMinTextSize(float minTextSize) {
     this.minTextSize = minTextSize;
   }
@@ -184,9 +167,4 @@
       refitText(getText(), width);
     }
   }
-
-  private int spToPx(int sp) {
-    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
-        getResources().getDisplayMetrics());
-  }
 }
diff --git a/app/src/main/java/com/google/reviewit/widget/ProjectBranchTopicAgeView.java b/app/src/main/java/com/google/reviewit/widget/ProjectBranchTopicAgeView.java
new file mode 100644
index 0000000..12bab21
--- /dev/null
+++ b/app/src/main/java/com/google/reviewit/widget/ProjectBranchTopicAgeView.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.reviewit.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.reviewit.R;
+import com.google.reviewit.app.Change;
+import com.google.reviewit.util.FormatUtil;
+import com.google.reviewit.util.WidgetUtil;
+
+import static com.google.reviewit.util.WidgetUtil.setVisible;
+
+public class ProjectBranchTopicAgeView extends LinearLayout {
+  public ProjectBranchTopicAgeView(Context context) {
+    this(context, null, 0);
+  }
+
+  public ProjectBranchTopicAgeView(Context context, AttributeSet attrs) {
+    this(context, attrs, 0);
+  }
+
+  public ProjectBranchTopicAgeView(
+      Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+
+    inflate(context, R.layout.project_branch_topic_age, this);
+  }
+
+  public void init(Change change) {
+    ChangeInfo info = change.info;
+    WidgetUtil.setText(findViewById(R.id.project), info.project);
+    WidgetUtil.setText(findViewById(R.id.branch), info.branch);
+    WidgetUtil.setText(findViewById(R.id.topic), info.topic);
+    WidgetUtil.setText(findViewById(R.id.age),
+        FormatUtil.formatDate(getContext(), info.updated));
+
+  }
+}
diff --git a/app/src/main/java/com/google/reviewit/widget/UserView.java b/app/src/main/java/com/google/reviewit/widget/UserView.java
new file mode 100644
index 0000000..23ab47d
--- /dev/null
+++ b/app/src/main/java/com/google/reviewit/widget/UserView.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.reviewit.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.reviewit.R;
+import com.google.reviewit.app.ReviewItApp;
+import com.google.reviewit.util.FormatUtil;
+import com.google.reviewit.util.WidgetUtil;
+
+public class UserView extends LinearLayout {
+  public UserView(Context context) {
+    this(context, null, 0);
+  }
+
+  public UserView(Context context, AttributeSet attrs) {
+    this(context, attrs, 0);
+  }
+
+  public UserView(
+      Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+
+    inflate(context, R.layout.user, this);
+
+    WidgetUtil widgetUtil = new WidgetUtil(context);
+    TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
+        R.styleable.UserView, 0, 0);
+    int avatarSize = (int) widgetUtil.toDimension(
+        a.getString(R.styleable.UserView_avatarSize), -1);
+    if (avatarSize > 0) {
+      findViewById(R.id.avatar).setLayoutParams(new LayoutParams(avatarSize,
+          avatarSize));
+    }
+
+    float textSize = widgetUtil.toDimension(
+        a.getString(R.styleable.UserView_textSize), -1);
+    if (textSize > 0) {
+      ((TextView)findViewById(R.id.userName)).setTextSize(
+          TypedValue.COMPLEX_UNIT_PX, textSize);
+    }
+  }
+
+  public void init(ReviewItApp app, AccountInfo account) {
+    WidgetUtil.displayAvatar(app, account,
+        (ImageView) findViewById(R.id.avatar));
+    WidgetUtil.setText(findViewById(R.id.userName), FormatUtil.format(account));
+  }
+}
diff --git a/app/src/main/res/layout/change_box.xml b/app/src/main/res/layout/change_box.xml
index e8eeccf..f802d0e 100644
--- a/app/src/main/res/layout/change_box.xml
+++ b/app/src/main/res/layout/change_box.xml
@@ -29,77 +29,10 @@
                 android:layout_height="match_parent"
                 android:layout_weight="1">
 
-    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                  android:orientation="horizontal"
-                  android:layout_width="match_parent"
-                  android:layout_height="wrap_content">
-
-      <LinearLayout
-        android:orientation="horizontal"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_weight="1">
-
-        <TextView
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:layout_marginRight="3dp"
-          android:textSize="9sp"
-          android:text="@string/project"/>
-
-        <TextView
-          android:id="@+id/project"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:textSize="9sp"/>
-      </LinearLayout>
-
-      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                    android:orientation="horizontal"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_weight="1">
-
-        <TextView
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:layout_marginRight="3dp"
-          android:textSize="9sp"
-          android:text="@string/branch"/>
-
-        <TextView
-          android:id="@+id/branch"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:textSize="9sp"/>
-      </LinearLayout>
-
-      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                    android:orientation="horizontal"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_weight="1">
-
-        <TextView
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:layout_marginRight="3dp"
-          android:textSize="9sp"
-          android:text="@string/topic"/>
-
-        <TextView
-          android:id="@+id/topic"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:textSize="9sp"/>
-      </LinearLayout>
-
-      <TextView
-        android:id="@+id/age"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:textSize="9sp"/>
-    </LinearLayout>
+    <com.google.reviewit.widget.ProjectBranchTopicAgeView
+      android:id="@+id/projectBranchTopicAge"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"/>
 
     <View
       android:background="@color/separator"
@@ -143,23 +76,11 @@
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content">
 
-    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                  android:orientation="horizontal"
-                  android:layout_width="wrap_content"
-                  android:layout_height="wrap_content"
-                  android:layout_marginTop="4dp">
-
-      <ImageView
-        android:id="@+id/avatar"
-        android:layout_width="20dp"
-        android:layout_height="20dp"
-        android:layout_marginRight="3dp"/>
-
-      <TextView
-        android:id="@+id/owner"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"/>
-    </LinearLayout>
+    <com.google.reviewit.widget.UserView
+      android:id="@+id/owner"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginTop="4dp"/>
 
     <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                   android:orientation="horizontal"
diff --git a/app/src/main/res/layout/change_entry.xml b/app/src/main/res/layout/change_entry.xml
new file mode 100644
index 0000000..411470d
--- /dev/null
+++ b/app/src/main/res/layout/change_entry.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:app="http://schemas.android.com/apk/res-auto"
+              android:orientation="horizontal"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:paddingLeft="3dp"
+              android:paddingTop="3dp"
+              android:paddingRight="3dp"
+              android:paddingBottom="3dp">
+
+  <LinearLayout
+    android:orientation="vertical"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_weight="1">
+
+    <com.google.reviewit.widget.ProjectBranchTopicAgeView
+      android:id="@+id/projectBranchTopicAge"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"/>
+
+    <TextView
+      android:id="@+id/subject"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"/>
+
+    <com.google.reviewit.widget.UserView
+      android:id="@+id/owner"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      app:textSize="10sp"
+      app:avatarSize="15dp"/>
+  </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/content_abandon.xml b/app/src/main/res/layout/content_abandon.xml
index da2b061..a1c8b7f 100644
--- a/app/src/main/res/layout/content_abandon.xml
+++ b/app/src/main/res/layout/content_abandon.xml
@@ -14,19 +14,27 @@
     See the License for the specific language governing permissions and
     limitations under the License. -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                xmlns:app="http://schemas.android.com/apk/res-auto"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:paddingBottom="@dimen/activity_vertical_margin"
-                android:paddingLeft="@dimen/activity_horizontal_margin"
-                android:paddingRight="@dimen/activity_horizontal_margin"
-                android:paddingTop="@dimen/activity_vertical_margin"
-                app:layout_behavior="@string/appbar_scrolling_view_behavior">
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical"
+  app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+  <com.google.reviewit.widget.ExpandableCommitMessageView
+    android:id="@+id/commitMessage"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="7dp"
+    android:background="@drawable/navigation_button_bar"/>
 
   <ScrollView
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin">
 
     <LinearLayout
       android:orientation="vertical"
@@ -58,4 +66,4 @@
         android:text="@string/abandon"/>
     </LinearLayout>
   </ScrollView>
-</RelativeLayout>
+</LinearLayout>
diff --git a/app/src/main/res/layout/content_post_review.xml b/app/src/main/res/layout/content_post_review.xml
index 0347ae0..6c2f411 100644
--- a/app/src/main/res/layout/content_post_review.xml
+++ b/app/src/main/res/layout/content_post_review.xml
@@ -32,41 +32,11 @@
 
     <include layout="@layout/progress"/>
 
-    <LinearLayout
+    <com.google.reviewit.widget.ExpandableCommitMessageView
+      android:id="@+id/commitMessage"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
-      android:paddingTop="5dp"
-      android:orientation="horizontal">
-
-      <ImageView
-        android:id="@+id/expandCommitMessage"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="-7dp"
-        android:clickable="true"
-        android:src="@drawable/ic_keyboard_arrow_right_white_36dp"/>
-
-      <ImageView
-        android:id="@+id/collapseCommitMessage"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="-7dp"
-        android:clickable="true"
-        android:src="@drawable/ic_keyboard_arrow_down_white_36dp"
-        android:visibility="gone"/>
-
-      <TextView
-        android:id="@+id/subject"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:padding="5dp"
-        android:layout_marginBottom="5dp"
-        android:layout_marginRight="36dp"
-        android:layout_weight="1"
-        android:textSize="9sp"
-        android:background="@drawable/grey_page_border"
-        android:textColor="@color/darkGrey"/>
-    </LinearLayout>
+      android:paddingTop="5dp"/>
 
     <LinearLayout
       android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/content_sort_settings.xml b/app/src/main/res/layout/content_query_settings.xml
similarity index 100%
rename from app/src/main/res/layout/content_sort_settings.xml
rename to app/src/main/res/layout/content_query_settings.xml
diff --git a/app/src/main/res/layout/content_restore.xml b/app/src/main/res/layout/content_restore.xml
index 5d6e879..e90f9a9 100644
--- a/app/src/main/res/layout/content_restore.xml
+++ b/app/src/main/res/layout/content_restore.xml
@@ -14,19 +14,27 @@
     See the License for the specific language governing permissions and
     limitations under the License. -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                xmlns:app="http://schemas.android.com/apk/res-auto"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:paddingBottom="@dimen/activity_vertical_margin"
-                android:paddingLeft="@dimen/activity_horizontal_margin"
-                android:paddingRight="@dimen/activity_horizontal_margin"
-                android:paddingTop="@dimen/activity_vertical_margin"
-                app:layout_behavior="@string/appbar_scrolling_view_behavior">
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:app="http://schemas.android.com/apk/res-auto"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical"
+              app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+  <com.google.reviewit.widget.ExpandableCommitMessageView
+    android:id="@+id/commitMessage"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="7dp"
+    android:background="@drawable/navigation_button_bar"/>
 
   <ScrollView
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin">
 
     <LinearLayout
       android:orientation="vertical"
@@ -58,4 +66,4 @@
         android:text="@string/restore"/>
     </LinearLayout>
   </ScrollView>
-</RelativeLayout>
+</LinearLayout>
diff --git a/app/src/main/res/layout/content_review_changes.xml b/app/src/main/res/layout/content_review_changes.xml
index 456ce76..3851a97 100644
--- a/app/src/main/res/layout/content_review_changes.xml
+++ b/app/src/main/res/layout/content_review_changes.xml
@@ -15,14 +15,47 @@
     limitations under the License. -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              android:orientation="vertical"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent">
+  android:orientation="vertical"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
 
-  <TextView
+  <include layout="@layout/progress"/>
+
+  <ScrollView
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:gravity="center"
-    android:textSize="40sp"
-    android:text="TODO"/>
-</LinearLayout>
\ No newline at end of file
+    android:layout_height="match_parent">
+
+    <LinearLayout
+      android:id="@+id/changeList"
+      android:orientation="vertical"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent">
+
+      <TextView
+        android:id="@+id/statusText"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:layout_marginTop="15dp"
+        android:textSize="24sp"
+        android:text="@string/loading"/>
+
+      <ProgressBar
+        android:id="@+id/initialProgress"
+        android:layout_marginTop="20dp"
+        android:layout_width="match_parent"
+        android:layout_height="150dp"
+        android:indeterminate="true"/>
+
+      <ImageView
+        android:id="@+id/reloadButton"
+        android:layout_width="match_parent"
+        android:layout_height="150dp"
+        android:layout_marginTop="20dp"
+        android:gravity="center_horizontal"
+        android:clickable="true"
+        android:src="@drawable/ic_refresh_black_48dp"
+        android:visibility="gone"/>
+    </LinearLayout>
+  </ScrollView>
+</LinearLayout>
diff --git a/app/src/main/res/layout/content_settings.xml b/app/src/main/res/layout/content_settings.xml
index 41bd70a..78eecf8 100644
--- a/app/src/main/res/layout/content_settings.xml
+++ b/app/src/main/res/layout/content_settings.xml
@@ -49,10 +49,10 @@
         android:layout_marginBottom="10dp"/>
 
       <TextView
-        android:id="@+id/sortConfig"
+        android:id="@+id/queryConfig"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:text="@string/sortConfig"
+        android:text="@string/queryConfig"
         android:gravity="center_horizontal"
         android:textSize="20sp"/>
     </LinearLayout>
diff --git a/app/src/main/res/layout/expandable_commit_message.xml b/app/src/main/res/layout/expandable_commit_message.xml
new file mode 100644
index 0000000..dbc2e07
--- /dev/null
+++ b/app/src/main/res/layout/expandable_commit_message.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:orientation="horizontal">
+
+  <ImageView
+    android:id="@+id/expandCommitMessage"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="-7dp"
+    android:clickable="true"
+    android:src="@drawable/ic_keyboard_arrow_right_white_36dp"/>
+
+  <ImageView
+    android:id="@+id/collapseCommitMessage"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="-7dp"
+    android:clickable="true"
+    android:src="@drawable/ic_keyboard_arrow_down_white_36dp"
+    android:visibility="gone"/>
+
+  <TextView
+    android:id="@+id/subject"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:padding="5dp"
+    android:layout_marginBottom="5dp"
+    android:layout_marginRight="36dp"
+    android:layout_weight="1"
+    android:textSize="9sp"
+    android:background="@drawable/grey_page_border"
+    android:textColor="@color/darkGrey"/>
+</LinearLayout>
diff --git a/app/src/main/res/layout/project_branch_topic_age.xml b/app/src/main/res/layout/project_branch_topic_age.xml
new file mode 100644
index 0000000..8c945bf
--- /dev/null
+++ b/app/src/main/res/layout/project_branch_topic_age.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="horizontal"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content">
+
+  <LinearLayout
+    android:orientation="horizontal"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_weight="1">
+
+    <TextView
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginRight="3dp"
+      android:textSize="9sp"
+      android:text="@string/project"/>
+
+    <TextView
+      android:id="@+id/project"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:textSize="9sp"/>
+  </LinearLayout>
+
+  <LinearLayout
+    android:orientation="horizontal"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_weight="1">
+
+    <TextView
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginRight="3dp"
+      android:textSize="9sp"
+      android:text="@string/branch"/>
+
+    <TextView
+      android:id="@+id/branch"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:textSize="9sp"/>
+  </LinearLayout>
+
+  <LinearLayout
+    android:orientation="horizontal"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_weight="1">
+
+    <TextView
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginRight="3dp"
+      android:textSize="9sp"
+      android:text="@string/topic"/>
+
+    <TextView
+      android:id="@+id/topic"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:textSize="9sp"/>
+  </LinearLayout>
+
+  <TextView
+    android:id="@+id/age"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:textSize="9sp"/>
+</LinearLayout>
diff --git a/app/src/main/res/layout/user.xml b/app/src/main/res/layout/user.xml
new file mode 100644
index 0000000..190b04b
--- /dev/null
+++ b/app/src/main/res/layout/user.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="horizontal"
+  android:layout_width="wrap_content"
+  android:layout_height="wrap_content">
+
+  <ImageView
+    android:id="@+id/avatar"
+    android:layout_width="20dp"
+    android:layout_height="20dp"
+    android:layout_marginRight="3dp"/>
+
+  <TextView
+    android:id="@+id/userName"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"/>
+</LinearLayout>
diff --git a/app/src/main/res/menu/menu_review_changes.xml b/app/src/main/res/menu/menu_review_changes.xml
new file mode 100644
index 0000000..af11e52
--- /dev/null
+++ b/app/src/main/res/menu/menu_review_changes.xml
@@ -0,0 +1,22 @@
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto">
+  <item
+    android:id="@+id/action_reload_query"
+    android:orderInCategory="100"
+    android:title="@string/action_reload_query"
+    app:showAsAction="never"/>
+</menu>
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 662122a..3d6fa8e 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -21,4 +21,9 @@
     <attr name="maxLineLength" format="integer"/>
     <attr name="ellipsize" format="boolean"/>
   </declare-styleable>
+
+  <declare-styleable name="UserView">
+    <attr name="avatarSize" format="string"/>
+    <attr name="textSize" format="string"/>
+  </declare-styleable>
 </resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 68e7525..c4de3bc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -40,6 +40,9 @@
   <string name="action_restore">Restore...</string>
   <string name="action_undo">Undo</string>
 
+  <!-- ReviewChangesFragment -->
+  <string name="no_changes_match">No changes match the query!</string>
+
   <!-- ErrorFragment -->
   <string name="title_activity_error">Error</string>
 
@@ -131,7 +134,7 @@
 
   <!-- SettingsFragment -->
   <string name="serverConfig">Server Configuration</string>
-  <string name="sortConfig">Sort Configuration</string>
+  <string name="queryConfig">Query Configuration</string>
 
   <string name="add_server">Add Server</string>
   <string name="connection_failed">Connection failed: %1$s</string>