Add option to limit suggestions displayed by space available.

This effectively makes the suggestions list non-scrollable, by
limiting the number of suggestions to the number that can fit in the
available space.

Also fixed an issue in RankAwarePromoted whereby it would promote too
many suggestions if the promoted list was non-empty before hand.

Bug: 3086387

Change-Id: Iae5740fbd32b104c73b1b7dbf446c7e61a3e2e6a
diff --git a/res/layout-xlarge/search_activity.xml b/res/layout-xlarge/search_activity.xml
index 7de78e3..47c48f5 100644
--- a/res/layout-xlarge/search_activity.xml
+++ b/res/layout-xlarge/search_activity.xml
@@ -39,18 +39,28 @@
                 android:layout_marginLeft="20dp"
                 android:layout_marginBottom="12dip"
                 android:background="#00000000">
-            <view
-                class="com.android.quicksearchbox.ui.SuggestionsView"
-                android:id="@+id/suggestions"
+
+            <FrameLayout
+                android:id="@+id/suggestions_container"
                 android:layout_width="match_parent"
-                android:layout_height="wrap_content"
+                android:layout_height="match_parent"
                 android:layout_alignParentLeft="true"
                 android:layout_alignParentRight="true"
                 android:layout_below="@+id/search_edit_frame_plus_buttons"
-                android:background="#408080FF"
-                android:cacheColorHint="@android:color/transparent"
+                android:background="#00000000"
                 >
-            </view>
+
+                <view
+                    class="com.android.quicksearchbox.ui.SuggestionsView"
+                    android:id="@+id/suggestions"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:background="#408080FF"
+                    android:cacheColorHint="@android:color/transparent"
+                    >
+                </view>
+
+            </FrameLayout>
 
             <LinearLayout
                 android:id="@+id/search_edit_frame_plus_buttons"
@@ -133,7 +143,7 @@
                 android:id="@+id/right_pane"
                 android:layout_width="400dp"
                 android:layout_height="match_parent"
-                android:background="#408080FF"
+                android:background="#00000000"
                 android:layout_marginBottom="12dip"
                 android:layout_marginLeft="12dp">
 
@@ -149,21 +159,33 @@
                 android:cacheColorHint="@android:color/transparent"
                 android:listSelector="@null"
                 android:scrollbars="vertical"
+                android:background="#408080FF"
                 />
 
-            <view
-                class="com.android.quicksearchbox.ui.SuggestionsView"
-                android:id="@+id/shortcuts"
+            <FrameLayout
+                android:id="@+id/shortcuts_container"
                 android:layout_width="match_parent"
-                android:layout_height="wrap_content"
+                android:layout_height="match_parent"
                 android:layout_alignParentLeft="true"
                 android:layout_alignParentRight="true"
                 android:layout_below="@+id/shortcut_title"
                 android:layout_above="@+id/corpus_list"
-                android:cacheColorHint="@android:color/transparent"
                 android:layout_alignWithParentIfMissing="true"
+                android:background="#00000000"
                 >
-            </view>
+
+                <view
+                    class="com.android.quicksearchbox.ui.SuggestionsView"
+                    android:id="@+id/shortcuts"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:background="#408080FF"
+                    android:cacheColorHint="@android:color/transparent"
+                    >
+
+                 </view>
+
+            </FrameLayout>
 
             <!-- The search plate is after the suggestions, to give it a higher
                  z-index. -->
diff --git a/res/values/config.xml b/res/values/config.xml
index 19b41b1..298fd63 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -68,4 +68,9 @@
   <!-- Whether or not to show zero-query shortcuts -->
   <bool name="show_zero_query_shortcuts">true</bool>
 
+  <!-- Whether or not to show scrolling suggestions. If this returns false, the number of
+   suggestions displayed will be limited by the height of the suggestions view, so that it
+   does not scroll. -->
+  <bool name="show_scrolling_suggestions">false</bool>
+
 </resources>
diff --git a/src/com/android/quicksearchbox/Config.java b/src/com/android/quicksearchbox/Config.java
index eec1960..f768067 100644
--- a/src/com/android/quicksearchbox/Config.java
+++ b/src/com/android/quicksearchbox/Config.java
@@ -346,4 +346,13 @@
         }
     }
 
