Implement change list for reviewing changes

This is a first step to provide a change list from which the user can
select changes for review. This change only implements a basic version
of the change list that is not fully implemented yet:
- Selecting a change does not trigger any action yet.
- It only shows the first page of changes, it is not possible yet to
  load further pages.
- The entry for a change does not yet display all information that we
  would like to show here (e.g. current votes are missing).

Change-Id: I36bf7b3737240b8434475590c13c47e0afa01a3a
Signed-off-by: Edwin Kempin <ekempin@google.com>
diff --git a/app/src/main/java/com/google/reviewit/AbandonFragment.java b/app/src/main/java/com/google/reviewit/AbandonFragment.java
index 4233411..dcc2137 100644
--- a/app/src/main/java/com/google/reviewit/AbandonFragment.java
+++ b/app/src/main/java/com/google/reviewit/AbandonFragment.java
@@ -50,7 +50,7 @@
   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);
   }
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 b887e48..bb431db 100644
--- a/app/src/main/java/com/google/reviewit/PostReviewFragment.java
+++ b/app/src/main/java/com/google/reviewit/PostReviewFragment.java
@@ -71,7 +71,7 @@
 
     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);
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 5ec9622..45d398a 100644
--- a/app/src/main/java/com/google/reviewit/RestoreFragment.java
+++ b/app/src/main/java/com/google/reviewit/RestoreFragment.java
@@ -50,7 +50,7 @@
   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);
   }
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/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/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_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_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/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/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>