1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.quicksearchbox; 18 19import com.android.quicksearchbox.util.BatchingNamedTaskExecutor; 20import com.android.quicksearchbox.util.Consumer; 21import com.android.quicksearchbox.util.NamedTaskExecutor; 22 23import android.os.Handler; 24import android.util.Log; 25 26import java.util.ArrayList; 27import java.util.Collection; 28import java.util.Collections; 29import java.util.List; 30 31/** 32 * Suggestions provider implementation. 33 * 34 * The provider will only handle a single query at a time. If a new query comes 35 * in, the old one is cancelled. 36 */ 37public class SuggestionsProviderImpl implements SuggestionsProvider { 38 39 private static final boolean DBG = false; 40 private static final String TAG = "QSB.SuggestionsProviderImpl"; 41 42 private final Config mConfig; 43 44 private final NamedTaskExecutor mQueryExecutor; 45 46 private final Handler mPublishThread; 47 48 private Promoter mAllPromoter; 49 50 private Promoter mSingleCorpusPromoter; 51 52 private final ShortcutRepository mShortcutRepo; 53 54 private final ShouldQueryStrategy mShouldQueryStrategy = new ShouldQueryStrategy(); 55 56 private final Corpora mCorpora; 57 58 private final CorpusRanker mCorpusRanker; 59 60 private final Logger mLogger; 61 62 private BatchingNamedTaskExecutor mBatchingExecutor; 63 64 public SuggestionsProviderImpl(Config config, 65 NamedTaskExecutor queryExecutor, 66 Handler publishThread, 67 ShortcutRepository shortcutRepo, 68 Corpora corpora, 69 CorpusRanker corpusRanker, 70 Logger logger) { 71 mConfig = config; 72 mQueryExecutor = queryExecutor; 73 mPublishThread = publishThread; 74 mShortcutRepo = shortcutRepo; 75 mCorpora = corpora; 76 mCorpusRanker = corpusRanker; 77 mLogger = logger; 78 } 79 80 /** 81 * Sets the promoter used in All mode. 82 */ 83 public void setAllPromoter(Promoter promoter) { 84 mAllPromoter = promoter; 85 } 86 87 /** 88 * Sets the promoter used in single corpus mode. 89 */ 90 public void setSingleCorpusPromoter(Promoter promoter) { 91 mSingleCorpusPromoter = promoter; 92 } 93 94 public void close() { 95 cancelPendingTasks(); 96 } 97 98 /** 99 * Cancels all pending query tasks. 100 */ 101 private void cancelPendingTasks() { 102 if (mBatchingExecutor != null) { 103 mBatchingExecutor.cancelPendingTasks(); 104 mBatchingExecutor = null; 105 } 106 } 107 108 protected ShortcutCursor getShortcutsForQuery(String query, Corpus singleCorpus) { 109 if (mShortcutRepo == null) return null; 110 Collection<Corpus> allowedCorpora; 111 if (singleCorpus == null) { 112 allowedCorpora = mCorpora.getEnabledCorpora(); 113 } else { 114 allowedCorpora = Collections.singletonList(singleCorpus); 115 } 116 return mShortcutRepo.getShortcutsForQuery(query, allowedCorpora); 117 } 118 119 /** 120 * Gets the sources that should be queried for the given query. 121 */ 122 private List<Corpus> getCorporaToQuery(String query, Corpus singleCorpus) { 123 if (singleCorpus != null) return Collections.singletonList(singleCorpus); 124 List<Corpus> orderedCorpora = mCorpusRanker.getRankedCorpora(); 125 if (DBG) Log.d(TAG, "getCorporaToQuery query='"+query+"' orderedCorpora="+orderedCorpora); 126 ArrayList<Corpus> corporaToQuery = new ArrayList<Corpus>(orderedCorpora.size()); 127 for (Corpus corpus : orderedCorpora) { 128 if (shouldQueryCorpus(corpus, query)) { 129 if (DBG) Log.d(TAG, "should query corpus " + corpus); 130 corporaToQuery.add(corpus); 131 } else { 132 if (DBG) Log.d(TAG, "should NOT query corpus " + corpus); 133 } 134 } 135 if (DBG) Log.d(TAG, "getCorporaToQuery corporaToQuery=" + corporaToQuery); 136 return corporaToQuery; 137 } 138 139 protected boolean shouldQueryCorpus(Corpus corpus, String query) { 140 if (query.length() == 0 && !corpus.isWebCorpus()) { 141 // Only the web corpus sees zero length queries. 142 return false; 143 } 144 return mShouldQueryStrategy.shouldQueryCorpus(corpus, query); 145 } 146 147 private void updateShouldQueryStrategy(CorpusResult cursor) { 148 if (cursor.getCount() == 0) { 149 mShouldQueryStrategy.onZeroResults(cursor.getCorpus(), 150 cursor.getUserQuery()); 151 } 152 } 153 154 public Suggestions getSuggestions(String query, Corpus singleCorpus, int maxSuggestions) { 155 if (DBG) Log.d(TAG, "getSuggestions(" + query + ")"); 156 cancelPendingTasks(); 157 List<Corpus> corporaToQuery = getCorporaToQuery(query, singleCorpus); 158 Promoter promoter = singleCorpus == null ? mAllPromoter : mSingleCorpusPromoter; 159 final Suggestions suggestions = new Suggestions(promoter, 160 maxSuggestions, 161 query, 162 corporaToQuery); 163 ShortcutCursor shortcuts = getShortcutsForQuery(query, singleCorpus); 164 if (shortcuts != null) { 165 suggestions.setShortcuts(shortcuts); 166 } 167 168 // Fast path for the zero sources case 169 if (corporaToQuery.size() == 0) { 170 return suggestions; 171 } 172 173 int initialBatchSize = countDefaultCorpora(corporaToQuery); 174 if (initialBatchSize == 0) { 175 initialBatchSize = mConfig.getNumPromotedSources(); 176 } 177 178 mBatchingExecutor = new BatchingNamedTaskExecutor(mQueryExecutor); 179 180 long publishResultDelayMillis = mConfig.getPublishResultDelayMillis(); 181 SuggestionCursorReceiver receiver = new SuggestionCursorReceiver( 182 mBatchingExecutor, suggestions, initialBatchSize, 183 publishResultDelayMillis); 184 185 int maxResultsPerSource = mConfig.getMaxResultsPerSource(); 186 QueryTask.startQueries(query, maxResultsPerSource, corporaToQuery, mBatchingExecutor, 187 mPublishThread, receiver, singleCorpus != null); 188 mBatchingExecutor.executeNextBatch(initialBatchSize); 189 190 return suggestions; 191 } 192 193 private int countDefaultCorpora(List<Corpus> corpora) { 194 int count = 0; 195 for (Corpus corpus : corpora) { 196 if (corpus.isCorpusDefaultEnabled()) { 197 count++; 198 } 199 } 200 return count; 201 } 202 203 private class SuggestionCursorReceiver implements Consumer<CorpusResult> { 204 private final BatchingNamedTaskExecutor mExecutor; 205 private final Suggestions mSuggestions; 206 private final long mResultPublishDelayMillis; 207 private final ArrayList<CorpusResult> mPendingResults; 208 private final Runnable mResultPublishTask = new Runnable () { 209 public void run() { 210 if (DBG) Log.d(TAG, "Publishing delayed results"); 211 publishPendingResults(); 212 } 213 }; 214 215 private int mCountAtWhichToExecuteNextBatch; 216 217 public SuggestionCursorReceiver(BatchingNamedTaskExecutor executor, 218 Suggestions suggestions, int initialBatchSize, 219 long publishResultDelayMillis) { 220 mExecutor = executor; 221 mSuggestions = suggestions; 222 mCountAtWhichToExecuteNextBatch = initialBatchSize; 223 mResultPublishDelayMillis = publishResultDelayMillis; 224 mPendingResults = new ArrayList<CorpusResult>(); 225 } 226 227 public boolean consume(CorpusResult cursor) { 228 updateShouldQueryStrategy(cursor); 229 mPendingResults.add(cursor); 230 if (mResultPublishDelayMillis > 0 231 && !mSuggestions.isClosed() 232 && mSuggestions.getResultCount() + mPendingResults.size() 233 < mCountAtWhichToExecuteNextBatch) { 234 // This is not the last result of the batch, delay publishing 235 if (DBG) Log.d(TAG, "Delaying result by " + mResultPublishDelayMillis + " ms"); 236 mPublishThread.removeCallbacks(mResultPublishTask); 237 mPublishThread.postDelayed(mResultPublishTask, mResultPublishDelayMillis); 238 } else { 239 // This is the last result, publish immediately 240 if (DBG) Log.d(TAG, "Publishing result immediately"); 241 mPublishThread.removeCallbacks(mResultPublishTask); 242 publishPendingResults(); 243 } 244 if (!mSuggestions.isClosed()) { 245 executeNextBatchIfNeeded(); 246 } 247 if (cursor != null && mLogger != null) { 248 mLogger.logLatency(cursor); 249 } 250 return true; 251 } 252 253 private void publishPendingResults() { 254 mSuggestions.addCorpusResults(mPendingResults); 255 mPendingResults.clear(); 256 } 257 258 private void executeNextBatchIfNeeded() { 259 if (mSuggestions.getResultCount() == mCountAtWhichToExecuteNextBatch) { 260 // We've just finished one batch 261 if (mSuggestions.getPromoted().getCount() < mConfig.getMaxPromotedSuggestions()) { 262 // But we still don't have enough results, ask for more 263 int nextBatchSize = mConfig.getNumPromotedSources(); 264 mCountAtWhichToExecuteNextBatch += nextBatchSize; 265 mExecutor.executeNextBatch(nextBatchSize); 266 } 267 } 268 } 269 } 270 271} 272