+    public boolean showScrollingSuggestions() {
+        try {
+            return mContext.getResources().getBoolean(R.bool.show_scrolling_suggestions);
+        } catch (Resources.NotFoundException ex) {
+            Log.e(TAG, "Could not load show_zero_query_shortcuts", ex);
+            return true;
+        }
+    }
+
 }
diff --git a/src/com/android/quicksearchbox/RankAwarePromoter.java b/src/com/android/quicksearchbox/RankAwarePromoter.java
index 1424def..6d89ce8 100644
--- a/src/com/android/quicksearchbox/RankAwarePromoter.java
+++ b/src/com/android/quicksearchbox/RankAwarePromoter.java
@@ -66,29 +66,31 @@
             }
         }
 
+        int slotsLeft = Math.max(0, maxPromoted - promoted.getCount());
+
         // Share the top slots equally among each of the default corpora
-        if (maxPromoted > 0 && !defaultResults.isEmpty()) {
-            int slotsToFill = Math.min(getSlotsAboveKeyboard() - promoted.getCount(), maxPromoted);
+        if (slotsLeft > 0 && !defaultResults.isEmpty()) {
+            int slotsToFill = Math.min(getSlotsAboveKeyboard() - promoted.getCount(), slotsLeft);
             if (slotsToFill > 0) {
                 int stripeSize = Math.max(1, slotsToFill / defaultResults.size());
-                maxPromoted -= roundRobin(defaultResults, slotsToFill, stripeSize, promoted);
+                slotsLeft -= roundRobin(defaultResults, slotsToFill, stripeSize, promoted);
             }
         }
 
         // Then try to fill with the remaining promoted results
-        if (maxPromoted > 0 && !defaultResults.isEmpty()) {
-            int stripeSize = Math.max(1, maxPromoted / defaultResults.size());
-            maxPromoted -= roundRobin(defaultResults, maxPromoted, stripeSize, promoted);
+        if (slotsLeft > 0 && !defaultResults.isEmpty()) {
+            int stripeSize = Math.max(1, slotsLeft / defaultResults.size());
+            slotsLeft -= roundRobin(defaultResults, slotsLeft, stripeSize, promoted);
             // We may still have a few slots left
-            maxPromoted -= roundRobin(defaultResults, maxPromoted, maxPromoted, promoted);
+            slotsLeft -= roundRobin(defaultResults, slotsLeft, slotsLeft, promoted);
         }
 
         // Then try to fill with the rest
-        if (maxPromoted > 0 && !otherResults.isEmpty()) {
-            int stripeSize = Math.max(1, maxPromoted / otherResults.size());
-            maxPromoted -= roundRobin(otherResults, maxPromoted, stripeSize, promoted);
+        if (slotsLeft > 0 && !otherResults.isEmpty()) {
+            int stripeSize = Math.max(1, slotsLeft / otherResults.size());
+            slotsLeft -= roundRobin(otherResults, slotsLeft, stripeSize, promoted);
             // We may still have a few slots left
-            maxPromoted -= roundRobin(otherResults, maxPromoted, maxPromoted, promoted);
+            slotsLeft -= roundRobin(otherResults, slotsLeft, slotsLeft, promoted);
         }
 
         if (DBG) Log.d(TAG, "Returning " + promoted.toString());
diff --git a/src/com/android/quicksearchbox/SearchActivity.java b/src/com/android/quicksearchbox/SearchActivity.java
index dc2fdf0..baabf80 100644
--- a/src/com/android/quicksearchbox/SearchActivity.java
+++ b/src/com/android/quicksearchbox/SearchActivity.java
@@ -102,7 +102,11 @@
 
         mSearchActivityView = setupContentView();
 
-        mSearchActivityView.setMaxPromoted(getConfig().getMaxPromotedSuggestions());
+        if (getConfig().showScrollingSuggestions()) {
+            mSearchActivityView.setMaxPromoted(getConfig().getMaxPromotedSuggestions());
+        } else {
+            mSearchActivityView.limitSuggestionsToViewHeight();
+        }
 
         mSearchActivityView.setSearchClickListener(new SearchActivityView.SearchClickListener() {
             public boolean onSearchClicked(int method) {
diff --git a/src/com/android/quicksearchbox/ui/SearchActivityView.java b/src/com/android/quicksearchbox/ui/SearchActivityView.java
index 0b1965c..362039a 100644
--- a/src/com/android/quicksearchbox/ui/SearchActivityView.java
+++ b/src/com/android/quicksearchbox/ui/SearchActivityView.java
@@ -223,9 +223,14 @@
     }
 
     public void setMaxPromoted(int maxPromoted) {
+        mSuggestionsView.setLimitSuggestionsToViewHeight(false);
         mSuggestionsAdapter.setMaxPromoted(maxPromoted);
     }
 
+    public void limitSuggestionsToViewHeight() {
+        mSuggestionsView.setLimitSuggestionsToViewHeight(true);
+    }
+
     public void setQueryListener(QueryListener listener) {
         mQueryListener = listener;
     }
diff --git a/src/com/android/quicksearchbox/ui/SearchActivityViewTwoPane.java b/src/com/android/quicksearchbox/ui/SearchActivityViewTwoPane.java
index f7fcc71..985edd4 100644
--- a/src/com/android/quicksearchbox/ui/SearchActivityViewTwoPane.java
+++ b/src/com/android/quicksearchbox/ui/SearchActivityViewTwoPane.java
@@ -139,10 +139,18 @@
     @Override
     public void setMaxPromoted(int maxPromoted) {
         super.setMaxPromoted(maxPromoted);
+        mResultsView.setLimitSuggestionsToViewHeight(false);
         mResultsAdapter.setMaxPromoted(maxPromoted);
     }
 
     @Override
+    public void limitSuggestionsToViewHeight() {
+        super.limitSuggestionsToViewHeight();
+        mResultsView.setLimitSuggestionsToViewHeight(true);
+    }
+
+
+    @Override
     public void setSettingsButtonClickListener(OnClickListener listener) {
         mSettingsButton.setOnClickListener(listener);
     }
diff --git a/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java b/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java
index 34255f5..2536a9b 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java
@@ -99,6 +99,7 @@
     }
 
     public void setMaxPromoted(int maxPromoted) {
+        if (DBG) Log.d(TAG, "setMaxPromoted " + maxPromoted);
         mMaxPromoted = maxPromoted;
         onSuggestionsChanged();
     }
@@ -184,7 +185,6 @@
 
     @Override
     public int getItemViewType(int position) {
-        if (DBG) Log.d(TAG, "getItemViewType(" + position + ") mCursor=" + mCursor);
         if (mCursor == null) {
             return 0;
         }
@@ -246,7 +246,10 @@
      * {@link #setSuggestions(Suggestions)}.
      */
     private void changeCursor(SuggestionCursor newCursor) {
-        if (DBG) Log.d(TAG, "changeCursor(" + newCursor + ")");
+        if (DBG) {
+            Log.d(TAG, "changeCursor(" + newCursor + ") count=" +
+                    (newCursor == null ? 0 : newCursor.getCount()));
+        }
         if (newCursor == mCursor) {
             if (newCursor != null) {
                 // Shortcuts may have changed without the cursor changing.
diff --git a/src/com/android/quicksearchbox/ui/SuggestionsView.java b/src/com/android/quicksearchbox/ui/SuggestionsView.java
index be81d55..c3d9e74 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionsView.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionsView.java
@@ -16,10 +16,14 @@
 
 package com.android.quicksearchbox.ui;
 
+import com.android.quicksearchbox.R;
 import com.android.quicksearchbox.SuggestionPosition;
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.FrameLayout;
 import android.widget.ListAdapter;
 import android.widget.ListView;
 
@@ -31,6 +35,8 @@
     private static final boolean DBG = false;
     private static final String TAG = "QSB.SuggestionsView";
 
+    private boolean mLimitSuggestionsToViewHeight;
+
     public SuggestionsView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
@@ -42,6 +48,9 @@
                     "SuggestionsView adapter must be a SuggestionsAdapter (got " + adapter + ")");
         }
         super.setAdapter(adapter);
+        if (mLimitSuggestionsToViewHeight) {
+            setMaxPromotedByHeight();
+        }
     }
 
     @Override
@@ -74,4 +83,45 @@
         return (SuggestionPosition) getSelectedItem();
     }
 
+    public void setLimitSuggestionsToViewHeight(boolean limit) {
+        mLimitSuggestionsToViewHeight = limit;
+        if (mLimitSuggestionsToViewHeight) {
+            setMaxPromotedByHeight();
+        }
+    }
+
+    @Override
+    protected void onSizeChanged (int w, int h, int oldw, int oldh) {
+        if (mLimitSuggestionsToViewHeight) {
+            setMaxPromotedByHeight();
+        }
+    }
+
+    private void setMaxPromotedByHeight() {
+        SuggestionsAdapter adapter = getAdapter();
+        if (adapter != null) {
+            float maxHeight;
+            if (getParent() instanceof FrameLayout) {
+                // We put the SuggestionView inside a frame layout so that we know what its
+                // maximum height is. Since this views height is set to 'wrap content' (in two-pane
+                // mode at least), we can't use our own height for these calculations.
+                maxHeight = ((View) getParent()).getHeight();
+                if (DBG) Log.d(TAG, "Parent height=" + maxHeight);
+            } else {
+                maxHeight = getHeight();
+                if (DBG) Log.d(TAG, "This height=" + maxHeight);
+            }
+            float suggestionHeight =
+                getContext().getResources().getDimension(R.dimen.suggestion_view_height);
+            if (suggestionHeight != 0) {
+                int suggestions = Math.max(1, (int) Math.floor(maxHeight / suggestionHeight));
+                if (DBG) {
+                    Log.d(TAG, "view height=" + maxHeight + " suggestion height=" +
+                            suggestionHeight + " -> maxSuggestions=" + suggestions);
+                }
+                adapter.setMaxPromoted(suggestions);
+            }
+        }
+    }
+
 }
diff --git a/tests/src/com/android/quicksearchbox/RankAwarePromoterTest.java b/tests/src/com/android/quicksearchbox/RankAwarePromoterTest.java
index 20cb905..8678eac 100644
--- a/tests/src/com/android/quicksearchbox/RankAwarePromoterTest.java
+++ b/tests/src/com/android/quicksearchbox/RankAwarePromoterTest.java
@@ -32,7 +32,8 @@
     public static final int MAX_PROMOTED_SUGGESTIONS = 8;
     public static final String TEST_QUERY = "query";
 
-    private List<Corpus> mCorpora = createMockCorpora(5, MAX_PROMOTED_CORPORA);
+    private final List<Corpus> mCorpora = createMockCorpora(5, MAX_PROMOTED_CORPORA);
+    private final Corpus mShortcuts = createMockShortcutsCorpus();
     private RankAwarePromoter mPromoter;
 
     @Override
@@ -66,6 +67,18 @@
         }
     }
 
+    public void testPromotesRightNumberOfSuggestions() {
+        List<CorpusResult> suggestions = getSuggestions(TEST_QUERY);
+        ListSuggestionCursor promoted = new ListSuggestionCursor(TEST_QUERY);
+        SuggestionCursor shortcuts = mShortcuts.
+                getSuggestions(TEST_QUERY, MAX_PROMOTED_SUGGESTIONS / 2, true);
+        for (int i = 0; i < shortcuts.getCount(); ++i) {
+            promoted.add(new SuggestionPosition(shortcuts, 1));
+        }
+        mPromoter.promoteSuggestions(suggestions, MAX_PROMOTED_SUGGESTIONS, promoted);
+        assertEquals(MAX_PROMOTED_SUGGESTIONS, promoted.getCount());
+    }
+
     private List<CorpusResult> getSuggestions(String query) {
         ArrayList<CorpusResult> results = new ArrayList<CorpusResult>();
         for (Corpus corpus : mCorpora) {
@@ -83,4 +96,11 @@
         }
         return corpora;
     }
+
+    private static Corpus createMockShortcutsCorpus() {
+        Source mockSource = new MockSource("Shortcuts");
+        Corpus mockCorpus = new MockCorpus(mockSource, true);
+        return mockCorpus;
+    }
+
 }