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            suggestions.done();
136        }
137
138        int maxResultsPerSource = mConfig.getMaxResultsPerSource();
139        QueryTask.startQueries(query, maxResultsPerSource, corporaToQuery, mBatchingExecutor,
140                mPublishThread, receiver, corporaToQuery.size() == 1);
141        mBatchingExecutor.executeNextBatch(initialBatchSize);
142
143        return suggestions;
144    }
145
146    private int countDefaultCorpora(List<Corpus> corpora) {
147        int count = 0;
148        for (Corpus corpus : corpora) {
149            if (corpus.isCorpusDefaultEnabled()) {
150                count++;
151            }
152        }
153        return count;
154    }
155
156    private boolean shouldDisplayResults(String query) {
157        if (query.length() == 0 && !mConfig.showSuggestionsForZeroQuery()) {
158            // Note that even though we don't display such results, it's
159            // useful to run the query itself because it warms up the network
160            // connection.
161            return false;
162        }
163        return true;
164    }
165
166
167    private class SuggestionCursorReceiver implements Consumer<CorpusResult> {
168        private final BatchingNamedTaskExecutor mExecutor;
169        private final Suggestions mSuggestions;
170        private final long mResultPublishDelayMillis;
171        private final ArrayList<CorpusResult> mPendingResults;
172        private final Runnable mResultPublishTask = new Runnable () {
173            public void run() {
174                if (DBG) Log.d(TAG, "Publishing delayed results");
175                publishPendingResults();
176            }
177        };
178
179        private int mCountAtWhichToExecuteNextBatch;
180
181        public SuggestionCursorReceiver(BatchingNamedTaskExecutor executor,
182                Suggestions suggestions, int initialBatchSize,
183                long publishResultDelayMillis) {
184            mExecutor = executor;
185            mSuggestions = suggestions;
186            mCountAtWhichToExecuteNextBatch = initialBatchSize;
187            mResultPublishDelayMillis = publishResultDelayMillis;
188            mPendingResults = new ArrayList<CorpusResult>();
189        }
190
191        public boolean consume(CorpusResult cursor) {
192            if (DBG) {
193                Log.d(TAG, "SuggestionCursorReceiver.consume(" + cursor + ") corpus=" +
194                        cursor.getCorpus() + " count = " + cursor.getCount());
195            }
196            updateShouldQueryStrategy(cursor);
197            mPendingResults.add(cursor);
198            if (mResultPublishDelayMillis > 0
199                    && !mSuggestions.isClosed()
200                    && mSuggestions.getResultCount() + mPendingResults.size()
201                            < mCountAtWhichToExecuteNextBatch) {
202                // This is not the last result of the batch, delay publishing
203                if (DBG) Log.d(TAG, "Delaying result by " + mResultPublishDelayMillis + " ms");
204                mPublishThread.removeCallbacks(mResultPublishTask);
205                mPublishThread.postDelayed(mResultPublishTask, mResultPublishDelayMillis);
206            } else {
207                // This is the last result, publish immediately
208                if (DBG) Log.d(TAG, "Publishing result immediately");
209                mPublishThread.removeCallbacks(mResultPublishTask);
210                publishPendingResults();
211            }
212            if (!mSuggestions.isClosed()) {
213                executeNextBatchIfNeeded();
214            }
215            if (cursor != null && mLogger != null) {
216                mLogger.logLatency(cursor);
217            }
218            return true;
219        }
220
221        private void publishPendingResults() {
222            mSuggestions.addCorpusResults(mPendingResults);
223            mPendingResults.clear();
224        }
225
226        private void executeNextBatchIfNeeded() {
227            if (mSuggestions.getResultCount() == mCountAtWhichToExecuteNextBatch) {
228                // We've just finished one batch, ask for more
229                int nextBatchSize = mConfig.getNumPromotedSources();
230                mCountAtWhichToExecuteNextBatch += nextBatchSize;
231                mExecutor.executeNextBatch(nextBatchSize);
232            }
233        }
234    }
235}
236