| /* |
| * Copyright (C) 2009 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.android.quicksearchbox.util.BatchingNamedTaskExecutor; |
| import com.android.quicksearchbox.util.Consumer; |
| import com.android.quicksearchbox.util.NamedTaskExecutor; |
| import com.android.quicksearchbox.util.NoOpConsumer; |
| |
| import android.os.Handler; |
| import android.util.Log; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Suggestions provider implementation. |
| * |
| * The provider will only handle a single query at a time. If a new query comes |
| * in, the old one is cancelled. |
| */ |
| public class SuggestionsProviderImpl implements SuggestionsProvider { |
| |
| private static final boolean DBG = false; |
| private static final String TAG = "QSB.SuggestionsProviderImpl"; |
| |
| private final Config mConfig; |
| |
| private final NamedTaskExecutor mQueryExecutor; |
| |
| private final Handler mPublishThread; |
| |
| private final ShouldQueryStrategy mShouldQueryStrategy; |
| |
| private final Logger mLogger; |
| |
| private BatchingNamedTaskExecutor mBatchingExecutor; |
| |
| public SuggestionsProviderImpl(Config config, |
| NamedTaskExecutor queryExecutor, |
| Handler publishThread, |
| Logger logger) { |
| mConfig = config; |
| mQueryExecutor = queryExecutor; |
| mPublishThread = publishThread; |
| mLogger = logger; |
| mShouldQueryStrategy = new ShouldQueryStrategy(mConfig); |
| } |
| |
| public void close() { |
| cancelPendingTasks(); |
| } |
| |
| /** |
| * Cancels all pending query tasks. |
| */ |
| private void cancelPendingTasks() { |
| if (mBatchingExecutor != null) { |
| mBatchingExecutor.cancelPendingTasks(); |
| mBatchingExecutor = null; |
| } |
| } |
| |
| /** |
| * Gets the sources that should be queried for the given query. |
| */ |
| private List<Corpus> filterCorpora(String query, List<Corpus> corpora) { |
| // If there is only one corpus, always query it |
| if (corpora.size() <= 1) return corpora; |
| ArrayList<Corpus> corporaToQuery = new ArrayList<Corpus>(corpora.size()); |
| for (Corpus corpus : corpora) { |
| if (shouldQueryCorpus(corpus, query)) { |
| if (DBG) Log.d(TAG, "should query corpus " + corpus); |
| corporaToQuery.add(corpus); |
| } else { |
| if (DBG) Log.d(TAG, "should NOT query corpus " + corpus); |
| } |
| } |
| if (DBG) Log.d(TAG, "getCorporaToQuery corporaToQuery=" + corporaToQuery); |
| return corporaToQuery; |
| } |
| |
| protected boolean shouldQueryCorpus(Corpus corpus, String query) { |
| return mShouldQueryStrategy.shouldQueryCorpus(corpus, query); |
| } |
| |
| private void updateShouldQueryStrategy(CorpusResult cursor) { |
| if (cursor.getCount() == 0) { |
| mShouldQueryStrategy.onZeroResults(cursor.getCorpus(), |
| cursor.getUserQuery()); |
| } |
| } |
| |
| public Suggestions getSuggestions(String query, List<Corpus> corporaToQuery) { |
| if (DBG) Log.d(TAG, "getSuggestions(" + query + ")"); |
| corporaToQuery = filterCorpora(query, corporaToQuery); |
| final Suggestions suggestions = new Suggestions(query, corporaToQuery); |
| Log.i(TAG, "chars:" + query.length() + ",corpora:" + corporaToQuery); |
| |
| // Fast path for the zero sources case |
| if (corporaToQuery.size() == 0) { |
| return suggestions; |
| } |
| |
| int initialBatchSize = countDefaultCorpora(corporaToQuery); |
| if (initialBatchSize == 0) { |
| initialBatchSize = mConfig.getNumPromotedSources(); |
| } |
| |
| mBatchingExecutor = new BatchingNamedTaskExecutor(mQueryExecutor); |
| |
| long publishResultDelayMillis = mConfig.getPublishResultDelayMillis(); |
| |
| Consumer<CorpusResult> receiver; |
| if (shouldDisplayResults(query)) { |
| receiver = new SuggestionCursorReceiver( |
| mBatchingExecutor, suggestions, initialBatchSize, |
| publishResultDelayMillis); |
| } else { |
| receiver = new NoOpConsumer<CorpusResult>(); |
| suggestions.done(); |
| } |
| |
| int maxResultsPerSource = mConfig.getMaxResultsPerSource(); |
| QueryTask.startQueries(query, maxResultsPerSource, corporaToQuery, mBatchingExecutor, |
| mPublishThread, receiver, corporaToQuery.size() == 1); |
| mBatchingExecutor.executeNextBatch(initialBatchSize); |
| |
| return suggestions; |
| } |
| |
| private int countDefaultCorpora(List<Corpus> corpora) { |
| int count = 0; |
| for (Corpus corpus : corpora) { |
| if (corpus.isCorpusDefaultEnabled()) { |
| count++; |
| } |
| } |
| return count; |
| } |
| |
| private boolean shouldDisplayResults(String query) { |
| if (query.length() == 0 && !mConfig.showSuggestionsForZeroQuery()) { |
| // Note that even though we don't display such results, it's |
| // useful to run the query itself because it warms up the network |
| // connection. |
| return false; |
| } |
| return true; |
| } |
| |
| |
| private class SuggestionCursorReceiver implements Consumer<CorpusResult> { |
| private final BatchingNamedTaskExecutor mExecutor; |
| private final Suggestions mSuggestions; |
| private final long mResultPublishDelayMillis; |
| private final ArrayList<CorpusResult> mPendingResults; |
| private final Runnable mResultPublishTask = new Runnable () { |
| public void run() { |
| if (DBG) Log.d(TAG, "Publishing delayed results"); |
| publishPendingResults(); |
| } |
| }; |
| |
| private int mCountAtWhichToExecuteNextBatch; |
| |
| public SuggestionCursorReceiver(BatchingNamedTaskExecutor executor, |
| Suggestions suggestions, int initialBatchSize, |
| long publishResultDelayMillis) { |
| mExecutor = executor; |
| mSuggestions = suggestions; |
| mCountAtWhichToExecuteNextBatch = initialBatchSize; |
| mResultPublishDelayMillis = publishResultDelayMillis; |
| mPendingResults = new ArrayList<CorpusResult>(); |
| } |
| |
| public boolean consume(CorpusResult cursor) { |
| if (DBG) { |
| Log.d(TAG, "SuggestionCursorReceiver.consume(" + cursor + ") corpus=" + |
| cursor.getCorpus() + " count = " + cursor.getCount()); |
| } |
| updateShouldQueryStrategy(cursor); |
| mPendingResults.add(cursor); |
| if (mResultPublishDelayMillis > 0 |
| && !mSuggestions.isClosed() |
| && mSuggestions.getResultCount() + mPendingResults.size() |
| < mCountAtWhichToExecuteNextBatch) { |
| // This is not the last result of the batch, delay publishing |
| if (DBG) Log.d(TAG, "Delaying result by " + mResultPublishDelayMillis + " ms"); |
| mPublishThread.removeCallbacks(mResultPublishTask); |
| mPublishThread.postDelayed(mResultPublishTask, mResultPublishDelayMillis); |
| } else { |
| // This is the last result, publish immediately |
| if (DBG) Log.d(TAG, "Publishing result immediately"); |
| mPublishThread.removeCallbacks(mResultPublishTask); |
| publishPendingResults(); |
| } |
| if (!mSuggestions.isClosed()) { |
| executeNextBatchIfNeeded(); |
| } |
| if (cursor != null && mLogger != null) { |
| mLogger.logLatency(cursor); |
| } |
| return true; |
| } |
| |
| private void publishPendingResults() { |
| mSuggestions.addCorpusResults(mPendingResults); |
| mPendingResults.clear(); |
| } |
| |
| private void executeNextBatchIfNeeded() { |
| if (mSuggestions.getResultCount() == mCountAtWhichToExecuteNextBatch) { |
| // We've just finished one batch, ask for more |
| int nextBatchSize = mConfig.getNumPromotedSources(); |
| mCountAtWhichToExecuteNextBatch += nextBatchSize; |
| mExecutor.executeNextBatch(nextBatchSize); |
| } |
| } |
| } |
| } |