SuggestionsProviderImpl.java revision 848fa7a19abedc372452073abaf52780c7b6d78d
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 BlendingSuggestionsProvider 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 BlendingSuggestionsProvider(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 SuggestionCursor 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 BlendedSuggestions 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 BlendedSuggestions suggestions = new BlendedSuggestions(promoter,
160                maxSuggestions,
161                query,
162                corporaToQuery);
163        SuggestionCursor 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 BlendedSuggestions 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                BlendedSuggestions 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            if (DBG) {
229                Log.d(TAG, "SuggestionCursorReceiver.consume(" + cursor + ") corpus=" +
230                        cursor.getCorpus() + " count = " + cursor.getCount());
231            }
232            updateShouldQueryStrategy(cursor);
233            mPendingResults.add(cursor);
234            if (mResultPublishDelayMillis > 0
235                    && !mSuggestions.isClosed()
236                    && mSuggestions.getResultCount() + mPendingResults.size()
237                            < mCountAtWhichToExecuteNextBatch) {
238                // This is not the last result of the batch, delay publishing
239                if (DBG) Log.d(TAG, "Delaying result by " + mResultPublishDelayMillis + " ms");
240                mPublishThread.removeCallbacks(mResultPublishTask);
241                mPublishThread.postDelayed(mResultPublishTask, mResultPublishDelayMillis);
242            } else {
243                // This is the last result, publish immediately
244                if (DBG) Log.d(TAG, "Publishing result immediately");
245                mPublishThread.removeCallbacks(mResultPublishTask);
246                publishPendingResults();
247            }
248            if (!mSuggestions.isClosed()) {
249                executeNextBatchIfNeeded();
250            }
251            if (cursor != null && mLogger != null) {
252                mLogger.logLatency(cursor);
253            }
254            return true;
255        }
256
257        private void publishPendingResults() {
258            mSuggestions.addCorpusResults(mPendingResults);
259            mPendingResults.clear();
260        }
261
262        private void executeNextBatchIfNeeded() {
263            if (mSuggestions.getResultCount() == mCountAtWhichToExecuteNextBatch) {
264                // We've just finished one batch
265                if (mSuggestions.getPromoted().getCount() < mConfig.getMaxPromotedSuggestions()) {
266                    // But we still don't have enough results, ask for more
267                    int nextBatchSize = mConfig.getNumPromotedSources();
268                    mCountAtWhichToExecuteNextBatch += nextBatchSize;
269                    mExecutor.executeNextBatch(nextBatchSize);
270                }
271            }
272        }
273    }
274
275}
276