SuggestionsProviderImpl.java revision 233f899a01d805b27f17ce4f72fdbc7c279c6c36
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;
22import com.android.quicksearchbox.util.NoOpConsumer;
23
24import android.os.Handler;
25import android.util.Log;
26
27import java.util.ArrayList;
28import java.util.List;
29
30/**
31 * Suggestions provider implementation.
32 *
33 * The provider will only handle a single query at a time. If a new query comes
34 * in, the old one is cancelled.
35 */
36public class SuggestionsProviderImpl implements SuggestionsProvider {
37
38    private static final boolean DBG = false;
39    private static final String TAG = "QSB.SuggestionsProviderImpl";
40
41    private final Config mConfig;
42
43    private final NamedTaskExecutor mQueryExecutor;
44
45    private final Handler mPublishThread;
46
47    private final ShouldQueryStrategy mShouldQueryStrategy;
48
49    private final Logger mLogger;
50
51    private BatchingNamedTaskExecutor mBatchingExecutor;
52
53    public SuggestionsProviderImpl(Config config,
54            NamedTaskExecutor queryExecutor,
55            Handler publishThread,
56            Logger logger) {
57        mConfig = config;
58        mQueryExecutor = queryExecutor;
59        mPublishThread = publishThread;
60        mLogger = logger;
61        mShouldQueryStrategy = new ShouldQueryStrategy(mConfig);
62    }
63
64    public void close() {
65        cancelPendingTasks();
66    }
67
68    /**
69     * Cancels all pending query tasks.
70     */
71    private void cancelPendingTasks() {
72        if (mBatchingExecutor != null) {
73            mBatchingExecutor.cancelPendingTasks();
74            mBatchingExecutor = null;
75        }
76    }
77
78    /**
79     * Gets the sources that should be queried for the given query.
80     */
81    private List<Corpus> filterCorpora(String query, List<Corpus> corpora) {
82        // If there is only one corpus, always query it
83        if (corpora.size() <= 1) return corpora;
84        ArrayList<Corpus> corporaToQuery = new ArrayList<Corpus>(corpora.size());
85        for (Corpus corpus : corpora) {
86            if (shouldQueryCorpus(corpus, query)) {
87                if (DBG) Log.d(TAG, "should query corpus " + corpus);
88                corporaToQuery.add(corpus);
89            } else {
90                if (DBG) Log.d(TAG, "should NOT query corpus " + corpus);
91            }
92        }
93        if (DBG) Log.d(TAG, "getCorporaToQuery corporaToQuery=" + corporaToQuery);
94        return corporaToQuery;
95    }
96
97    protected boolean shouldQueryCorpus(Corpus corpus, String query) {
98        return mShouldQueryStrategy.shouldQueryCorpus(corpus, query);
99    }
100
101    private void updateShouldQueryStrategy(CorpusResult cursor) {
102        if (cursor.getCount() == 0) {
103            mShouldQueryStrategy.onZeroResults(cursor.getCorpus(),
104                    cursor.getUserQuery());
105        }
106    }
107
108    public Suggestions getSuggestions(String query, List<Corpus> corporaToQuery) {
109        if (DBG) Log.d(TAG, "getSuggestions(" + query + ")");
110        corporaToQuery = filterCorpora(query, corporaToQuery);
111        final Suggestions suggestions = new Suggestions(query, corporaToQuery);
112        Log.i(TAG, "chars:" + query.length() + ",corpora:" + corporaToQuery);
113
114        // Fast path for the zero sources case
115        if (corporaToQuery.size() == 0) {
116            return suggestions;
117        }
118
119        int initialBatchSize = countDefaultCorpora(corporaToQuery);
120        if (initialBatchSize == 0) {
121            initialBatchSize = mConfig.getNumPromotedSources();
122        }
123
124        mBatchingExecutor = new BatchingNamedTaskExecutor(mQueryExecutor);
125
126        long publishResultDelayMillis = mConfig.getPublishResultDelayMillis();
127
128        Consumer<CorpusResult> receiver;
129        if (shouldDisplayResults(query)) {
130            receiver = new SuggestionCursorReceiver(
131                    mBatchingExecutor, suggestions, initialBatchSize,
132                    publishResultDelayMillis);
133        } else {
134            receiver = new NoOpConsumer<CorpusResult>();
135        }
136
137        int maxResultsPerSource = mConfig.getMaxResultsPerSource();
138        QueryTask.startQueries(query, maxResultsPerSource, corporaToQuery, mBatchingExecutor,
139                mPublishThread, receiver, corporaToQuery.size() == 1);
140        mBatchingExecutor.executeNextBatch(initialBatchSize);
141
142        return suggestions;
143    }
144
145    private int countDefaultCorpora(List<Corpus> corpora) {
146        int count = 0;
147        for (Corpus corpus : corpora) {
148            if (corpus.isCorpusDefaultEnabled()) {
149                count++;
150            }
151        }
152        return count;
153    }
154
155    private boolean shouldDisplayResults(String query) {
156        if (query.length() == 0 && !mConfig.showSuggestionsForZeroQuery()) {
157            // Note that even though we don't display such results, it's
158            // useful to run the query itself because it warms up the network
159            // connection.
160            return false;
161        }
162        return true;
163    }
164
165
166    private class SuggestionCursorReceiver implements Consumer<CorpusResult> {
167        private final BatchingNamedTaskExecutor mExecutor;
168        private final Suggestions mSuggestions;
169        private final long mResultPublishDelayMillis;
170        private final ArrayList<CorpusResult> mPendingResults;
171        private final Runnable mResultPublishTask = new Runnable () {
172            public void run() {
173                if (DBG) Log.d(TAG, "Publishing delayed results");
174                publishPendingResults();
175            }
176        };
177
178        private int mCountAtWhichToExecuteNextBatch;
179
180        public SuggestionCursorReceiver(BatchingNamedTaskExecutor executor,
181                Suggestions suggestions, int initialBatchSize,
182                long publishResultDelayMillis) {
183            mExecutor = executor;
184            mSuggestions = suggestions;
185            mCountAtWhichToExecuteNextBatch = initialBatchSize;
186            mResultPublishDelayMillis = publishResultDelayMillis;
187            mPendingResults = new ArrayList<CorpusResult>();
188        }
189
190        public boolean consume(CorpusResult cursor) {
191            if (DBG) {
192                Log.d(TAG, "SuggestionCursorReceiver.consume(" + cursor + ") corpus=" +
193                        cursor.getCorpus() + " count = " + cursor.getCount());
194            }
195            updateShouldQueryStrategy(cursor);
196            mPendingResults.add(cursor);
197            if (mResultPublishDelayMillis > 0
198                    && !mSuggestions.isClosed()
199                    && mSuggestions.getResultCount() + mPendingResults.size()
200                            < mCountAtWhichToExecuteNextBatch) {
201                // This is not the last result of the batch, delay publishing
202                if (DBG) Log.d(TAG, "Delaying result by " + mResultPublishDelayMillis + " ms");
203                mPublishThread.removeCallbacks(mResultPublishTask);
204                mPublishThread.postDelayed(mResultPublishTask, mResultPublishDelayMillis);
205            } else {
206                // This is the last result, publish immediately
207                if (DBG) Log.d(TAG, "Publishing result immediately");
208                mPublishThread.removeCallbacks(mResultPublishTask);
209                publishPendingResults();
210            }
211            if (!mSuggestions.isClosed()) {
212                executeNextBatchIfNeeded();
213            }
214            if (cursor != null && mLogger != null) {
215                mLogger.logLatency(cursor);
216            }
217            return true;
218        }
219
220        private void publishPendingResults() {
221            mSuggestions.addCorpusResults(mPendingResults);
222            mPendingResults.clear();
223        }
224
225        private void executeNextBatchIfNeeded() {
226            if (mSuggestions.getResultCount() == mCountAtWhichToExecuteNextBatch) {
227                // We've just finished one batch, ask for more
228                int nextBatchSize = mConfig.getNumPromotedSources();
229                mCountAtWhichToExecuteNextBatch += nextBatchSize;
230                mExecutor.executeNextBatch(nextBatchSize);
231            }
232        }
233    }
234}
235