/* * Copyright (C) 2010 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.android.quicksearchbox; import com.google.common.annotations.VisibleForTesting; import android.util.Log; import java.util.Iterator; import java.util.LinkedList; /** * A promoter that gives preference to suggestions from higher ranking corpora. */ public class RankAwarePromoter extends AbstractPromoter { private static final boolean DBG = false; private static final String TAG = "QSB.RankAwarePromoter"; public RankAwarePromoter(Config config, SuggestionFilter filter, Promoter next) { super(filter, next, config); } @Override public void doPickPromoted(Suggestions suggestions, int maxPromoted, ListSuggestionCursor promoted) { promoteSuggestions(suggestions.getCorpusResults(), maxPromoted, promoted); } @VisibleForTesting void promoteSuggestions(Iterable<CorpusResult> suggestions, int maxPromoted, ListSuggestionCursor promoted) { if (DBG) Log.d(TAG, "Available results: " + suggestions); // Split non-empty results into important suggestions and not-so-important // suggestions, each corpus's cursor positioned at the first suggestion. LinkedList<CorpusResult> highRankingSuggestions = new LinkedList<CorpusResult>(); LinkedList<CorpusResult> lowRankingSuggestions = new LinkedList<CorpusResult>(); partitionSuggestionsByRank(suggestions, highRankingSuggestions, lowRankingSuggestions); // Top results, evenly distributed between each high-ranking corpus. promoteTopSuggestions(highRankingSuggestions, promoted, maxPromoted); // Then try to fill promoted list with the remaining high-ranking suggestions, // and then use the low-ranking suggestions if the list isn't full yet. promoteEquallyFromEachCorpus(highRankingSuggestions, promoted, maxPromoted); promoteEquallyFromEachCorpus(lowRankingSuggestions, promoted, maxPromoted); if (DBG) Log.d(TAG, "Returning " + promoted.toString()); } /** * Shares the top slots evenly among each of the high-ranking (default) corpora. * * The corpora will appear in the promoted list in the order they are listed * among the incoming suggestions (this method doesn't change their order). */ private void promoteTopSuggestions(LinkedList<CorpusResult> highRankingSuggestions, ListSuggestionCursor promoted, int maxPromoted) { int slotsLeft = getSlotsLeft(promoted, maxPromoted); if (slotsLeft > 0 && !highRankingSuggestions.isEmpty()) { int slotsToFill = Math.min(getSlotsAboveKeyboard() - promoted.getCount(), slotsLeft); if (slotsToFill > 0) { int stripeSize = Math.max(1, slotsToFill / highRankingSuggestions.size()); roundRobin(highRankingSuggestions, slotsToFill, stripeSize, promoted); } } } /** * Tries to promote the same number of elements from each corpus. * * The corpora will appear in the promoted list in the order they are listed * among the incoming suggestions (this method doesn't change their order). */ private void promoteEquallyFromEachCorpus(LinkedList<CorpusResult> suggestions, ListSuggestionCursor promoted, int maxPromoted) { int slotsLeft = getSlotsLeft(promoted, maxPromoted); if (slotsLeft == 0) { // No more items to add. return; } if (suggestions.isEmpty()) { return; } int stripeSize = Math.max(1, slotsLeft / suggestions.size()); roundRobin(suggestions, slotsLeft, stripeSize, promoted); // We may still have a few slots left slotsLeft = getSlotsLeft(promoted, maxPromoted); roundRobin(suggestions, slotsLeft, slotsLeft, promoted); } /** * Partitions the suggestions into "important" (high-ranking) * and "not-so-important" (low-ranking) suggestions, dependent on the * rank of the corpus the result is part of. * * @param suggestions * @param highRankingSuggestions These should be displayed first to the * user. * @param lowRankingSuggestions These should be displayed if the * high-ranking suggestions don't fill all the available space in the * result view. */ private void partitionSuggestionsByRank(Iterable<CorpusResult> suggestions, LinkedList<CorpusResult> highRankingSuggestions, LinkedList<CorpusResult> lowRankingSuggestions) { for (CorpusResult result : suggestions) { if (result.getCount() > 0) { result.moveTo(0); Corpus corpus = result.getCorpus(); if (isCorpusHighlyRanked(corpus)) { highRankingSuggestions.add(result); } else { lowRankingSuggestions.add(result); } } } } private boolean isCorpusHighlyRanked(Corpus corpus) { // The default corpora shipped with QSB (apps, etc.) are // more important than ones that were registered later. return corpus == null || corpus.isCorpusDefaultEnabled(); } private int getSlotsLeft(ListSuggestionCursor promoted, int maxPromoted) { // It's best to calculate this after each addition because duplicates // may get filtered out automatically in the list of promoted items. return Math.max(0, maxPromoted - promoted.getCount()); } private int getSlotsAboveKeyboard() { return getConfig().getNumSuggestionsAboveKeyboard(); } /** * Promotes "stripes" of suggestions from each corpus. * * @param results the list of CorpusResults from which to promote. * Exhausted CorpusResults are removed from the list. * @param maxPromoted maximum number of suggestions to promote. * @param stripeSize number of suggestions to take from each corpus. * @param promoted the list to which promoted suggestions are added. * @return the number of suggestions actually promoted. */ private int roundRobin(LinkedList<CorpusResult> results, int maxPromoted, int stripeSize, ListSuggestionCursor promoted) { int count = 0; if (maxPromoted > 0 && !results.isEmpty()) { for (Iterator<CorpusResult> iter = results.iterator(); count < maxPromoted && iter.hasNext();) { CorpusResult result = iter.next(); count += promote(result, stripeSize, promoted); if (result.getPosition() == result.getCount()) { iter.remove(); } } } return count; } /** * Copies suggestions from a SuggestionCursor to the list of promoted suggestions. * * @param cursor from which to copy the suggestions * @param count maximum number of suggestions to copy * @param promoted the list to which to add the suggestions * @return the number of suggestions actually copied. */ private int promote(SuggestionCursor cursor, int count, ListSuggestionCursor promoted) { if (count < 1 || cursor.getPosition() >= cursor.getCount()) { return 0; } int addedCount = 0; do { if (accept(cursor)) { if (promoted.add(new SuggestionPosition(cursor))) { // Added successfully (wasn't already promoted). addedCount++; } } } while (cursor.moveToNext() && addedCount < count); return addedCount; } }