Suggestions.java revision 3540e5a9eab6ff33e6d45f1d908bcaf4c4f49152
1/*
2 * Copyright (C) 2010 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.google.common.annotations.VisibleForTesting;
20
21import android.database.DataSetObservable;
22import android.database.DataSetObserver;
23import android.util.Log;
24
25import java.util.ArrayList;
26import java.util.HashSet;
27import java.util.List;
28import java.util.Set;
29
30/**
31 * Collects all corpus results for a single query.
32 */
33public class Suggestions {
34    private static final boolean DBG = false;
35    private static final String TAG = "QSB.Suggestions";
36
37    /** True if {@link Suggestions#close} has been called. */
38    private boolean mClosed = false;
39    protected final String mQuery;
40
41    private ShortcutCursor mShortcuts;
42
43    private final MyShortcutsObserver mShortcutsObserver = new MyShortcutsObserver();
44
45    /**
46     * The observers that want notifications of changes to the published suggestions.
47     * This object may be accessed on any thread.
48     */
49    private final DataSetObservable mDataSetObservable = new DataSetObservable();
50
51    /** The sources that are expected to report. */
52    private final List<Corpus> mExpectedCorpora;
53
54    /**
55     * All {@link SuggestionCursor} objects that have been published so far,
56     * in the order that they were published.
57     * This object may only be accessed on the UI thread.
58     * */
59    private final ArrayList<CorpusResult> mCorpusResults;
60
61    private CorpusResult mWebResult;
62
63    private int mRefCount = 0;
64
65    private boolean mDone = false;
66
67    public Suggestions(String query, List<Corpus> expectedCorpora) {
68        mQuery = query;
69        mExpectedCorpora = expectedCorpora;
70        mCorpusResults = new ArrayList<CorpusResult>(mExpectedCorpora.size());
71        if (DBG) {
72            Log.d(TAG, "new Suggestions [" + hashCode() + "] query \"" + query
73                    + "\" expected corpora: " + mExpectedCorpora);
74        }
75    }
76
77    public void acquire() {
78        mRefCount++;
79    }
80
81    public void release() {
82        mRefCount--;
83        if (mRefCount <= 0) {
84            close();
85        }
86    }
87
88    public List<Corpus> getExpectedCorpora() {
89        return mExpectedCorpora;
90    }
91
92    /**
93     * Gets the number of corpora that are expected to report.
94     */
95    @VisibleForTesting
96    public int getExpectedResultCount() {
97        return mExpectedCorpora.size();
98    }
99
100    public boolean expectsCorpus(Corpus corpus) {
101        for (Corpus expectedCorpus : mExpectedCorpora) {
102            if (expectedCorpus.equals(corpus)) return true;
103        }
104        return false;
105    }
106
107    /**
108     * Gets the set of corpora that have reported results to this suggestions set.
109     *
110     * @return A collection of corpora.
111     */
112    public Set<Corpus> getIncludedCorpora() {
113        HashSet<Corpus> corpora = new HashSet<Corpus>();
114        for (CorpusResult result : mCorpusResults) {
115            corpora.add(result.getCorpus());
116        }
117        return corpora;
118    }
119
120    /**
121     * Sets the shortcut suggestions.
122     * Must be called on the UI thread, or before this object is seen by the UI thread.
123     *
124     * @param shortcuts The shortcuts.
125     */
126    public void setShortcuts(ShortcutCursor shortcuts) {
127        if (DBG) Log.d(TAG, "setShortcuts(" + shortcuts + ")");
128        if (mShortcuts != null) {
129            throw new IllegalStateException("Got duplicate shortcuts: old: " + mShortcuts
130                    + ", new: " + shortcuts);
131        }
132        if (shortcuts == null) return;
133        if (isClosed()) {
134            shortcuts.close();
135            return;
136        }
137        if (!mQuery.equals(shortcuts.getUserQuery())) {
138            throw new IllegalArgumentException("Got shortcuts for wrong query: "
139                    + mQuery + " != " + shortcuts.getUserQuery());
140        }
141        mShortcuts = shortcuts;
142        if (shortcuts != null) {
143            mShortcuts.registerDataSetObserver(mShortcutsObserver);
144        }
145        notifyDataSetChanged();
146    }
147
148    /**
149     * Marks the suggestions set as complete, regardless of whether all corpora have
150     * returned.
151     */
152    public void done() {
153        mDone = true;
154    }
155
156    /**
157     * Checks whether all sources have reported.
158     * Must be called on the UI thread, or before this object is seen by the UI thread.
159     */
160    public boolean isDone() {
161        // TODO: Handle early completion because we have all the results we want.
162        return mDone || mCorpusResults.size() >= mExpectedCorpora.size();
163    }
164
165    /**
166     * Adds a list of corpus results. Must be called on the UI thread, or before this
167     * object is seen by the UI thread.
168     */
169    public void addCorpusResults(List<CorpusResult> corpusResults) {
170        if (isClosed()) {
171            for (CorpusResult corpusResult : corpusResults) {
172                corpusResult.close();
173            }
174            return;
175        }
176
177        for (CorpusResult corpusResult : corpusResults) {
178            if (DBG) {
179                Log.d(TAG, "addCorpusResult["+ hashCode() + "] corpus:" +
180                        corpusResult.getCorpus().getName() + " results:" + corpusResult.getCount());
181            }
182            if (!mQuery.equals(corpusResult.getUserQuery())) {
183              throw new IllegalArgumentException("Got result for wrong query: "
184                    + mQuery + " != " + corpusResult.getUserQuery());
185            }
186            mCorpusResults.add(corpusResult);
187            if (corpusResult.getCorpus().isWebCorpus()) {
188                mWebResult = corpusResult;
189            }
190        }
191        notifyDataSetChanged();
192    }
193
194    /**
195     * Registers an observer that will be notified when the reported results or
196     * the done status changes.
197     */
198    public void registerDataSetObserver(DataSetObserver observer) {
199        if (mClosed) {
200            throw new IllegalStateException("registerDataSetObserver() when closed");
201        }
202        mDataSetObservable.registerObserver(observer);
203    }
204
205
206    /**
207     * Unregisters an observer.
208     */
209    public void unregisterDataSetObserver(DataSetObserver observer) {
210        mDataSetObservable.unregisterObserver(observer);
211    }
212
213    /**
214     * Calls {@link DataSetObserver#onChanged()} on all observers.
215     */
216    protected void notifyDataSetChanged() {
217        if (DBG) Log.d(TAG, "notifyDataSetChanged()");
218        mDataSetObservable.notifyChanged();
219    }
220
221    /**
222     * Closes all the source results and unregisters all observers.
223     */
224    private void close() {
225        if (DBG) Log.d(TAG, "close() [" + hashCode() + "]");
226        if (mClosed) {
227            throw new IllegalStateException("Double close()");
228        }
229        mClosed = true;
230        mDataSetObservable.unregisterAll();
231        if (mShortcuts != null) {
232            mShortcuts.close();
233            mShortcuts = null;
234        }
235
236        for (CorpusResult result : mCorpusResults) {
237            result.close();
238        }
239        mCorpusResults.clear();
240    }
241
242    public boolean isClosed() {
243        return mClosed;
244    }
245
246    public ShortcutCursor getShortcuts() {
247        return mShortcuts;
248    }
249
250    private void refreshShortcuts(SuggestionCursor promoted) {
251        if (DBG) Log.d(TAG, "refreshShortcuts(" + promoted + ")");
252        for (int i = 0; i < promoted.getCount(); ++i) {
253            promoted.moveTo(i);
254            if (promoted.isSuggestionShortcut()) {
255                getShortcuts().refresh(promoted);
256            }
257        }
258    }
259
260    @Override
261    protected void finalize() {
262        if (!mClosed) {
263            Log.e(TAG, "LEAK! Finalized without being closed: Suggestions[" + getQuery() + "]");
264        }
265    }
266
267    public String getQuery() {
268        return mQuery;
269    }
270
271    public SuggestionCursor getPromoted(Promoter promoter, int maxPromoted) {
272        SuggestionCursor promoted = buildPromoted(promoter, maxPromoted);
273        refreshShortcuts(promoted);
274        return promoted;
275    }
276
277    protected SuggestionCursor buildPromoted(Promoter promoter, int maxPromoted) {
278        ListSuggestionCursor promoted = new ListSuggestionCursorNoDuplicates(mQuery);
279        if (promoter == null) {
280            return promoted;
281        }
282        promoter.pickPromoted(this, maxPromoted, promoted);
283        if (DBG) {
284            Log.d(TAG, "pickPromoted(" + getShortcuts() + "," + mCorpusResults + ","
285                    + maxPromoted + ") = " + promoted);
286        }
287        return promoted;
288    }
289
290    /**
291     * Gets the list of corpus results reported so far. Do not modify or hang on to
292     * the returned iterator.
293     */
294    public Iterable<CorpusResult> getCorpusResults() {
295        return mCorpusResults;
296    }
297
298    public CorpusResult getCorpusResult(Corpus corpus) {
299        for (CorpusResult result : mCorpusResults) {
300            if (result.getCorpus().equals(corpus)) {
301                return result;
302            }
303        }
304        return null;
305    }
306
307    public CorpusResult getWebResult() {
308        return mWebResult;
309    }
310
311    /**
312     * Gets the number of source results.
313     * Must be called on the UI thread, or before this object is seen by the UI thread.
314     */
315    public int getResultCount() {
316        if (isClosed()) {
317            throw new IllegalStateException("Called getSourceCount() when closed.");
318        }
319        return mCorpusResults == null ? 0 : mCorpusResults.size();
320    }
321
322    @Override
323    public String toString() {
324        return "Suggestions@" + hashCode() + "{expectedCorpora=" + mExpectedCorpora
325                + ",mCorpusResults.size()=" + mCorpusResults.size() + "}";
326    }
327
328    private class MyShortcutsObserver extends DataSetObserver {
329        @Override
330        public void onChanged() {
331            notifyDataSetChanged();
332        }
333    }
334
335}
336